@cortexkit/opencode-anthropic-auth 1.7.0 → 1.9.0

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/README.md CHANGED
@@ -19,6 +19,7 @@ This repo is a Bun workspace monorepo with two user-facing integrations and one
19
19
  | Primary Claude Pro/Max OAuth | OpenCode `/connect anthropic` | Pi `/login anthropic` |
20
20
  | Provider integration point | OpenCode plugin fetch/request transform | Pi `registerProvider("anthropic")` provider override |
21
21
  | Sidecar config | `~/.config/opencode/anthropic-auth.json` | `~/.pi/agent/anthropic-auth.json` |
22
+ | Runtime state | `~/.config/opencode/anthropic-auth-state.json` | next to the Pi sidecar as `anthropic-auth-state.json` |
22
23
  | Commands | `/claude-cache`, `/claude-cachekeep`, `/claude-routing`, `/claude-fast`, `/claude-quota`, `/claude-dump`, `/claude-killswitch` | `/claude-cache`, `/claude-cachekeep`, `/claude-routing`, `/claude-fast`, `/claude-quota`, `/claude-dump` |
23
24
  | Fallback accounts, quota routing, killswitch, relay, dumps, fast mode | Supported | Supported through the same shared core and Pi sidecar |
24
25
 
@@ -195,6 +196,9 @@ Example:
195
196
  "claudeFast": {
196
197
  "enabled": false
197
198
  },
199
+ "costZeroing": {
200
+ "enabled": true
201
+ },
198
202
  "relay": {
199
203
  "enabled": false,
200
204
  "url": "https://opencode-anthropic-relay.example.workers.dev",
@@ -206,11 +210,13 @@ Example:
206
210
  }
207
211
  ```
208
212
 
209
- The `routing` block controls `/claude-routing`, `claudeCache` controls `/claude-cache`, `cacheKeep` controls `/claude-cachekeep`, and `claudeFast` controls `/claude-fast`. Set `quota.showToasts` to `true` to opt into OpenCode quota toast notifications after quota refreshes. The `main` field identifies OpenCode's primary auth entry; Pi keeps primary OAuth credentials in Pi's own credential store, but uses the same sidecar shape for CortexKit settings and fallback accounts.
213
+ The `routing` block controls `/claude-routing`, `claudeCache` controls `/claude-cache`, `cacheKeep` controls `/claude-cachekeep`, and `claudeFast` controls `/claude-fast`. OpenCode zeroes Anthropic OAuth model costs by default because OAuth usage is quota-based; set `costZeroing.enabled` to `false` only if you want OpenCode to display the provider's model pricing instead. Set `quota.showToasts` to `true` to opt into OpenCode quota toast notifications after quota refreshes. The `main` field identifies OpenCode's primary auth entry; Pi keeps primary OAuth credentials in Pi's own credential store, but uses the same sidecar shape for CortexKit settings and fallback account labels.
214
+
215
+ Runtime data is stored separately in `anthropic-auth-state.json`: fallback OAuth tokens, API-route keys, token refresh backoff, quota snapshots, and quota API backoff. Background refresh and quota checks write only the state file, so editing `anthropic-auth.json` does not get overwritten by another running plugin instance.
210
216
 
211
217
  ## Fallback accounts
212
218
 
213
- Fallback accounts are separate Claude OAuth accounts managed by this plugin. By default, the main account is tried first unless quota policy says it is currently unusable. Fallbacks are then tried in sidecar order when the primary request returns a configured fallback status.
219
+ Fallback accounts are separate Claude OAuth accounts or Anthropic-compatible API-key routes managed by this plugin. By default, the main account is tried first unless quota policy says it is currently unusable. Fallbacks are then tried in sidecar order when the primary request returns a configured fallback status.
214
220
 
215
221
  Use `/claude-routing fallback-first` to prefer usable fallback accounts before the main account. Use `/claude-routing main-first` to restore the default. The command persists `routing.mode` and takes effect on the next request without restarting.
216
222
 
@@ -224,11 +230,16 @@ Add and inspect OpenCode fallback accounts with the CLI:
224
230
 
225
231
  ```bash
226
232
  bunx @cortexkit/opencode-anthropic-auth login personal-alt
233
+ bunx @cortexkit/opencode-anthropic-auth api add kie-opus
227
234
  bunx @cortexkit/opencode-anthropic-auth list
