@cortexkit/opencode-anthropic-auth 1.7.0 → 1.8.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
 
@@ -206,11 +207,13 @@ Example:
206
207
  }
207
208
  ```
208
209
 
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.
210
+ 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 account labels.
211
+
212
+ 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
213
 
211
214
  ## Fallback accounts
212
215
 
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.
216
+ 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
217
 
215
218
  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
219
 
@@ -224,11 +227,16 @@ Add and inspect OpenCode fallback accounts with the CLI:
224
227
 
225
228
  ```bash
226
229
  bunx @cortexkit/opencode-anthropic-auth login personal-alt
230
+ bunx @cortexkit/opencode-anthropic-auth api add kie-opus
227
231
  bunx @cortexkit/opencode-anthropic-auth list
228
232
  ```
229
233
 
230
234
  Prefer npm? Use `npx -y @cortexkit/opencode-anthropic-auth ...` with the same subcommands.
231
235
 
236
+ 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.
237
+
238
+ OpenCode cost accounting stays simple: when the native Anthropic auth entry is OAuth, OpenCode sees zero-cost Claude OAuth models. 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.
239
+
232
240
  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
241
 
234
242
  ```bash
@@ -363,7 +371,7 @@ Both OpenCode and Pi packages can persistently request Anthropic fast mode for s
363
371
  /claude-fast off
364
372
  ```
365
373
 
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`.
374
+ 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
375
 
368
376
  Fast and standard speeds do not share prompt-cache prefixes, so switching this setting can cause cache misses.
369
377
 
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{Ia as I,Ka as $,m as w,n as k,p as A,r as R,s as d,t as l,u as p}from"./index-gcmgg93d.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()}