opencode-anthropic-fix 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +31 -0
- package/.husky/pre-commit +2 -0
- package/.husky/pre-push +3 -0
- package/.planning/CLI-ENHANCEMENTS.md +221 -0
- package/.planning/codebase/ARCHITECTURE.md +201 -0
- package/.planning/codebase/CONCERNS.md +219 -0
- package/.planning/codebase/CONVENTIONS.md +335 -0
- package/.planning/codebase/INTEGRATIONS.md +215 -0
- package/.planning/codebase/STACK.md +160 -0
- package/.planning/codebase/STRUCTURE.md +244 -0
- package/.planning/codebase/TESTING.md +456 -0
- package/.prettierignore +3 -0
- package/.prettierrc +10 -0
- package/CONTRIBUTING.md +525 -0
- package/LICENSE +21 -0
- package/README.md +478 -0
- package/agents.md +22 -0
- package/bun.lock +43 -0
- package/cli.mjs +1773 -0
- package/cli.test.mjs +1797 -0
- package/docs/mimese-http-header-system-prompt.md +326 -0
- package/docs/plans/afcw-upstream-watcher.json +495 -0
- package/eslint.config.mjs +69 -0
- package/index.mjs +2209 -0
- package/index.test.mjs +2690 -0
- package/lib/accounts.mjs +670 -0
- package/lib/accounts.test.mjs +782 -0
- package/lib/backoff.mjs +217 -0
- package/lib/backoff.test.mjs +296 -0
- package/lib/config.mjs +366 -0
- package/lib/config.test.mjs +307 -0
- package/lib/oauth.mjs +103 -0
- package/lib/rotation.mjs +287 -0
- package/lib/rotation.test.mjs +346 -0
- package/lib/storage.mjs +265 -0
- package/lib/storage.test.mjs +470 -0
- package/nul +1 -0
- package/package.json +40 -0
- package/script/publish.ts +29 -0
- package/scripts/build.mjs +34 -0
- package/scripts/install.mjs +319 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
This document covers the architecture, implementation details, and development workflow for the multi-account fork of `opencode-anthropic-auth`.
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/actualyze-ai/opencode-anthropic-auth.git
|
|
9
|
+
cd opencode-anthropic-auth
|
|
10
|
+
npm install
|
|
11
|
+
npm run install:link # Symlink plugin + CLI for live development
|
|
12
|
+
npm test # Run all tests (~2s)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Running Tests
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm test # Single run
|
|
19
|
+
npm run test:watch # Watch mode
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Tests use [vitest](https://vitest.dev/). All modules are tested in isolation with mocked dependencies.
|
|
23
|
+
|
|
24
|
+
### Linting and Formatting
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run lint # Check for lint errors
|
|
28
|
+
npm run lint:fix # Fix auto-fixable lint errors
|
|
29
|
+
npm run format # Format all files with Prettier
|
|
30
|
+
npm run format:check # Check formatting without writing
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Git hooks enforce quality automatically:
|
|
34
|
+
|
|
35
|
+
- **Pre-commit:** `lint-staged` runs Prettier and ESLint on staged `.mjs` files, Prettier on `.json`/`.md` files
|
|
36
|
+
- **Pre-push:** full test suite + Prettier format check
|
|
37
|
+
|
|
38
|
+
## Project Structure
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
opencode-anthropic-auth/
|
|
42
|
+
index.mjs Plugin entry point (OAuth flow, fetch interceptor, retry loop, slash commands)
|
|
43
|
+
index.test.mjs Plugin integration tests (lifecycle, fetch, transforms, slash commands)
|
|
44
|
+
cli.mjs Standalone CLI (17 subcommands, auth flows, live usage quotas)
|
|
45
|
+
cli.test.mjs CLI command tests (auth + account management + IO capture)
|
|
46
|
+
package.json Dependencies: @openauthjs/openauth (prod), esbuild + vitest + eslint + prettier (dev)
|
|
47
|
+
eslint.config.mjs ESLint flat config
|
|
48
|
+
.prettierrc Prettier config
|
|
49
|
+
.prettierignore Prettier ignore patterns
|
|
50
|
+
.husky/ Git hooks (pre-commit: lint-staged, pre-push: test + format check)
|
|
51
|
+
lib/
|
|
52
|
+
oauth.mjs Shared OAuth helpers (authorize, exchange, revoke) — used by both plugin and CLI
|
|
53
|
+
accounts.mjs AccountManager class (pool management, selection, persistence)
|
|
54
|
+
accounts.test.mjs AccountManager tests
|
|
55
|
+
rotation.mjs HealthScoreTracker, TokenBucketTracker, selectAccount()
|
|
56
|
+
rotation.test.mjs Selection algorithm tests
|
|
57
|
+
backoff.mjs Rate limit parsing, backoff calculation
|
|
58
|
+
backoff.test.mjs Backoff calculation tests
|
|
59
|
+
config.mjs Config loader/saver, validation, env overrides
|
|
60
|
+
config.test.mjs Config loading/validation tests
|
|
61
|
+
storage.mjs Account persistence (atomic writes, deduplication)
|
|
62
|
+
storage.test.mjs Storage persistence tests
|
|
63
|
+
scripts/
|
|
64
|
+
build.mjs esbuild bundler (produces dist/)
|
|
65
|
+
install.mjs Unified installer (link/copy/uninstall)
|
|
66
|
+
dist/ Build output (gitignored)
|
|
67
|
+
opencode-anthropic-auth-plugin.js Bundled plugin (self-contained)
|
|
68
|
+
opencode-anthropic-auth-cli.mjs Bundled CLI (self-contained)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Architecture Overview
|
|
72
|
+
|
|
73
|
+
```mermaid
|
|
74
|
+
graph TB
|
|
75
|
+
subgraph OpenCode
|
|
76
|
+
OC[OpenCode Runtime] -->|loads plugin| Plugin
|
|
77
|
+
OC -->|calls| Fetch[provider.fetch]
|
|
78
|
+
OC -->|/anthropic| SlashCmd[Slash Command Handler]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
subgraph Plugin["index.mjs (Plugin)"]
|
|
82
|
+
Auth[Auth Methods]
|
|
83
|
+
Loader[Auth Loader]
|
|
84
|
+
SysTransform[System Prompt Transform]
|
|
85
|
+
FetchInterceptor[Fetch Interceptor]
|
|
86
|
+
SlashCmd -->|in-process| CLI[cli.mjs dispatch]
|
|
87
|
+
SlashCmd -->|login/reauth| OAuthFlow[Slash OAuth Flow]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
subgraph Core["lib/ (Core)"]
|
|
91
|
+
OAuth[OAuth Helpers]
|
|
92
|
+
AM[AccountManager]
|
|
93
|
+
Rotation[Rotation Engine]
|
|
94
|
+
Backoff[Backoff Calculator]
|
|
95
|
+
Config[Config Loader]
|
|
96
|
+
Storage[Account Storage]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
subgraph External
|
|
100
|
+
Anthropic[Anthropic API]
|
|
101
|
+
Disk[(~/.config/opencode/)]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
Auth -->|authorize/exchange| OAuth
|
|
105
|
+
OAuthFlow -->|authorize/exchange| OAuth
|
|
106
|
+
CLI -->|account commands| Storage
|
|
107
|
+
Auth -->|add account| AM
|
|
108
|
+
Loader -->|init| AM
|
|
109
|
+
FetchInterceptor -->|select account| AM
|
|
110
|
+
FetchInterceptor -->|build headers| Anthropic
|
|
111
|
+
FetchInterceptor -->|on account-specific errors| Backoff
|
|
112
|
+
AM -->|select| Rotation
|
|
113
|
+
AM -->|persist| Storage
|
|
114
|
+
Storage -->|read/write| Disk
|
|
115
|
+
Config -->|read/write| Disk
|
|
116
|
+
OAuth -->|token exchange| Anthropic
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Plugin Lifecycle
|
|
120
|
+
|
|
121
|
+
The plugin integrates with OpenCode through five hooks:
|
|
122
|
+
|
|
123
|
+
```mermaid
|
|
124
|
+
sequenceDiagram
|
|
125
|
+
participant User
|
|
126
|
+
participant OpenCode
|
|
127
|
+
participant Plugin
|
|
128
|
+
participant AccountManager
|
|
129
|
+
participant Anthropic
|
|
130
|
+
|
|
131
|
+
Note over OpenCode,Plugin: Phase 1: Authentication
|
|
132
|
+
User->>OpenCode: Ctrl+K → Connect Provider
|
|
133
|
+
OpenCode->>Plugin: methods[0].authorize()
|
|
134
|
+
Plugin->>User: Show OAuth URL
|
|
135
|
+
User->>Anthropic: Authorize in browser
|
|
136
|
+
User->>Plugin: Paste auth code
|
|
137
|
+
Plugin->>Anthropic: Exchange code for tokens
|
|
138
|
+
Plugin->>AccountManager: addAccount(tokens)
|
|
139
|
+
AccountManager->>AccountManager: saveToDisk()
|
|
140
|
+
Plugin->>OpenCode: Return credentials
|
|
141
|
+
|
|
142
|
+
Note over OpenCode,Plugin: Phase 2: Loader (runs on next API call)
|
|
143
|
+
OpenCode->>Plugin: loader(getAuth, provider)
|
|
144
|
+
Plugin->>Plugin: Zero out model costs
|
|
145
|
+
Plugin->>AccountManager: Load from disk + auth fallback
|
|
146
|
+
Plugin->>OpenCode: Return {apiKey, fetch}
|
|
147
|
+
|
|
148
|
+
Note over OpenCode,Plugin: Phase 3: Request Handling
|
|
149
|
+
User->>OpenCode: Send message
|
|
150
|
+
OpenCode->>Plugin: fetch(url, init)
|
|
151
|
+
Plugin->>AccountManager: getCurrentAccount()
|
|
152
|
+
AccountManager->>AccountManager: selectAccount(strategy)
|
|
153
|
+
Plugin->>Plugin: Refresh token if expired
|
|
154
|
+
Plugin->>Plugin: Transform body + URL + headers
|
|
155
|
+
Plugin->>Anthropic: POST /v1/messages?beta=true
|
|
156
|
+
alt Success (200)
|
|
157
|
+
Anthropic->>Plugin: Response stream
|
|
158
|
+
Plugin->>Plugin: Strip mcp_ prefixes
|
|
159
|
+
Plugin->>OpenCode: Transformed response
|
|
160
|
+
else Account-specific error (429/401 or 400/403 billing/quota/permission)
|
|
161
|
+
Anthropic->>Plugin: Error response
|
|
162
|
+
Plugin->>AccountManager: markRateLimited(account, reason)
|
|
163
|
+
Plugin->>Plugin: Retry with next account (try each account once)
|
|
164
|
+
else Service-wide error (500/503/529)
|
|
165
|
+
Anthropic->>Plugin: Error response
|
|
166
|
+
Plugin->>OpenCode: Return error directly
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Request Transformation Pipeline
|
|
171
|
+
|
|
172
|
+
Every API request goes through a multi-stage transformation:
|
|
173
|
+
|
|
174
|
+
```mermaid
|
|
175
|
+
flowchart LR
|
|
176
|
+
subgraph Input
|
|
177
|
+
Body[Request Body]
|
|
178
|
+
URL[Request URL]
|
|
179
|
+
Headers[Request Headers]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
subgraph Transform
|
|
183
|
+
TB[transformRequestBody]
|
|
184
|
+
TU[transformRequestUrl]
|
|
185
|
+
TH[buildRequestHeaders]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
subgraph Output
|
|
189
|
+
OB["Sanitized body<br/>(OpenCode→Claude Code,<br/>tool name prefixing)"]
|
|
190
|
+
OU["Modified URL<br/>(?beta=true)"]
|
|
191
|
+
OH["OAuth headers<br/>(Bearer token,<br/>anthropic-beta,<br/>user-agent)"]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
Body --> TB --> OB
|
|
195
|
+
URL --> TU --> OU
|
|
196
|
+
Headers --> TH --> OH
|
|
197
|
+
|
|
198
|
+
subgraph Response
|
|
199
|
+
RS[Response Stream] --> Strip["Strip mcp_ prefixes<br/>from tool names"]
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Body Transformations
|
|
204
|
+
|
|
205
|
+
| Step | What | Why |
|
|
206
|
+
| -------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
|
|
207
|
+
| System prompt sanitization | Replace "OpenCode" with "Claude Code", "opencode" with "Claude" (preserves paths like `/path/to/opencode-foo`) | Anthropic's API blocks the string "OpenCode" |
|
|
208
|
+
| Tool definition prefixing | Add `mcp_` prefix to `tools[].name` | Required by Anthropic's OAuth API |
|
|
209
|
+
| Tool use prefixing | Add `mcp_` prefix to `tool_use` blocks in `messages[].content` | Matches the tool definition prefixes |
|
|
210
|
+
|
|
211
|
+
### URL Transformations
|
|
212
|
+
|
|
213
|
+
| Step | What | Why |
|
|
214
|
+
| --------- | ------------------------------------- | --------------------------- |
|
|
215
|
+
| Beta flag | Append `?beta=true` to `/v1/messages` | Enables OAuth beta features |
|
|
216
|
+
|
|
217
|
+
### Header Transformations
|
|
218
|
+
|
|
219
|
+
| Step | What | Why |
|
|
220
|
+
| ---------------- | -------------------------------------------------- | ------------------------------ |
|
|
221
|
+
| Authorization | `Bearer <access_token>` | OAuth authentication |
|
|
222
|
+
| Beta headers | `oauth-2025-04-20,interleaved-thinking-2025-05-14` | Required beta features |
|
|
223
|
+
| User agent | `claude-cli/2.1.2 (external, cli)` | Identifies as Claude CLI |
|
|
224
|
+
| Remove x-api-key | Delete if present | OAuth uses Bearer, not API key |
|
|
225
|
+
|
|
226
|
+
### Response Transformations
|
|
227
|
+
|
|
228
|
+
| Step | What | Why |
|
|
229
|
+
| -------------- | -------------------------------------------- | ---------------------------- |
|
|
230
|
+
| Strip prefixes | Remove `mcp_` from `"name"` fields in stream | Undo the tool name prefixing |
|
|
231
|
+
|
|
232
|
+
## Account Selection Engine
|
|
233
|
+
|
|
234
|
+
### Strategy: Sticky (Default)
|
|
235
|
+
|
|
236
|
+
```mermaid
|
|
237
|
+
flowchart TD
|
|
238
|
+
Start[Select Account] --> HasCurrent{Current account<br/>exists?}
|
|
239
|
+
HasCurrent -->|Yes| IsAvailable{Available and<br/>healthy?}
|
|
240
|
+
IsAvailable -->|Yes| UseCurrent[Use current account]
|
|
241
|
+
IsAvailable -->|No| NextAvailable[Use next available<br/>via cursor]
|
|
242
|
+
HasCurrent -->|No| NextAvailable
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Stays on one account until it fails or is rate-limited. Best for single-account use.
|
|
246
|
+
|
|
247
|
+
### Strategy: Round-Robin
|
|
248
|
+
|
|
249
|
+
```mermaid
|
|
250
|
+
flowchart TD
|
|
251
|
+
Start[Select Account] --> Filter[Filter enabled,<br/>non-rate-limited]
|
|
252
|
+
Filter --> Pick["Pick accounts[cursor % count]"]
|
|
253
|
+
Pick --> Advance[cursor++]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Rotates through accounts on every request. Spreads load evenly.
|
|
257
|
+
|
|
258
|
+
### Strategy: Hybrid
|
|
259
|
+
|
|
260
|
+
```mermaid
|
|
261
|
+
flowchart TD
|
|
262
|
+
Start[Select Account] --> Filter[Filter enabled,<br/>non-rate-limited,<br/>healthy, has tokens]
|
|
263
|
+
Filter --> Score[Calculate hybrid score<br/>for each candidate]
|
|
264
|
+
Score --> Stickiness[Add stickiness bonus<br/>to current account]
|
|
265
|
+
Stickiness --> Compare{Best score vs<br/>current score}
|
|
266
|
+
Compare -->|"Advantage < threshold"| KeepCurrent[Keep current account]
|
|
267
|
+
Compare -->|"Advantage >= threshold"| SwitchBest[Switch to best account]
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Hybrid score formula:**
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
score = (healthScore × 2) + ((tokens / maxTokens) × 500) + min(secondsSinceUsed, 3600) × 0.1
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
- **Health component** (0-200): Rewards reliable accounts
|
|
277
|
+
- **Token component** (0-500): Prefers accounts with available rate limit budget (`tokens / maxTokens` ratio scaled to 0-500)
|
|
278
|
+
- **Freshness component** (0-360): Slight preference for less recently used accounts
|
|
279
|
+
- **Stickiness bonus** (+150): Added to current account to prevent unnecessary switching
|
|
280
|
+
- **Switch threshold** (100): Must beat current by this much to trigger a switch
|
|
281
|
+
|
|
282
|
+
### Health Score System
|
|
283
|
+
|
|
284
|
+
Each account has a health score (0-100) that tracks reliability:
|
|
285
|
+
|
|
286
|
+
| Event | Score Change |
|
|
287
|
+
| ------------------ | ------------ |
|
|
288
|
+
| Successful request | +1 |
|
|
289
|
+
| Rate limited | -10 |
|
|
290
|
+
| General failure | -20 |
|
|
291
|
+
| Passive recovery | +2 per hour |
|
|
292
|
+
|
|
293
|
+
Accounts below `min_usable` (default: 50) are skipped by the hybrid strategy.
|
|
294
|
+
|
|
295
|
+
### Token Bucket Rate Limiting
|
|
296
|
+
|
|
297
|
+
Client-side rate limiting prevents sending requests to accounts that are likely to be rate-limited:
|
|
298
|
+
|
|
299
|
+
| Parameter | Default | Description |
|
|
300
|
+
| ------------------------------ | ------- | -------------------------------- |
|
|
301
|
+
| `max_tokens` | 50 | Maximum tokens per account |
|
|
302
|
+
| `regeneration_rate_per_minute` | 6 | Tokens regenerated per minute |
|
|
303
|
+
| `initial_tokens` | 50 | Starting tokens for new accounts |
|
|
304
|
+
|
|
305
|
+
Each request consumes 1 token. When an account runs out, it's skipped until tokens regenerate.
|
|
306
|
+
|
|
307
|
+
## Rate Limit Handling
|
|
308
|
+
|
|
309
|
+
### Error Classification
|
|
310
|
+
|
|
311
|
+
| HTTP Status / Body Classifier | Parsed Reason | Default Backoff |
|
|
312
|
+
| -------------------------------------------------- | --------------------- | ---------------------------- |
|
|
313
|
+
| 429 | `RATE_LIMIT_EXCEEDED` | 30s |
|
|
314
|
+
| 401 | `AUTH_FAILED` | 5s |
|
|
315
|
+
| 400/403 with quota/billing/auth/permission signals | `QUOTA_EXHAUSTED` | 1m, 5m, 30m, 2h (escalating) |
|
|
316
|
+
| 400/403 with rate-limit signals | `RATE_LIMIT_EXCEEDED` | 30s |
|
|
317
|
+
| 500/503/529 | service-wide | no account switching |
|
|
318
|
+
|
|
319
|
+
The `Retry-After` header always takes precedence over calculated backoffs.
|
|
320
|
+
|
|
321
|
+
### Retry Flow
|
|
322
|
+
|
|
323
|
+
```mermaid
|
|
324
|
+
flowchart TD
|
|
325
|
+
Request[Send Request] --> Check{Response OK?}
|
|
326
|
+
Check -->|200| Success[Mark success,<br/>return response]
|
|
327
|
+
Check -->|500/503/529| ReturnError[Return error response]
|
|
328
|
+
Check -->|429/401/400/403| Classify{Account-specific?}
|
|
329
|
+
Classify -->|No| ReturnError
|
|
330
|
+
Classify -->|Yes| Parse[Parse error reason]
|
|
331
|
+
Parse --> Mark[Mark account rate-limited]
|
|
332
|
+
Mark --> Attempts{Untried accounts left?}
|
|
333
|
+
Attempts -->|Yes| NextAccount[Select next account]
|
|
334
|
+
Attempts -->|No| Throw[Throw account exhausted]
|
|
335
|
+
NextAccount --> Request
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Account Storage
|
|
339
|
+
|
|
340
|
+
### File Format
|
|
341
|
+
|
|
342
|
+
Accounts are stored at `~/.config/opencode/anthropic-accounts.json`:
|
|
343
|
+
|
|
344
|
+
```json
|
|
345
|
+
{
|
|
346
|
+
"version": 1,
|
|
347
|
+
"accounts": [
|
|
348
|
+
{
|
|
349
|
+
"email": "alice@example.com",
|
|
350
|
+
"refreshToken": "rt_...",
|
|
351
|
+
"addedAt": 1706000000000,
|
|
352
|
+
"lastUsed": 1706000100000,
|
|
353
|
+
"enabled": true,
|
|
354
|
+
"rateLimitResetTimes": {},
|
|
355
|
+
"consecutiveFailures": 0,
|
|
356
|
+
"lastFailureTime": null
|
|
357
|
+
}
|
|
358
|
+
],
|
|
359
|
+
"activeIndex": 0
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Safety Measures
|
|
364
|
+
|
|
365
|
+
- **Atomic writes:** Write to temp file, then rename (prevents corruption on crash)
|
|
366
|
+
- **File permissions:** `0600` (owner read/write only)
|
|
367
|
+
- **Gitignore:** Auto-generated `.gitignore` in the config directory
|
|
368
|
+
- **Deduplication:** Accounts with the same refresh token are merged (keeps most recently used)
|
|
369
|
+
- **Debounced saves:** The plugin debounces disk writes to 1 second to avoid excessive I/O
|
|
370
|
+
- **Max accounts:** Hard limit of 10 accounts
|
|
371
|
+
|
|
372
|
+
## OpenCode Plugin API
|
|
373
|
+
|
|
374
|
+
### How Plugins Work
|
|
375
|
+
|
|
376
|
+
OpenCode plugins export an async function that receives a `{ client }` object and returns hooks:
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
export async function AnthropicAuthPlugin({ client }) {
|
|
380
|
+
return {
|
|
381
|
+
// Hook: Register slash commands
|
|
382
|
+
config: async (input) => {
|
|
383
|
+
input.command ??= {};
|
|
384
|
+
input.command["anthropic"] = {
|
|
385
|
+
template: "/anthropic",
|
|
386
|
+
description: "Manage Anthropic multi-account auth",
|
|
387
|
+
};
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
// Hook: Handle slash commands (before default execution)
|
|
391
|
+
"command.execute.before": async (input) => {
|
|
392
|
+
if (input.command !== "anthropic") return;
|
|
393
|
+
// Handle /anthropic subcommands...
|
|
394
|
+
throw new Error("__HANDLED__"); // Prevents default handler
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
// Hook: Transform system prompts
|
|
398
|
+
"experimental.chat.system.transform": (input, output) => { ... },
|
|
399
|
+
|
|
400
|
+
// Hook: Authentication
|
|
401
|
+
auth: {
|
|
402
|
+
provider: "anthropic", // Which provider this plugin handles
|
|
403
|
+
loader(getAuth, provider) { ... }, // Called after auth is stored
|
|
404
|
+
methods: [ // Auth methods shown in Connect dialog
|
|
405
|
+
{ label: "...", type: "oauth", authorize: async () => { ... } },
|
|
406
|
+
{ label: "...", type: "api" },
|
|
407
|
+
],
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Key Integration Points
|
|
414
|
+
|
|
415
|
+
| Hook | When It Runs | What It Does |
|
|
416
|
+
| ------------------------------------ | ------------------------------------------- | -------------------------------------------------- |
|
|
417
|
+
| `config` | Plugin initialization | Registers `/anthropic` slash command |
|
|
418
|
+
| `command.execute.before` | User runs `/anthropic ...` | Routes to slash command handler (in-process CLI) |
|
|
419
|
+
| `auth.methods[].authorize()` | User clicks "Connect Provider" | Starts OAuth flow, returns URL + callback |
|
|
420
|
+
| `auth.methods[].callback(code)` | User pastes auth code | Exchanges code for tokens, adds account |
|
|
421
|
+
| `auth.loader(getAuth, provider)` | After auth is stored, on each state refresh | Initializes AccountManager, returns custom `fetch` |
|
|
422
|
+
| `experimental.chat.system.transform` | Before each API call | Prepends "Claude Code" prefix to system prompt |
|
|
423
|
+
|
|
424
|
+
### Slash Command Architecture
|
|
425
|
+
|
|
426
|
+
The `/anthropic` slash command runs CLI commands **deterministically in-process** (no subprocess spawning or PATH resolution). The plugin calls `cliMain(argv, { io })` directly, capturing console output via `AsyncLocalStorage`-based IO routing.
|
|
427
|
+
|
|
428
|
+
OAuth flows (`login`, `reauth`) are two-step in slash mode:
|
|
429
|
+
|
|
430
|
+
1. `/anthropic login` or `/anthropic reauth <N>` — starts OAuth flow, stores PKCE verifier in-memory with 10-minute TTL
|
|
431
|
+
2. `/anthropic login complete <code#state>` or `/anthropic reauth complete <code#state>` — exchanges code for tokens
|
|
432
|
+
|
|
433
|
+
Destructive commands (`remove`, `logout`) auto-inject `--force` to avoid readline prompts. Interactive `manage` is blocked with guidance to use granular commands.
|
|
434
|
+
|
|
435
|
+
### The `loader` Return Value
|
|
436
|
+
|
|
437
|
+
The loader returns an object with `apiKey` and `fetch`:
|
|
438
|
+
|
|
439
|
+
```javascript
|
|
440
|
+
return {
|
|
441
|
+
apiKey: "", // Empty string (OAuth doesn't use API keys)
|
|
442
|
+
async fetch(input, init) {
|
|
443
|
+
// This replaces the default fetch for all Anthropic API calls
|
|
444
|
+
// Handles: account selection, token refresh, request transformation,
|
|
445
|
+
// retry loop, response transformation
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
OpenCode calls this `fetch` function instead of the global `fetch` for all requests to the Anthropic provider.
|
|
451
|
+
|
|
452
|
+
## Known Limitations
|
|
453
|
+
|
|
454
|
+
### Custom Provider Namespace
|
|
455
|
+
|
|
456
|
+
We investigated using a custom provider ID (e.g., `anthropic-oauth`) to avoid the conflict entirely. This doesn't work because OpenCode's `mergeProvider()` silently drops providers not found in the models.dev database:
|
|
457
|
+
|
|
458
|
+
```javascript
|
|
459
|
+
function mergeProvider(providerID, provider) {
|
|
460
|
+
const existing = providers[providerID];
|
|
461
|
+
if (existing) {
|
|
462
|
+
/* merge */ return;
|
|
463
|
+
}
|
|
464
|
+
const match = database[providerID];
|
|
465
|
+
if (!match) return; // ← silently drops unknown providers
|
|
466
|
+
/* ... */
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
A custom provider requires `opencode.json` config with at least one model definition to bootstrap into the database. This adds significant config management overhead for marginal benefit.
|
|
471
|
+
|
|
472
|
+
## Testing
|
|
473
|
+
|
|
474
|
+
### Test Structure
|
|
475
|
+
|
|
476
|
+
| File | Tests | Coverage |
|
|
477
|
+
| ------------------- | ----------------------------------------------------------- | ---------------------------- |
|
|
478
|
+
| `backoff.test.mjs` | Rate limit parsing, backoff calculation | `lib/backoff.mjs` |
|
|
479
|
+
| `rotation.test.mjs` | Health scores, token buckets, selection algorithms | `lib/rotation.mjs` |
|
|
480
|
+
| `config.test.mjs` | Config loading, validation, env overrides | `lib/config.mjs` |
|
|
481
|
+
| `storage.test.mjs` | Account persistence, deduplication, atomic writes | `lib/storage.mjs` |
|
|
482
|
+
| `accounts.test.mjs` | AccountManager lifecycle, pool management, empty bootstrap | `lib/accounts.mjs` |
|
|
483
|
+
| `index.test.mjs` | Plugin lifecycle, fetch interceptor, transforms, slash cmds | `index.mjs`, `lib/oauth.mjs` |
|
|
484
|
+
| `cli.test.mjs` | CLI auth + account commands, IO capture, live usage quotas | `cli.mjs` |
|
|
485
|
+
|
|
486
|
+
### Writing Tests
|
|
487
|
+
|
|
488
|
+
Tests mock filesystem and network calls. Use the existing patterns:
|
|
489
|
+
|
|
490
|
+
```javascript
|
|
491
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
492
|
+
|
|
493
|
+
// Mock dependencies before importing the module under test
|
|
494
|
+
vi.mock("./lib/storage.mjs", () => ({
|
|
495
|
+
loadAccounts: vi.fn(),
|
|
496
|
+
saveAccounts: vi.fn(),
|
|
497
|
+
getStoragePath: vi.fn(() => "/mock/path"),
|
|
498
|
+
}));
|
|
499
|
+
|
|
500
|
+
// Import after mocking
|
|
501
|
+
import { cmdList } from "./cli.mjs";
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Running Specific Tests
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
npx vitest run backoff # Run backoff tests only
|
|
508
|
+
npx vitest run --reporter=verbose # Verbose output
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Dependencies
|
|
512
|
+
|
|
513
|
+
| Package | Type | Purpose |
|
|
514
|
+
| ---------------------- | ---------- | --------------------------------------------------------- |
|
|
515
|
+
| `@openauthjs/openauth` | Production | PKCE code generation for OAuth flow |
|
|
516
|
+
| `@opencode-ai/plugin` | Dev | Plugin API type definitions (used via JSDoc) |
|
|
517
|
+
| `esbuild` | Dev | Bundles plugin + CLI into single files |
|
|
518
|
+
| `vitest` | Dev | Test runner |
|
|
519
|
+
| `eslint` | Dev | Linter (flat config) |
|
|
520
|
+
| `@eslint/js` | Dev | ESLint recommended rules |
|
|
521
|
+
| `prettier` | Dev | Code formatter |
|
|
522
|
+
| `husky` | Dev | Git hooks (pre-commit: lint-staged, pre-push: test + fmt) |
|
|
523
|
+
| `lint-staged` | Dev | Runs prettier + eslint on staged files |
|
|
524
|
+
|
|
525
|
+
The plugin has **one production dependency** (`@openauthjs/openauth`), which is bundled into the dist output by esbuild. The bundled files have zero external dependencies beyond Node.js built-ins.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SST
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|