228
235
  ```
229
236
 
230
237
  Prefer npm? Use `npx -y @cortexkit/opencode-anthropic-auth ...` with the same subcommands.
231
238
 
239
+ API fallback routes use `Authorization: Bearer <key>` by default and rewrite requests to the configured Anthropic-compatible base URL. For Kie, use `https://api.kie.ai/claude` as the base URL; the plugin appends `/v1/messages` automatically. API-key routes are only eligible after direct evidence that the main OAuth quota is exhausted: a fresh token-bound quota snapshot at 0% remaining, or an actual main OAuth model response with HTTP 429 / streaming rate-limit error followed by a live quota check that confirms 0% remaining. Low-but-nonzero quota, stale cached quota, unconfirmed 429s, 401, or 403 do not trigger API-key routes. API-route keys are stored in `anthropic-auth-state.json`, while `anthropic-auth.json` keeps the route label, type, enabled flag, base URL, and auth-header mode.
240
+
241
+ OpenCode cost accounting stays simple: when the native Anthropic auth entry is OAuth, OpenCode sees zero-cost Claude OAuth models by default. If a request falls through to an API-key route, token accounting is still recorded, but OpenCode's built-in dollar cost is not route-aware. Advanced users can set `costZeroing.enabled` to `false` to show Anthropic model pricing for OAuth sessions too.
242
+
232
243
  For Pi fallback accounts, write the same account JSON shape to `~/.pi/agent/anthropic-auth.json`. The CLI helper currently lives in the OpenCode package, so you can also point it at Pi's sidecar path when logging in a fallback account:
233
244
 
234
245
  ```bash
@@ -363,7 +374,7 @@ Both OpenCode and Pi packages can persistently request Anthropic fast mode for s
363
374
  /claude-fast off
364
375
  ```
365
376
 
366
- When enabled, supported requests add `speed: "fast"` to the Anthropic JSON body and include the `fast-mode-2026-02-01` beta header. Unsupported models are left at standard speed. Anthropic currently documents fast mode for `claude-opus-4-6`, `claude-opus-4-7`, and `claude-opus-4-8`.
377
+ When enabled, supported requests add `speed: "fast"` to the Anthropic JSON body and include the `fast-mode-2026-02-01` beta header. Unsupported models are left at standard speed. Anthropic currently documents fast mode for `claude-opus-4-6`, `claude-opus-4-7`, and `claude-opus-4-8`; Claude Fable 5 and Mythos 5 are not fast-mode models.
367
378
 
368
379
  Fast and standard speeds do not share prompt-cache prefixes, so switching this setting can cause cache misses.
369
380
 
@@ -585,19 +596,19 @@ This repo uses CortexKit's tag-driven release workflow.
585
596
  Preview a release:
586
597
 
587
598
  ```bash
588
- ./scripts/release.sh 1.8.0 --dry
599
+ ./scripts/release.sh 1.9.0 --dry
589
600
  ```
590
601
 
591
602
  Create and push the release tag:
592
603
 
593
604
  ```bash
594
- ./scripts/release.sh 1.8.0
605
+ ./scripts/release.sh 1.9.0
595
606
  ```
596
607
 
597
608
  Wait for GitHub Actions:
598
609
 
599
610
  ```bash
600
- ./scripts/wait-release.sh v1.8.0
611
+ ./scripts/wait-release.sh v1.9.0
601
612
  ```
602
613
 
603
614
  The release workflow runs checks, publishes the core, OpenCode, and Pi packages to npm with provenance, and creates the GitHub release.
package/dist/cli.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import{Aa as k,m as y,n as w,p as l,q as u,r as m,ya as b}from"./index-ykvgffhr.js";import{stdin as A,stdout as $}from"node:process";import{createInterface as R}from"node:readline/promises";function v(){return{version:1,main:{type:"opencode",provider:"anthropic"},fallbackOn:[401,403,429],refresh:{enabled:!0,intervalMinutes:10,refreshBeforeExpiryMinutes:240},quota:{enabled:!0,checkIntervalMinutes:5,minimumRemaining:{five_hour:10,seven_day:20},failClosedOnUnknownQuota:!0},accounts:[]}}function h(){console.log(`Usage:
2
+ import{Ja as I,La as $,m as w,n as k,p as A,r as R,t as d,u as l,v as p}from"./index-taa0ezxk.js";import{stdin as E,stdout as U}from"node:process";import{createInterface as _}from"node:readline/promises";function g(){return{version:1,main:{type:"opencode",provider:"anthropic"},fallbackOn:[401,403,429],refresh:{enabled:!0,intervalMinutes:10,refreshBeforeExpiryMinutes:240},quota:{enabled:!0,checkIntervalMinutes:5,minimumRemaining:{five_hour:10,seven_day:20},failClosedOnUnknownQuota:!0},accounts:[]}}function y(){console.log(`Usage:
3
3
  opencode-anthropic-auth login [label]
4
+ opencode-anthropic-auth api add [label]
4
5
  opencode-anthropic-auth list
5
6
  opencode-anthropic-auth relay setup
6
7
 
7
8
  Fallback accounts are stored in:
8
- ${l()}`)}function p(e,t){let a=e?.trim();if(!a)throw Error(`${t} is required`);return a}async function d(e){let t=await fetch(`https://api.cloudflare.com/client/v4${e.path}`,{method:e.method,headers:{authorization:`Bearer ${e.token}`,...e.body instanceof FormData?{}:{"content-type":"application/json"},...e.headers},body:e.body}),a=await t.text(),n;try{n=JSON.parse(a)}catch{throw Error(`Cloudflare API returned ${t.status}: ${a}`)}if(!t.ok||n.success===!1){let r=n.errors?.map((o)=>o.message).filter(Boolean).join("; ");throw Error(r||`Cloudflare API returned ${t.status}`)}return n.result}async function E(e,t,a){return d({token:e,method:"POST",path:`/accounts/${t}/storage/kv/namespaces`,body:JSON.stringify({title:a})})}async function I(e){let t={main_module:"worker.js",compatibility_date:"2026-04-28",bindings:[{type:"kv_namespace",name:"RELAY_STATE",namespace_id:e.kvNamespaceId},{type:"secret_text",name:"RELAY_TOKEN",text:e.relayToken}]},a=new FormData;return a.set("metadata",JSON.stringify(t)),a.set("worker.js",new Blob([k],{type:"application/javascript+module"}),"worker.js"),d({token:e.token,method:"PUT",path:`/accounts/${e.accountId}/workers/scripts/${e.scriptName}`,body:a})}async function T(e,t,a){await d({token:e,method:"POST",path:`/accounts/${t}/workers/scripts/${a}/subdomain`,body:JSON.stringify({enabled:!0,previews_enabled:!1})})}async function C(e,t){return d({token:e,method:"GET",path:`/accounts/${t}/workers/subdomain`}).catch(()=>null)}async function S(){let e=await u()??v(),t=p(process.env.CLOUDFLARE_API_TOKEN?.trim()||await c("Cloudflare API token: "),"Cloudflare API token"),a=p(process.env.CLOUDFLARE_ACCOUNT_ID||await c("Cloudflare account ID: "),"Cloudflare account ID"),n=await c("Worker name [opencode-anthropic-relay]: ")||"opencode-anthropic-relay",r=`${n}-state`,o=b();console.log("Creating Cloudflare KV namespace...");let s=await E(t,a,r);console.log("Uploading relay Worker..."),await I({token:t,accountId:a,scriptName:n,kvNamespaceId:s.id,relayToken:o}),await T(t,a,n).catch((f)=>{console.warn(`Could not enable workers.dev automatically: ${f instanceof Error?f.message:String(f)}`)});let i=await C(t,a),g=(i?.subdomain?`https://${n}.${i.subdomain}.workers.dev`:"")||p(await c("Relay Worker URL: "),"Relay Worker URL");e.relay={enabled:!0,url:g,token:o,fallbackToDirect:!0,transport:"http"},await m(e),console.log(`Relay enabled at ${g}`),console.log(`Config saved to ${l()}.`)}async function c(e){let t=R({input:A,output:$});try{return(await t.question(e)).trim()}finally{t.close()}}function O(e,t){let a=e.accounts.findIndex((n)=>n.id===t.id||t.label&&n.label===t.label);if(a>=0){e.accounts[a]={...e.accounts[a],...t,addedAt:e.accounts[a]?.addedAt??t.addedAt,quota:t.quota,lastRefreshedAt:t.lastRefreshedAt,lastRefreshError:t.lastRefreshError,lastQuotaRefreshError:t.lastQuotaRefreshError};return}e.accounts.push(t)}async function U(e){let t=await u()??v(),a=e?.trim()||await c("Fallback account label (optional): "),n=await y("max");console.log(`
9
+ ${d()}`)}function h(e,t){let a=e?.trim();if(!a)throw Error(`${t} is required`);return a}async function f(e){let t=await fetch(`https://api.cloudflare.com/client/v4${e.path}`,{method:e.method,headers:{authorization:`Bearer ${e.token}`,...e.body instanceof FormData?{}:{"content-type":"application/json"},...e.headers},body:e.body}),a=await t.text(),n;try{n=JSON.parse(a)}catch{throw Error(`Cloudflare API returned ${t.status}: ${a}`)}if(!t.ok||n.success===!1){let r=n.errors?.map((o)=>o.message).filter(Boolean).join("; ");throw Error(r||`Cloudflare API returned ${t.status}`)}return n.result}async function P(e,t,a){return f({token:e,method:"POST",path:`/accounts/${t}/storage/kv/namespaces`,body:JSON.stringify({title:a})})}async function T(e){let t={main_module:"worker.js",compatibility_date:"2026-04-28",bindings:[{type:"kv_namespace",name:"RELAY_STATE",namespace_id:e.kvNamespaceId},{type:"secret_text",name:"RELAY_TOKEN",text:e.relayToken}]},a=new FormData;return a.set("metadata",JSON.stringify(t)),a.set("worker.js",new Blob([$],{type:"application/javascript+module"}),"worker.js"),f({token:e.token,method:"PUT",path:`/accounts/${e.accountId}/workers/scripts/${e.scriptName}`,body:a})}async function C(e,t,a){await f({token:e,method:"POST",path:`/accounts/${t}/workers/scripts/${a}/subdomain`,body:JSON.stringify({enabled:!0,previews_enabled:!1})})}async function O(e,t){return f({token:e,method:"GET",path:`/accounts/${t}/workers/subdomain`}).catch(()=>null)}async function N(){let e=await l()??g(),t=h(process.env.CLOUDFLARE_API_TOKEN?.trim()||await s("Cloudflare API token: "),"Cloudflare API token"),a=h(process.env.CLOUDFLARE_ACCOUNT_ID||await s("Cloudflare account ID: "),"Cloudflare account ID"),n=await s("Worker name [opencode-anthropic-relay]: ")||"opencode-anthropic-relay",r=`${n}-state`,o=I();console.log("Creating Cloudflare KV namespace...");let i=await P(t,a,r);console.log("Uploading relay Worker..."),await T({token:t,accountId:a,scriptName:n,kvNamespaceId:i.id,relayToken:o}),await C(t,a,n).catch((m)=>{console.warn(`Could not enable workers.dev automatically: ${m instanceof Error?m.message:String(m)}`)});let c=await O(t,a),b=(c?.subdomain?`https://${n}.${c.subdomain}.workers.dev`:"")||h(await s("Relay Worker URL: "),"Relay Worker URL");e.relay={enabled:!0,url:b,token:o,fallbackToDirect:!0,transport:"http"},await p(e),console.log(`Relay enabled at ${b}`),console.log(`Config saved to ${d()}.`)}var u=null;async function s(e){return u??=_({input:E,output:U}),(await u.question(e)).trim()}function L(){u?.close(),u=null}function v(e,t){let a=e.accounts.findIndex((n)=>n.id===t.id||t.label&&n.label===t.label);if(a>=0){e.accounts[a]={...e.accounts[a],...t,addedAt:e.accounts[a]?.addedAt??t.addedAt,...t.type==="oauth"&&{quota:t.quota,lastRefreshedAt:t.lastRefreshedAt,lastRefreshError:t.lastRefreshError,lastQuotaRefreshError:t.lastQuotaRefreshError}};return}e.accounts.push(t)}async function S(e){let t=await l()??g(),a=e?.trim()||await s("Fallback account label (optional): "),n=await w("max");console.log(`
9
10
  Open this URL in your browser and complete Claude sign-in:
10
11
  `),console.log(`${n.url}
11
- `);let r=await c("Paste the full callback URL or authorization code here: "),o=await w(r,n.verifier,n.redirectUri,n.state);if(o.type==="failed")throw Error("Authentication failed");let s=Date.now();O(t,{id:a||crypto.randomUUID(),label:a||void 0,type:"oauth",access:o.access,refresh:o.refresh,expires:o.expires,enabled:!0,addedAt:s,lastUsed:s}),await m(t),console.log(`
12
- Saved fallback account${a?` "${a}"`:""}.`)}async function _(){let e=await u();if(!e?.accounts.length){console.log(`No fallback accounts found at ${l()}.`);return}for(let[t,a]of e.accounts.entries()){let n=a.label||a.id,r=a.enabled===!1?"disabled":"enabled",o=a.quota?.five_hour?.remainingPercent,s=a.quota?.seven_day?.remainingPercent,i=o===void 0&&s===void 0?"quota unknown":`5h ${o??"?"}%, 1w ${s??"?"}% remaining`;console.log(`${t+1}. ${n} (${r}) — ${i}`)}}async function x(){let[e,t]=process.argv.slice(2);if(!e||e==="help"||e==="--help"||e==="-h"){h();return}if(e==="login"){await U(t);return}if(e==="list"){await _();return}if(e==="relay"&&t==="setup"){await S();return}h(),process.exitCode=1}try{await x()}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exitCode=1}
12
+ `);let r=await s("Paste the full callback URL or authorization code here: "),o=await k(r,n.verifier,n.redirectUri,n.state);if(o.type==="failed")throw Error("Authentication failed");let i=Date.now();v(t,{id:a||crypto.randomUUID(),label:a||void 0,type:"oauth",access:o.access,refresh:o.refresh,expires:o.expires,enabled:!0,addedAt:i,lastUsed:i}),await p(t),console.log(`
13
+ Saved fallback account${a?` "${a}"`:""}.`)}async function x(e){let t=await l()??g(),a=e?.trim()||await s("API fallback label (optional): "),n=process.env.OPENCODE_ANTHROPIC_AUTH_API_BASE_URL?.trim()||(await s("Anthropic-compatible base URL [https://api.kie.ai/claude]: ")).trim()||"https://api.kie.ai/claude";if(!R(n))throw Error("API fallback base URL must be an http(s) URL without embedded credentials");let r=process.env.OPENCODE_ANTHROPIC_AUTH_API_KEY?.trim()||await s("API key: ");if(!r.trim())throw Error("API key is required");let i=(process.env.OPENCODE_ANTHROPIC_AUTH_API_AUTH_HEADER?.trim()||await s("Auth header [authorization-bearer|x-api-key] (default authorization-bearer): ")).trim().toLowerCase()==="x-api-key"?"x-api-key":"authorization-bearer",c=Date.now();v(t,{id:a||crypto.randomUUID(),label:a||void 0,type:"api",apiKey:r.trim(),baseURL:n,authHeader:i,enabled:!0,addedAt:c,lastUsed:c}),await p(t),console.log(`
14
+ Saved API fallback route${a?` "${a}"`:""} (${n}).`)}async function D(){let e=await l();if(!e?.accounts.length){console.log(`No fallback accounts found at ${d()}.`);return}for(let[t,a]of e.accounts.entries()){let n=a.label||a.id,r=a.enabled===!1?"disabled":"enabled";if(!A(a)){console.log(`${t+1}. ${n} (${r}) — API route ${a.baseURL}`);continue}let o=a.quota?.five_hour?.remainingPercent,i=a.quota?.seven_day?.remainingPercent,c=o===void 0&&i===void 0?"quota unknown":`5h ${o??"?"}%, 1w ${i??"?"}% remaining`;console.log(`${t+1}. ${n} (${r}) — ${c}`)}}async function q(){let[e,t,a]=process.argv.slice(2);if(!e||e==="help"||e==="--help"||e==="-h"){y();return}if(e==="login"){await S(t);return}if(e==="api"&&t==="add"){await x(a);return}if(e==="list"){await D();return}if(e==="relay"&&t==="setup"){await N();return}y(),process.exitCode=1}try{await q()}catch(e){console.error(e instanceof Error?e.message:String(e)),process.exitCode=1}finally{L()}