@cortexkit/opencode-anthropic-auth
Advanced tools
Sorry, the diff of this file is too big to display
+6
-254
| #!/usr/bin/env node | ||
| import { stdin as input, stdout as output } from 'node:process'; | ||
| import { createInterface } from 'node:readline/promises'; | ||
| import { authorize, exchange, generateRelayToken, getAccountStoragePath, loadAccounts, saveAccounts, WORKER_SCRIPT, } from '@cortexkit/anthropic-auth-core'; | ||
| function defaultStorage() { | ||
| return { | ||
| version: 1, | ||
| main: { type: 'opencode', provider: 'anthropic' }, | ||
| fallbackOn: [401, 403, 429], | ||
| refresh: { | ||
| enabled: true, | ||
| intervalMinutes: 10, | ||
| refreshBeforeExpiryMinutes: 240, | ||
| }, | ||
| quota: { | ||
| enabled: true, | ||
| checkIntervalMinutes: 5, | ||
| minimumRemaining: { | ||
| five_hour: 10, | ||
| seven_day: 20, | ||
| }, | ||
| failClosedOnUnknownQuota: true, | ||
| }, | ||
| accounts: [], | ||
| }; | ||
| } | ||
| function usage() { | ||
| console.log(`Usage: | ||
| import{l as y,m as w,o as l,p as u,q as m,ra as b,ta as k}from"./index-sebye7nt.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: | ||
| opencode-anthropic-auth login [label] | ||
@@ -34,228 +8,6 @@ opencode-anthropic-auth list | ||
| Fallback accounts are stored in: | ||
| ${getAccountStoragePath()}`); | ||
| } | ||
| function requireText(value, name) { | ||
| const trimmed = value?.trim(); | ||
| if (!trimmed) | ||
| throw new Error(`${name} is required`); | ||
| return trimmed; | ||
| } | ||
| async function cloudflareRequest(options) { | ||
| const response = await fetch(`https://api.cloudflare.com/client/v4${options.path}`, { | ||
| method: options.method, | ||
| headers: { | ||
| authorization: `Bearer ${options.token}`, | ||
| ...(options.body instanceof FormData | ||
| ? {} | ||
| : { 'content-type': 'application/json' }), | ||
| ...options.headers, | ||
| }, | ||
| body: options.body, | ||
| }); | ||
| const text = await response.text(); | ||
| let data; | ||
| try { | ||
| data = JSON.parse(text); | ||
| } | ||
| catch { | ||
| throw new Error(`Cloudflare API returned ${response.status}: ${text}`); | ||
| } | ||
| if (!response.ok || data.success === false) { | ||
| const message = data.errors | ||
| ?.map((error) => error.message) | ||
| .filter(Boolean) | ||
| .join('; '); | ||
| throw new Error(message || `Cloudflare API returned ${response.status}`); | ||
| } | ||
| return data.result; | ||
| } | ||
| async function createKvNamespace(token, accountId, title) { | ||
| return cloudflareRequest({ | ||
| token, | ||
| method: 'POST', | ||
| path: `/accounts/${accountId}/storage/kv/namespaces`, | ||
| body: JSON.stringify({ title }), | ||
| }); | ||
| } | ||
| async function uploadRelayWorker(options) { | ||
| const metadata = { | ||
| main_module: 'worker.js', | ||
| compatibility_date: '2026-04-28', | ||
| bindings: [ | ||
| { | ||
| type: 'kv_namespace', | ||
| name: 'RELAY_STATE', | ||
| namespace_id: options.kvNamespaceId, | ||
| }, | ||
| { | ||
| type: 'secret_text', | ||
| name: 'RELAY_TOKEN', | ||
| text: options.relayToken, | ||
| }, | ||
| ], | ||
| }; | ||
| const form = new FormData(); | ||
| form.set('metadata', JSON.stringify(metadata)); | ||
| form.set('worker.js', new Blob([WORKER_SCRIPT], { type: 'application/javascript+module' }), 'worker.js'); | ||
| return cloudflareRequest({ | ||
| token: options.token, | ||
| method: 'PUT', | ||
| path: `/accounts/${options.accountId}/workers/scripts/${options.scriptName}`, | ||
| body: form, | ||
| }); | ||
| } | ||
| async function enableWorkersDev(token, accountId, scriptName) { | ||
| await cloudflareRequest({ | ||
| token, | ||
| method: 'POST', | ||
| path: `/accounts/${accountId}/workers/scripts/${scriptName}/subdomain`, | ||
| body: JSON.stringify({ enabled: true, previews_enabled: false }), | ||
| }); | ||
| } | ||
| async function getWorkersSubdomain(token, accountId) { | ||
| return cloudflareRequest({ | ||
| token, | ||
| method: 'GET', | ||
| path: `/accounts/${accountId}/workers/subdomain`, | ||
| }).catch(() => null); | ||
| } | ||
| async function relaySetup() { | ||
| const storage = (await loadAccounts()) ?? defaultStorage(); | ||
| const token = requireText(process.env.CLOUDFLARE_API_TOKEN?.trim() || | ||
| (await prompt('Cloudflare API token: ')), 'Cloudflare API token'); | ||
| const accountId = requireText(process.env.CLOUDFLARE_ACCOUNT_ID || | ||
| (await prompt('Cloudflare account ID: ')), 'Cloudflare account ID'); | ||
| const scriptName = (await prompt('Worker name [opencode-anthropic-relay]: ')) || | ||
| 'opencode-anthropic-relay'; | ||
| const kvTitle = `${scriptName}-state`; | ||
| const relayToken = generateRelayToken(); | ||
| console.log('Creating Cloudflare KV namespace...'); | ||
| const namespace = await createKvNamespace(token, accountId, kvTitle); | ||
| console.log('Uploading relay Worker...'); | ||
| await uploadRelayWorker({ | ||
| token, | ||
| accountId, | ||
| scriptName, | ||
| kvNamespaceId: namespace.id, | ||
| relayToken, | ||
| }); | ||
| await enableWorkersDev(token, accountId, scriptName).catch((error) => { | ||
| console.warn(`Could not enable workers.dev automatically: ${error instanceof Error ? error.message : String(error)}`); | ||
| }); | ||
| const subdomain = await getWorkersSubdomain(token, accountId); | ||
| const defaultUrl = subdomain?.subdomain | ||
| ? `https://${scriptName}.${subdomain.subdomain}.workers.dev` | ||
| : ''; | ||
| const url = defaultUrl || | ||
| requireText(await prompt('Relay Worker URL: '), 'Relay Worker URL'); | ||
| storage.relay = { | ||
| enabled: true, | ||
| url, | ||
| token: relayToken, | ||
| fallbackToDirect: true, | ||
| transport: 'http', | ||
| }; | ||
| await saveAccounts(storage); | ||
| console.log(`Relay enabled at ${url}`); | ||
| console.log(`Config saved to ${getAccountStoragePath()}.`); | ||
| } | ||
| async function prompt(message) { | ||
| const rl = createInterface({ input, output }); | ||
| try { | ||
| return (await rl.question(message)).trim(); | ||
| } | ||
| finally { | ||
| rl.close(); | ||
| } | ||
| } | ||
| function upsertAccount(storage, account) { | ||
| const index = storage.accounts.findIndex((candidate) => candidate.id === account.id || | ||
| (account.label && candidate.label === account.label)); | ||
| if (index >= 0) { | ||
| storage.accounts[index] = { | ||
| ...storage.accounts[index], | ||
| ...account, | ||
| addedAt: storage.accounts[index]?.addedAt ?? account.addedAt, | ||
| quota: account.quota, | ||
| lastRefreshedAt: account.lastRefreshedAt, | ||
| lastRefreshError: account.lastRefreshError, | ||
| lastQuotaRefreshError: account.lastQuotaRefreshError, | ||
| }; | ||
| return; | ||
| } | ||
| storage.accounts.push(account); | ||
| } | ||
| async function login(labelArg) { | ||
| const storage = (await loadAccounts()) ?? defaultStorage(); | ||
| const label = labelArg?.trim() || (await prompt('Fallback account label (optional): ')); | ||
| const authorization = await authorize('max'); | ||
| console.log('\nOpen this URL in your browser and complete Claude sign-in:\n'); | ||
| console.log(`${authorization.url}\n`); | ||
| const code = await prompt('Paste the full callback URL or authorization code here: '); | ||
| const result = await exchange(code, authorization.verifier, authorization.redirectUri, authorization.state); | ||
| if (result.type === 'failed') { | ||
| throw new Error('Authentication failed'); | ||
| } | ||
| const now = Date.now(); | ||
| upsertAccount(storage, { | ||
| id: label || crypto.randomUUID(), | ||
| label: label || undefined, | ||
| type: 'oauth', | ||
| access: result.access, | ||
| refresh: result.refresh, | ||
| expires: result.expires, | ||
| enabled: true, | ||
| addedAt: now, | ||
| lastUsed: now, | ||
| }); | ||
| await saveAccounts(storage); | ||
| console.log(`\nSaved fallback account${label ? ` "${label}"` : ''}.`); | ||
| } | ||
| async function listAccounts() { | ||
| const storage = await loadAccounts(); | ||
| if (!storage?.accounts.length) { | ||
| console.log(`No fallback accounts found at ${getAccountStoragePath()}.`); | ||
| return; | ||
| } | ||
| for (const [index, account] of storage.accounts.entries()) { | ||
| const label = account.label || account.id; | ||
| const status = account.enabled === false ? 'disabled' : 'enabled'; | ||
| const fiveHour = account.quota?.five_hour?.remainingPercent; | ||
| const sevenDay = account.quota?.seven_day?.remainingPercent; | ||
| const quota = fiveHour === undefined && sevenDay === undefined | ||
| ? 'quota unknown' | ||
| : `5h ${fiveHour ?? '?'}%, 1w ${sevenDay ?? '?'}% remaining`; | ||
| console.log(`${index + 1}. ${label} (${status}) — ${quota}`); | ||
| } | ||
| } | ||
| async function main() { | ||
| const [command, label] = process.argv.slice(2); | ||
| if (!command || | ||
| command === 'help' || | ||
| command === '--help' || | ||
| command === '-h') { | ||
| usage(); | ||
| return; | ||
| } | ||
| if (command === 'login') { | ||
| await login(label); | ||
| return; | ||
| } | ||
| if (command === 'list') { | ||
| await listAccounts(); | ||
| return; | ||
| } | ||
| if (command === 'relay' && label === 'setup') { | ||
| await relaySetup(); | ||
| return; | ||
| } | ||
| usage(); | ||
| process.exitCode = 1; | ||
| } | ||
| try { | ||
| await main(); | ||
| } | ||
| catch (error) { | ||
| console.error(error instanceof Error ? error.message : String(error)); | ||
| process.exitCode = 1; | ||
| } | ||
| ${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(` | ||
| Open this URL in your browser and complete Claude sign-in: | ||
| `),console.log(`${n.url} | ||
| `);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(` | ||
| 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} |
+5
-1039
@@ -1,1039 +0,5 @@ | ||
| import { randomUUID } from 'node:crypto'; | ||
| import { acquireRefreshFileLock, authorize, buildClaudeQuotaSummary, buildFallbackQuotaSummaries, buildRefreshOperationError, CACHE_1H_COMMAND_NAME, CACHE_KEEP_EXTENDED_TTL_BETA, CacheKeepManager, CLAUDE_CACHE_KEEP_COMMAND_NAME, CLAUDE_DUMP_COMMAND_NAME, CLAUDE_FAST_COMMAND_NAME, CLAUDE_QUOTAS_COMMAND_NAME, ClaudeOAuthRefreshError, exchange, executeCache1hCommand, executeCacheKeepCommand, executeDumpCommand, executeFastModeCommand, FallbackAccountManager, fetchOAuthQuotaSnapshot, formatRefreshBackoffMessage, getCache1hMode, getCache1hPersistentMode, getCacheKeepWindow, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getRelayConfig, hashRefreshToken, isCache1hEnabled, isCache1hPersistentlyEnabled, isCacheKeepHybridActive, isCacheKeepPersistentlyEnabled, isDumpPersistentlyEnabled, isFastModeEnabled, isFastModePersistentlyEnabled, isFastModeSupportedModel, loadAccounts, log, mergeAnthropicBetas, parseCache1hCommandAction, parseCacheKeepCommandAction, parseDumpCommandAction, parseFastModeCommandAction, quotaSnapshotPassesPolicy, refreshBackoffActive, refreshClaudeOAuthToken, resolveClaudeCodeIdentity, saveAccounts, sendViaRelay, setCache1hPersistentEnabled, setCache1hPersistentMode, setCache1hState, setCacheKeepPersistentEnabled, setCacheKeepPersistentWindow, setDumpEnabled, setDumpPersistentEnabled, setFastModeEnabled, setFastModePersistentEnabled, shouldFallbackStatus, } from '@cortexkit/anthropic-auth-core'; | ||
| import { resolvePromptContext } from "./prompt-context.js"; | ||
| import { addFastModeBetaHeader, createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js"; | ||
| const HANDLED_SENTINEL = '__OPENCODE_ANTHROPIC_AUTH_COMMAND_HANDLED__'; | ||
| const MAIN_AUTH_REFRESH_TICK_MS = 60_000; | ||
| const MIN_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES = 240; | ||
| const DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES = MIN_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES; | ||
| let nextPerfRequestId = 1; | ||
| let eventLoopLagMonitorStarted = false; | ||
| function perfLoggingEnabled() { | ||
| return process.env.OPENCODE_ANTHROPIC_AUTH_PERF === '1'; | ||
| } | ||
| function nowMs() { | ||
| return performance.now(); | ||
| } | ||
| function roundMs(value) { | ||
| return Math.round(value * 10) / 10; | ||
| } | ||
| function startEventLoopLagMonitor() { | ||
| if (eventLoopLagMonitorStarted || | ||
| process.env.NODE_ENV === 'test' || | ||
| !perfLoggingEnabled()) { | ||
| return; | ||
| } | ||
| eventLoopLagMonitorStarted = true; | ||
| const intervalMs = 100; | ||
| const thresholdMs = 250; | ||
| let expected = nowMs() + intervalMs; | ||
| setInterval(() => { | ||
| const current = nowMs(); | ||
| const lag = current - expected; | ||
| expected = current + intervalMs; | ||
| if (lag < thresholdMs) | ||
| return; | ||
| log('[perf] opencode event_loop_lag', { | ||
| lagMs: roundMs(lag), | ||
| thresholdMs, | ||
| }); | ||
| }, intervalMs).unref?.(); | ||
| } | ||
| function createPerfTrace(data) { | ||
| const start = nowMs(); | ||
| const trace = { | ||
| requestId: String(nextPerfRequestId++), | ||
| start, | ||
| last: start, | ||
| mark(stage, stageData) { | ||
| const current = nowMs(); | ||
| if (perfLoggingEnabled()) { | ||
| log('[perf] opencode request stage', { | ||
| requestId: trace.requestId, | ||
| stage, | ||
| deltaMs: roundMs(current - trace.last), | ||
| totalMs: roundMs(current - trace.start), | ||
| ...stageData, | ||
| }); | ||
| } | ||
| trace.last = current; | ||
| }, | ||
| done(stage, stageData) { | ||
| const current = nowMs(); | ||
| if (perfLoggingEnabled()) { | ||
| log('[perf] opencode request done', { | ||
| requestId: trace.requestId, | ||
| stage, | ||
| deltaMs: roundMs(current - trace.last), | ||
| totalMs: roundMs(current - trace.start), | ||
| ...stageData, | ||
| }); | ||
| } | ||
| trace.last = current; | ||
| }, | ||
| }; | ||
| if (perfLoggingEnabled()) { | ||
| log('[perf] opencode request start', { | ||
| requestId: trace.requestId, | ||
| ...data, | ||
| }); | ||
| } | ||
| return trace; | ||
| } | ||
| async function sendIgnoredMessage(ctx, sessionId, text) { | ||
| const session = ctx.client.session; | ||
| const promptContext = await resolvePromptContext(ctx.client, sessionId); | ||
| const request = { | ||
| path: { id: sessionId }, | ||
| body: { | ||
| noReply: true, | ||
| parts: [{ type: 'text', text, ignored: true }], | ||
| }, | ||
| }; | ||
| if (promptContext?.agent) | ||
| request.body.agent = promptContext.agent; | ||
| if (promptContext?.model) | ||
| request.body.model = promptContext.model; | ||
| if (promptContext?.variant) | ||
| request.body.variant = promptContext.variant; | ||
| if (typeof session?.promptAsync === 'function') { | ||
| await session.promptAsync(request); | ||
| return; | ||
| } | ||
| if (typeof session?.prompt === 'function') { | ||
| await Promise.resolve(session.prompt(request)); | ||
| return; | ||
| } | ||
| throw new Error('OpenCode session prompt API is unavailable for ignored replies.'); | ||
| } | ||
| function throwHandledSentinel() { | ||
| throw new Error(HANDLED_SENTINEL); | ||
| } | ||
| export const AnthropicAuthPlugin = async (ctx) => { | ||
| startEventLoopLagMonitor(); | ||
| const { client } = ctx; | ||
| const fallbackManager = new FallbackAccountManager(); | ||
| fallbackManager.startBackgroundRefresh(); | ||
| let latestRefreshMainAccessToken = null; | ||
| const cacheKeepManager = new CacheKeepManager({ | ||
| loadStorage: () => loadAccounts(), | ||
| prepareHeaders: async (headers, target) => { | ||
| if (!latestGetAuth) | ||
| return headers; | ||
| const auth = await latestGetAuth(); | ||
| if (auth.type !== 'oauth') | ||
| return headers; | ||
| if (!auth.access || (auth.expires && auth.expires < Date.now())) { | ||
| if (!latestRefreshMainAccessToken) | ||
| return headers; | ||
| auth.access = await latestRefreshMainAccessToken(); | ||
| } | ||
| if (!auth.access) | ||
| return headers; | ||
| try { | ||
| const parsedBody = JSON.parse(target.bodyText); | ||
| const identity = await resolveClaudeCodeIdentity(auth.access, typeof parsedBody.model === 'string' ? parsedBody.model : undefined); | ||
| headers.delete('anthropic-beta'); | ||
| setOAuthHeaders(headers, auth.access, { | ||
| body: parsedBody, | ||
| identity, | ||
| }); | ||
| headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [ | ||
| CACHE_KEEP_EXTENDED_TTL_BETA, | ||
| ])); | ||
| if (parsedBody.speed === 'fast') | ||
| addFastModeBetaHeader(headers); | ||
| } | ||
| catch { | ||
| setOAuthHeaders(headers, auth.access); | ||
| } | ||
| return headers; | ||
| }, | ||
| log, | ||
| }); | ||
| const initialCache1hStorage = await loadAccounts(); | ||
| const relayConfig = getRelayConfig(initialCache1hStorage); | ||
| setCache1hState({ | ||
| enabled: isCache1hPersistentlyEnabled(initialCache1hStorage), | ||
| mode: getCache1hPersistentMode(initialCache1hStorage), | ||
| }); | ||
| setDumpEnabled(isDumpPersistentlyEnabled(initialCache1hStorage)); | ||
| setFastModeEnabled(isFastModePersistentlyEnabled(initialCache1hStorage)); | ||
| let latestGetAuth = null; | ||
| let mainBackgroundRefreshTimer = null; | ||
| function mainRefreshBeforeExpiryMs(storage) { | ||
| const minutes = storage?.refresh?.refreshBeforeExpiryMinutes ?? | ||
| DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES; | ||
| return Math.max(MIN_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES, minutes) * 60_000; | ||
| } | ||
| function mainRefreshEnabled(storage) { | ||
| return storage?.refresh?.enabled !== false; | ||
| } | ||
| async function clearStaleMainRefreshError(refreshToken) { | ||
| if (!refreshToken) | ||
| return; | ||
| const storage = await loadAccounts(); | ||
| const error = storage?.refresh?.mainLastRefreshError; | ||
| if (!storage?.refresh || !error?.tokenHash) | ||
| return; | ||
| const tokenHash = hashRefreshToken(refreshToken); | ||
| if (error.tokenHash === tokenHash) | ||
| return; | ||
| storage.refresh.mainLastRefreshError = undefined; | ||
| await saveAccounts(storage); | ||
| log('[refresh] opencode main oauth cleared stale backoff after token rotation', { | ||
| previousCheckedAt: error.checkedAt, | ||
| previousNextRetryAt: error.nextRetryAt, | ||
| previousRetryCount: error.retryCount, | ||
| }); | ||
| } | ||
| async function buildQuotaCommandSummary() { | ||
| const accounts = []; | ||
| if (latestGetAuth) { | ||
| try { | ||
| const auth = await latestGetAuth(); | ||
| if (auth.type === 'oauth' && auth.access) { | ||
| accounts.push({ | ||
| name: 'OpenCode anthropic', | ||
| role: 'main', | ||
| quota: await fetchOAuthQuotaSnapshot({ accessToken: auth.access }), | ||
| }); | ||
| } | ||
| else if (auth.type === 'oauth') { | ||
| accounts.push({ | ||
| name: 'OpenCode anthropic', | ||
| role: 'main', | ||
| error: 'missing access token; send a request first or reconnect auth', | ||
| }); | ||
| } | ||
| } | ||
| catch (error) { | ||
| accounts.push({ | ||
| name: 'OpenCode anthropic', | ||
| role: 'main', | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| } | ||
| } | ||
| const { storage, errors } = await fallbackManager.refreshQuotaForAllAccounts(); | ||
| accounts.push(...buildFallbackQuotaSummaries(storage, new Map(errors.map((error) => [error.accountId, error.message])))); | ||
| if (!latestGetAuth) { | ||
| accounts.unshift({ | ||
| name: 'OpenCode anthropic', | ||
| role: 'main', | ||
| error: 'auth loader has not run yet; send a request first', | ||
| }); | ||
| } | ||
| return buildClaudeQuotaSummary({ accounts, refreshedAt: Date.now() }); | ||
| } | ||
| async function executePersistentCache1hCommand(argumentsText) { | ||
| const action = parseCache1hCommandAction(argumentsText); | ||
| if (action.type === 'enable' || action.type === 'disable') { | ||
| const enabled = action.type === 'enable'; | ||
| const storage = await setCache1hPersistentEnabled(enabled); | ||
| const mode = getCache1hPersistentMode(storage); | ||
| setCache1hState({ enabled, mode }); | ||
| return executeCache1hCommand({ argumentsText, enabled, mode }); | ||
| } | ||
| if (action.type === 'mode') { | ||
| const storage = await setCache1hPersistentMode(action.mode); | ||
| const enabled = isCache1hPersistentlyEnabled(storage); | ||
| setCache1hState({ enabled, mode: action.mode }); | ||
| return executeCache1hCommand({ | ||
| argumentsText, | ||
| enabled, | ||
| mode: action.mode, | ||
| }); | ||
| } | ||
| const storage = await loadAccounts(); | ||
| const enabled = isCache1hPersistentlyEnabled(storage); | ||
| const mode = getCache1hPersistentMode(storage); | ||
| setCache1hState({ enabled, mode }); | ||
| return executeCache1hCommand({ argumentsText, enabled, mode }); | ||
| } | ||
| async function executePersistentCacheKeepCommand(argumentsText) { | ||
| const action = parseCacheKeepCommandAction(argumentsText); | ||
| let storage = await loadAccounts(); | ||
| if (action.type === 'window') { | ||
| storage = await setCacheKeepPersistentWindow(action.startHour, action.endHour); | ||
| } | ||
| else if (action.type === 'disable') { | ||
| storage = await setCacheKeepPersistentEnabled(false); | ||
| } | ||
| const window = getCacheKeepWindow(storage); | ||
| const stats = cacheKeepManager.stats(window); | ||
| return executeCacheKeepCommand({ | ||
| argumentsText, | ||
| enabled: isCacheKeepPersistentlyEnabled(storage), | ||
| window, | ||
| hybridActive: isCacheKeepHybridActive(storage), | ||
| trackedSessions: stats.trackedSessions, | ||
| nextPrewarmAt: stats.nextPrewarmAt, | ||
| }); | ||
| } | ||
| async function executePersistentDumpCommand(argumentsText) { | ||
| const action = parseDumpCommandAction(argumentsText); | ||
| if (action.type === 'enable' || action.type === 'disable') { | ||
| const enabled = action.type === 'enable'; | ||
| await setDumpPersistentEnabled(enabled); | ||
| setDumpEnabled(enabled); | ||
| return executeDumpCommand({ argumentsText, enabled }); | ||
| } | ||
| const storage = await loadAccounts(); | ||
| const enabled = isDumpPersistentlyEnabled(storage); | ||
| setDumpEnabled(enabled); | ||
| return executeDumpCommand({ argumentsText, enabled }); | ||
| } | ||
| async function executePersistentFastModeCommand(argumentsText) { | ||
| const action = parseFastModeCommandAction(argumentsText); | ||
| if (action.type === 'enable' || action.type === 'disable') { | ||
| const enabled = action.type === 'enable'; | ||
| await setFastModePersistentEnabled(enabled); | ||
| setFastModeEnabled(enabled); | ||
| return executeFastModeCommand({ argumentsText, enabled }); | ||
| } | ||
| const storage = await loadAccounts(); | ||
| const enabled = isFastModePersistentlyEnabled(storage); | ||
| setFastModeEnabled(enabled); | ||
| return executeFastModeCommand({ argumentsText, enabled }); | ||
| } | ||
| return { | ||
| config: async (config) => { | ||
| config.command = { | ||
| ...(config.command ?? {}), | ||
| [CACHE_1H_COMMAND_NAME]: { | ||
| template: CACHE_1H_COMMAND_NAME, | ||
| description: 'Show or toggle 1-hour Anthropic ephemeral prompt cache TTL.', | ||
| }, | ||
| [CLAUDE_CACHE_KEEP_COMMAND_NAME]: { | ||
| template: CLAUDE_CACHE_KEEP_COMMAND_NAME, | ||
| description: 'Keep hybrid Claude cache warm for recently used sessions during a local time window.', | ||
| }, | ||
| [CLAUDE_QUOTAS_COMMAND_NAME]: { | ||
| template: CLAUDE_QUOTAS_COMMAND_NAME, | ||
| description: 'Show current Claude OAuth quota usage for all accounts.', | ||
| }, | ||
| [CLAUDE_DUMP_COMMAND_NAME]: { | ||
| template: CLAUDE_DUMP_COMMAND_NAME, | ||
| description: 'Show or toggle Anthropic request dump capture for debugging.', | ||
| }, | ||
| [CLAUDE_FAST_COMMAND_NAME]: { | ||
| template: CLAUDE_FAST_COMMAND_NAME, | ||
| description: 'Show or toggle Anthropic fast mode for supported Opus models.', | ||
| }, | ||
| }; | ||
| }, | ||
| 'command.execute.before': async (input) => { | ||
| if (input.command === CACHE_1H_COMMAND_NAME) { | ||
| await sendIgnoredMessage(ctx, input.sessionID, await executePersistentCache1hCommand(input.arguments)); | ||
| throwHandledSentinel(); | ||
| } | ||
| if (input.command === CLAUDE_CACHE_KEEP_COMMAND_NAME) { | ||
| await sendIgnoredMessage(ctx, input.sessionID, await executePersistentCacheKeepCommand(input.arguments)); | ||
| throwHandledSentinel(); | ||
| } | ||
| if (input.command === CLAUDE_QUOTAS_COMMAND_NAME) { | ||
| await sendIgnoredMessage(ctx, input.sessionID, await buildQuotaCommandSummary()); | ||
| throwHandledSentinel(); | ||
| } | ||
| if (input.command === CLAUDE_DUMP_COMMAND_NAME) { | ||
| await sendIgnoredMessage(ctx, input.sessionID, await executePersistentDumpCommand(input.arguments)); | ||
| throwHandledSentinel(); | ||
| } | ||
| if (input.command === CLAUDE_FAST_COMMAND_NAME) { | ||
| await sendIgnoredMessage(ctx, input.sessionID, await executePersistentFastModeCommand(input.arguments)); | ||
| throwHandledSentinel(); | ||
| } | ||
| }, | ||
| auth: { | ||
| provider: 'anthropic', | ||
| async loader(getAuth, provider) { | ||
| latestGetAuth = getAuth; | ||
| const auth = await getAuth(); | ||
| if (auth.type === 'oauth') { | ||
| // zero out cost for max plan | ||
| for (const model of Object.values(provider.models)) { | ||
| model.cost = { | ||
| input: 0, | ||
| output: 0, | ||
| cache: { | ||
| read: 0, | ||
| write: 0, | ||
| }, | ||
| }; | ||
| } | ||
| // Shared inflight refresh promise — prevents concurrent token refreshes | ||
| // from racing against each other (and causing 401 cascades with token rotation) | ||
| let refreshPromise = null; | ||
| let mainQuotaCache = null; | ||
| let mainQuotaRefreshPromise = null; | ||
| let mainQuotaRetryAfter = 0; | ||
| async function refreshMainAccessToken() { | ||
| if (!refreshPromise) { | ||
| refreshPromise = (async () => { | ||
| const maxRetries = 2; | ||
| const baseDelayMs = 500; | ||
| let leaseId = null; | ||
| let leaseTokenHash = null; | ||
| let releaseFileLock = null; | ||
| async function updateMainRefreshState(update) { | ||
| const storage = (await loadAccounts()) ?? { | ||
| version: 1, | ||
| main: { type: 'opencode', provider: 'anthropic' }, | ||
| accounts: [], | ||
| }; | ||
| storage.refresh = storage.refresh ?? {}; | ||
| update(storage); | ||
| await saveAccounts(storage); | ||
| } | ||
| for (let attempt = 0; attempt <= maxRetries; attempt++) { | ||
| let freshAuth = null; | ||
| try { | ||
| if (attempt > 0) { | ||
| const delay = baseDelayMs * 2 ** (attempt - 1); | ||
| await new Promise((resolve) => setTimeout(resolve, delay)); | ||
| } | ||
| // Re-read auth to get the latest refresh token. | ||
| // The outer `auth` snapshot may be stale if tokens | ||
| // were rotated since the fetch() call was made. | ||
| freshAuth = await getAuth(); | ||
| if (!freshAuth.refresh) { | ||
| throw new Error('Token refresh failed: missing refresh token'); | ||
| } | ||
| const storage = await loadAccounts(); | ||
| const refreshTokenHash = hashRefreshToken(freshAuth.refresh); | ||
| const mainError = storage?.refresh?.mainLastRefreshError; | ||
| log('[refresh] opencode main oauth refresh check', { | ||
| attempt, | ||
| expiresInMs: freshAuth.expires | ||
| ? freshAuth.expires - Date.now() | ||
| : undefined, | ||
| hasBackoff: Boolean(mainError), | ||
| backoffActive: mainError | ||
| ? refreshBackoffActive(mainError, freshAuth.refresh, Date.now()) | ||
| : false, | ||
| retryCount: mainError?.retryCount, | ||
| nextRetryAt: mainError?.nextRetryAt, | ||
| }); | ||
| if (mainError && | ||
| refreshBackoffActive(mainError, freshAuth.refresh, Date.now())) { | ||
| log('[refresh] opencode main oauth refresh skipped backoff', { | ||
| nextRetryAt: mainError.nextRetryAt, | ||
| retryCount: mainError.retryCount, | ||
| }); | ||
| throw new Error(formatRefreshBackoffMessage(mainError, Date.now())); | ||
| } | ||
| if (storage?.refresh?.mainRefreshLeaseUntil && | ||
| storage.refresh.mainRefreshLeaseUntil > Date.now() && | ||
| storage.refresh.mainRefreshLeaseTokenHash === | ||
| refreshTokenHash) { | ||
| log('[refresh] opencode main oauth refresh skipped lease', { | ||
| leaseUntil: storage.refresh.mainRefreshLeaseUntil, | ||
| }); | ||
| throw new Error('Claude OAuth refresh is already in progress'); | ||
| } | ||
| const fileLock = await acquireRefreshFileLock({ | ||
| name: 'opencode-main-oauth-refresh', | ||
| ttlMs: 2 * 60_000, | ||
| }); | ||
| if (!fileLock) { | ||
| log('[refresh] opencode main oauth refresh skipped file lock'); | ||
| throw new Error('Claude OAuth refresh is already in progress'); | ||
| } | ||
| releaseFileLock = fileLock.release; | ||
| leaseId = randomUUID(); | ||
| leaseTokenHash = refreshTokenHash; | ||
| await updateMainRefreshState((nextStorage) => { | ||
| nextStorage.refresh = nextStorage.refresh ?? {}; | ||
| nextStorage.refresh.mainRefreshLeaseId = | ||
| leaseId ?? undefined; | ||
| nextStorage.refresh.mainRefreshLeaseUntil = | ||
| Date.now() + 2 * 60_000; | ||
| nextStorage.refresh.mainRefreshLeaseTokenHash = | ||
| refreshTokenHash; | ||
| }); | ||
| const latestLease = await loadAccounts(); | ||
| log('[refresh] opencode main oauth refresh lease acquired', { | ||
| attempt, | ||
| leaseUntil: Date.now() + 2 * 60_000, | ||
| }); | ||
| if (latestLease?.refresh?.mainRefreshLeaseId !== leaseId || | ||
| latestLease.refresh.mainRefreshLeaseTokenHash !== | ||
| refreshTokenHash) { | ||
| throw new Error('Claude OAuth refresh is already in progress'); | ||
| } | ||
| log('[refresh] opencode main oauth refresh request start', { | ||
| attempt, | ||
| }); | ||
| const refreshed = await refreshClaudeOAuthToken({ | ||
| refreshToken: freshAuth.refresh, | ||
| }); | ||
| // biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set | ||
| await client.auth.set({ | ||
| path: { | ||
| id: 'anthropic', | ||
| }, | ||
| body: { | ||
| type: 'oauth', | ||
| refresh: refreshed.refresh, | ||
| access: refreshed.access, | ||
| expires: refreshed.expires, | ||
| }, | ||
| }); | ||
| await updateMainRefreshState((storage) => { | ||
| if (!storage?.refresh) | ||
| return; | ||
| storage.refresh.mainLastRefreshError = undefined; | ||
| if (storage.refresh.mainRefreshLeaseId === leaseId) { | ||
| storage.refresh.mainRefreshLeaseId = undefined; | ||
| storage.refresh.mainRefreshLeaseUntil = undefined; | ||
| storage.refresh.mainRefreshLeaseTokenHash = undefined; | ||
| } | ||
| }); | ||
| log('[refresh] opencode main oauth refresh succeeded', { | ||
| attempt, | ||
| expiresInMs: refreshed.expires - Date.now(), | ||
| }); | ||
| return refreshed.access; | ||
| } | ||
| catch (error) { | ||
| const isNetworkError = error instanceof Error && | ||
| (error.message.includes('fetch failed') || | ||
| ('code' in error && | ||
| (error.code === 'ECONNRESET' || | ||
| error.code === 'ECONNREFUSED' || | ||
| error.code === 'ETIMEDOUT' || | ||
| error.code === 'UND_ERR_CONNECT_TIMEOUT'))); | ||
| if (attempt < maxRetries && | ||
| (isNetworkError || | ||
| (error instanceof ClaudeOAuthRefreshError && | ||
| error.status >= 500))) { | ||
| continue; | ||
| } | ||
| log('[refresh] opencode main oauth refresh attempt failed', { | ||
| attempt, | ||
| error: error instanceof Error | ||
| ? error.message | ||
| : String(error), | ||
| transient: isNetworkError, | ||
| }); | ||
| const failedRefreshToken = freshAuth?.refresh; | ||
| if (failedRefreshToken && | ||
| error instanceof ClaudeOAuthRefreshError) { | ||
| await updateMainRefreshState((storage) => { | ||
| storage.refresh = storage.refresh ?? {}; | ||
| storage.refresh.mainLastRefreshError = | ||
| buildRefreshOperationError({ | ||
| error, | ||
| now: Date.now(), | ||
| refreshToken: failedRefreshToken, | ||
| previous: storage.refresh.mainLastRefreshError, | ||
| }); | ||
| }); | ||
| } | ||
| throw error; | ||
| } | ||
| finally { | ||
| if (leaseId) { | ||
| await updateMainRefreshState((storage) => { | ||
| if (!storage?.refresh) | ||
| return; | ||
| if (storage.refresh.mainRefreshLeaseId === leaseId && | ||
| storage.refresh.mainRefreshLeaseTokenHash === | ||
| leaseTokenHash) { | ||
| storage.refresh.mainRefreshLeaseId = undefined; | ||
| storage.refresh.mainRefreshLeaseUntil = undefined; | ||
| storage.refresh.mainRefreshLeaseTokenHash = undefined; | ||
| } | ||
| }).catch(() => { }); | ||
| } | ||
| await releaseFileLock?.().catch(() => { }); | ||
| } | ||
| } | ||
| // Unreachable — each iteration either returns or throws. | ||
| // Kept as a TypeScript exhaustiveness guard. | ||
| throw new Error('Token refresh exhausted all retries'); | ||
| })().finally(() => { | ||
| refreshPromise = null; | ||
| }); | ||
| } | ||
| return refreshPromise; | ||
| } | ||
| latestRefreshMainAccessToken = refreshMainAccessToken; | ||
| function startMainBackgroundRefresh() { | ||
| if (mainBackgroundRefreshTimer) { | ||
| clearInterval(mainBackgroundRefreshTimer); | ||
| mainBackgroundRefreshTimer = null; | ||
| } | ||
| const run = async () => { | ||
| try { | ||
| const storage = await loadAccounts(); | ||
| if (!mainRefreshEnabled(storage)) | ||
| return; | ||
| const latestAuth = await getAuth(); | ||
| if (latestAuth.type !== 'oauth') | ||
| return; | ||
| await clearStaleMainRefreshError(latestAuth.refresh); | ||
| if (!latestAuth.expires) | ||
| return; | ||
| const expiresInMs = latestAuth.expires - Date.now(); | ||
| const refreshBeforeMs = mainRefreshBeforeExpiryMs(storage); | ||
| if (expiresInMs > refreshBeforeMs) { | ||
| return; | ||
| } | ||
| log('[refresh] opencode main oauth background due', { | ||
| expiresInMs, | ||
| refreshBeforeMs, | ||
| }); | ||
| if (latestAuth.refresh && | ||
| refreshBackoffActive(storage?.refresh?.mainLastRefreshError, latestAuth.refresh, Date.now())) { | ||
| log('[refresh] opencode main oauth background skipped backoff', { | ||
| nextRetryAt: storage?.refresh?.mainLastRefreshError?.nextRetryAt, | ||
| retryCount: storage?.refresh?.mainLastRefreshError?.retryCount, | ||
| }); | ||
| return; | ||
| } | ||
| if (latestAuth.refresh && | ||
| storage?.refresh?.mainRefreshLeaseUntil && | ||
| storage.refresh.mainRefreshLeaseUntil > Date.now() && | ||
| storage.refresh.mainRefreshLeaseTokenHash === | ||
| hashRefreshToken(latestAuth.refresh)) { | ||
| return; | ||
| } | ||
| await refreshMainAccessToken(); | ||
| log('[refresh] opencode main oauth refreshed in background', { | ||
| expires: latestAuth.expires, | ||
| }); | ||
| } | ||
| catch (error) { | ||
| log('[refresh] opencode main oauth refresh failed', { | ||
| message: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| } | ||
| }; | ||
| mainBackgroundRefreshTimer = setInterval(() => { | ||
| void run(); | ||
| }, MAIN_AUTH_REFRESH_TICK_MS); | ||
| if ('unref' in mainBackgroundRefreshTimer) { | ||
| mainBackgroundRefreshTimer.unref(); | ||
| } | ||
| } | ||
| startMainBackgroundRefresh(); | ||
| function isReplayableRequest(input, body) { | ||
| if (input instanceof Request && input.body) | ||
| return false; | ||
| return body == null || typeof body === 'string'; | ||
| } | ||
| function isSubagentRequest(headers) { | ||
| return headers.has('x-parent-session-id'); | ||
| } | ||
| function isStreamingRateLimitText(text) { | ||
| return (text.includes('rate_limit_error') || | ||
| /exceed your account'?s rate limit/i.test(text)); | ||
| } | ||
| function mainQuotaRoutingEnabled(storage) { | ||
| return storage?.quota?.enabled === true; | ||
| } | ||
| async function inspectStreamingRateLimit(response, trace) { | ||
| if (!response.body || response.status !== 200) { | ||
| trace?.mark('inspect_stream_skip', { status: response.status }); | ||
| return { response, rateLimited: false }; | ||
| } | ||
| if (response.headers.get('x-cortexkit-relay-optimistic') === 'true') { | ||
| trace?.mark('inspect_stream_skip', { | ||
| status: response.status, | ||
| reason: 'optimistic_relay', | ||
| }); | ||
| return { response, rateLimited: false }; | ||
| } | ||
| const start = nowMs(); | ||
| const reader = response.body.getReader(); | ||
| const chunks = []; | ||
| const decoder = new TextDecoder(); | ||
| let text = ''; | ||
| let bytes = 0; | ||
| while (!text.includes('\n\n') && text.length < 65_536) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) | ||
| break; | ||
| chunks.push(value); | ||
| bytes += value.byteLength; | ||
| text += decoder.decode(value, { stream: true }); | ||
| if (isStreamingRateLimitText(text)) | ||
| break; | ||
| } | ||
| if (isStreamingRateLimitText(text)) { | ||
| await reader.cancel().catch(() => { }); | ||
| trace?.mark('inspect_stream_first_event', { | ||
| ms: roundMs(nowMs() - start), | ||
| bytes, | ||
| rateLimited: true, | ||
| }); | ||
| return { response, rateLimited: true }; | ||
| } | ||
| const stream = new ReadableStream({ | ||
| start(controller) { | ||
| for (const chunk of chunks) | ||
| controller.enqueue(chunk); | ||
| }, | ||
| async pull(controller) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| controller.close(); | ||
| return; | ||
| } | ||
| controller.enqueue(value); | ||
| }, | ||
| cancel(reason) { | ||
| return reader.cancel(reason); | ||
| }, | ||
| }); | ||
| trace?.mark('inspect_stream_first_event', { | ||
| ms: roundMs(nowMs() - start), | ||
| bytes, | ||
| rateLimited: false, | ||
| }); | ||
| return { | ||
| response: new Response(stream, { | ||
| status: response.status, | ||
| statusText: response.statusText, | ||
| headers: response.headers, | ||
| }), | ||
| rateLimited: false, | ||
| }; | ||
| } | ||
| async function sendWithAccessToken(input, init, accessToken, trace, route = 'unknown') { | ||
| const start = nowMs(); | ||
| const requestHeaders = mergeHeaders(input, init); | ||
| const relayAffinity = requestHeaders.get('x-session-affinity') || | ||
| requestHeaders.get('x-opencode-session'); | ||
| const subagentRequest = isSubagentRequest(requestHeaders); | ||
| requestHeaders.delete('x-parent-session-id'); | ||
| requestHeaders.delete('x-session-affinity'); | ||
| requestHeaders.delete('x-opencode-session'); | ||
| let body = init?.body; | ||
| let modelForIdentity; | ||
| if (body && typeof body === 'string') { | ||
| try { | ||
| const parsedBody = JSON.parse(body); | ||
| if (typeof parsedBody.model === 'string') { | ||
| modelForIdentity = parsedBody.model; | ||
| } | ||
| } | ||
| catch { } | ||
| } | ||
| const identity = await resolveClaudeCodeIdentity(accessToken, modelForIdentity); | ||
| const originalBytes = typeof body === 'string' ? body.length : undefined; | ||
| if (body && typeof body === 'string') { | ||
| const rewriteStart = nowMs(); | ||
| const fastModeRequested = (() => { | ||
| if (!isFastModeEnabled()) | ||
| return false; | ||
| try { | ||
| return isFastModeSupportedModel(JSON.parse(body).model); | ||
| } | ||
| catch { | ||
| return false; | ||
| } | ||
| })(); | ||
| body = await rewriteRequestBody(body, { | ||
| cache1hEnabled: !subagentRequest && isCache1hEnabled(), | ||
| cache1hMode: getCache1hMode(), | ||
| fastModeEnabled: fastModeRequested, | ||
| identity, | ||
| }); | ||
| try { | ||
| setOAuthHeaders(requestHeaders, accessToken, { | ||
| body: JSON.parse(body), | ||
| identity, | ||
| }); | ||
| } | ||
| catch { | ||
| setOAuthHeaders(requestHeaders, accessToken, { identity }); | ||
| } | ||
| if (fastModeRequested) | ||
| addFastModeBetaHeader(requestHeaders); | ||
| trace?.mark('rewrite_body', { | ||
| route, | ||
| ms: roundMs(nowMs() - rewriteStart), | ||
| originalBytes, | ||
| rewrittenBytes: body.length, | ||
| cacheEnabled: !subagentRequest && isCache1hEnabled(), | ||
| cacheMode: getCache1hMode(), | ||
| fastModeEnabled: fastModeRequested, | ||
| subagent: subagentRequest, | ||
| }); | ||
| } | ||
| const rewritten = rewriteUrl(input); | ||
| if (route === 'main' && | ||
| typeof body === 'string' && | ||
| !subagentRequest && | ||
| isCache1hEnabled() && | ||
| getCache1hMode() === 'hybrid') { | ||
| const storage = await loadAccounts(); | ||
| const tracked = await cacheKeepManager.track({ | ||
| sessionId: relayAffinity, | ||
| url: rewritten.url?.toString() ?? rewritten.input.toString(), | ||
| headers: requestHeaders, | ||
| bodyText: body, | ||
| storage, | ||
| cacheMode: 'hybrid', | ||
| }); | ||
| if (tracked.tracked) { | ||
| trace?.mark('cachekeep_track', { session: relayAffinity }); | ||
| } | ||
| } | ||
| const directFetch = () => fetch(rewritten.input, { | ||
| ...init, | ||
| body, | ||
| headers: requestHeaders, | ||
| ...(isInsecure() && { tls: { rejectUnauthorized: false } }), | ||
| }); | ||
| const sendStart = nowMs(); | ||
| const response = await sendViaRelay({ | ||
| config: relayConfig, | ||
| input: rewritten.input, | ||
| init, | ||
| headers: requestHeaders, | ||
| body, | ||
| fallback: directFetch, | ||
| affinity: relayAffinity, | ||
| optimisticResponse: relayConfig?.transport === 'websocket', | ||
| }); | ||
| trace?.mark('send_headers_received', { | ||
| route, | ||
| ms: roundMs(nowMs() - sendStart), | ||
| status: response.status, | ||
| relayConfigured: relayConfig != null, | ||
| totalSendWithAccessMs: roundMs(nowMs() - start), | ||
| }); | ||
| return response; | ||
| } | ||
| async function refreshMainQuotaCache(accessToken, storage) { | ||
| const now = Date.now(); | ||
| const quota = await fetchOAuthQuotaSnapshot({ accessToken }); | ||
| mainQuotaCache = { | ||
| accessToken, | ||
| refreshAfter: getQuotaNextRefreshAt(quota, storage, now), | ||
| quota, | ||
| }; | ||
| return quota; | ||
| } | ||
| function refreshMainQuotaCacheInBackground(accessToken, storage) { | ||
| const now = Date.now(); | ||
| if (mainQuotaRefreshPromise || now < mainQuotaRetryAfter) | ||
| return; | ||
| mainQuotaRefreshPromise = refreshMainQuotaCache(accessToken, storage) | ||
| .catch((error) => { | ||
| mainQuotaRetryAfter = now + getQuotaCheckIntervalMs(storage); | ||
| throw error; | ||
| }) | ||
| .finally(() => { | ||
| mainQuotaRefreshPromise = null; | ||
| }); | ||
| void mainQuotaRefreshPromise.catch(() => { }); | ||
| } | ||
| async function getMainQuotaForRouting(accessToken, storage) { | ||
| const now = Date.now(); | ||
| if (mainQuotaCache?.accessToken !== accessToken) { | ||
| return await refreshMainQuotaCache(accessToken, storage); | ||
| } | ||
| if (now >= mainQuotaCache.refreshAfter) { | ||
| refreshMainQuotaCacheInBackground(accessToken, storage); | ||
| } | ||
| return mainQuotaCache.quota; | ||
| } | ||
| async function tryUsableFallbackAccounts(input, init, accounts, storage, currentResponse, trace) { | ||
| if (!accounts.length) | ||
| return currentResponse ?? null; | ||
| await currentResponse?.body?.cancel().catch(() => { }); | ||
| let lastResponse = currentResponse ?? null; | ||
| for (const [index, account] of accounts.entries()) { | ||
| const access = account.access; | ||
| if (!access) | ||
| continue; | ||
| let response = await sendWithAccessToken(input, init, access, trace, `fallback_${index}`); | ||
| lastResponse = response; | ||
| let fallbackAgain = shouldFallbackStatus(response.status, storage); | ||
| if (!fallbackAgain) { | ||
| const inspected = await inspectStreamingRateLimit(response, trace); | ||
| response = inspected.response; | ||
| lastResponse = response; | ||
| fallbackAgain = inspected.rateLimited; | ||
| } | ||
| if (!fallbackAgain) { | ||
| await fallbackManager.markUsed(account); | ||
| return response; | ||
| } | ||
| if (index < accounts.length - 1) { | ||
| await response.body?.cancel().catch(() => { }); | ||
| } | ||
| } | ||
| return lastResponse; | ||
| } | ||
| async function tryFallbackAccounts(input, init, mainResponse, preselectedAccounts, trace) { | ||
| if (!isReplayableRequest(input, init?.body)) | ||
| return mainResponse; | ||
| const loadStart = nowMs(); | ||
| const storage = await loadAccounts(); | ||
| trace?.mark('fallback_load_storage', { | ||
| ms: roundMs(nowMs() - loadStart), | ||
| }); | ||
| let currentResponse = mainResponse; | ||
| let shouldFallback = shouldFallbackStatus(currentResponse.status, storage); | ||
| if (!shouldFallback) { | ||
| const inspected = await inspectStreamingRateLimit(currentResponse, trace); | ||
| currentResponse = inspected.response; | ||
| shouldFallback = inspected.rateLimited; | ||
| } | ||
| if (!shouldFallback) { | ||
| return currentResponse; | ||
| } | ||
| let accounts = preselectedAccounts; | ||
| if (!accounts) { | ||
| const accountsStart = nowMs(); | ||
| accounts = await fallbackManager.getUsableFallbackAccounts(); | ||
| trace?.mark('fallback_get_accounts', { | ||
| ms: roundMs(nowMs() - accountsStart), | ||
| accounts: accounts.length, | ||
| }); | ||
| } | ||
| return ((await tryUsableFallbackAccounts(input, init, accounts, storage, currentResponse, trace)) ?? currentResponse); | ||
| } | ||
| return { | ||
| apiKey: '', | ||
| async fetch(input, init) { | ||
| const initialBody = init?.body; | ||
| const trace = createPerfTrace({ | ||
| bodyBytes: typeof initialBody === 'string' | ||
| ? initialBody.length | ||
| : undefined, | ||
| relayConfigured: relayConfig != null, | ||
| }); | ||
| const authStart = nowMs(); | ||
| const auth = await getAuth(); | ||
| trace.mark('get_auth', { | ||
| ms: roundMs(nowMs() - authStart), | ||
| authType: auth.type, | ||
| hasAccess: Boolean(auth.access), | ||
| }); | ||
| if (auth.type !== 'oauth') { | ||
| const response = await fetch(input, init); | ||
| trace.done('non_oauth_passthrough', { status: response.status }); | ||
| return response; | ||
| } | ||
| await clearStaleMainRefreshError(auth.refresh); | ||
| if (!auth.access || !auth.expires || auth.expires < Date.now()) { | ||
| log('[refresh] opencode main oauth refresh required for request', { | ||
| hasAccess: Boolean(auth.access), | ||
| expiresInMs: auth.expires | ||
| ? auth.expires - Date.now() | ||
| : undefined, | ||
| }); | ||
| const refreshStart = nowMs(); | ||
| auth.access = await refreshMainAccessToken(); | ||
| trace.mark('refresh_main_access', { | ||
| ms: roundMs(nowMs() - refreshStart), | ||
| }); | ||
| } | ||
| if (!auth.access) { | ||
| trace.done('missing_access_error'); | ||
| throw new Error('OAuth access token is missing after refresh'); | ||
| } | ||
| const loadStart = nowMs(); | ||
| const storage = await loadAccounts(); | ||
| trace.mark('load_storage', { ms: roundMs(nowMs() - loadStart) }); | ||
| let preselectedFallbackAccounts; | ||
| if (isReplayableRequest(input, init?.body) && | ||
| mainQuotaRoutingEnabled(storage)) { | ||
| try { | ||
| const quotaStart = nowMs(); | ||
| const mainQuota = await getMainQuotaForRouting(auth.access, storage); | ||
| trace.mark('main_quota_for_routing', { | ||
| ms: roundMs(nowMs() - quotaStart), | ||
| passes: quotaSnapshotPassesPolicy(mainQuota, storage), | ||
| }); | ||
| if (!quotaSnapshotPassesPolicy(mainQuota, storage)) { | ||
| const fallbackStart = nowMs(); | ||
| preselectedFallbackAccounts = | ||
| await fallbackManager.getUsableFallbackAccounts(); | ||
| trace.mark('preselect_fallback_accounts', { | ||
| ms: roundMs(nowMs() - fallbackStart), | ||
| accounts: preselectedFallbackAccounts.length, | ||
| }); | ||
| const fallbackResponse = await tryUsableFallbackAccounts(input, init, preselectedFallbackAccounts, storage, undefined, trace); | ||
| if (fallbackResponse) { | ||
| trace.done('return_preselected_fallback', { | ||
| status: fallbackResponse.status, | ||
| }); | ||
| return createStrippedStream(fallbackResponse); | ||
| } | ||
| } | ||
| } | ||
| catch (error) { | ||
| trace.mark('main_quota_for_routing_error', { | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| // Main quota checks should optimize routing, not break requests. | ||
| } | ||
| } | ||
| const mainResponse = await sendWithAccessToken(input, init, auth.access, trace, 'main'); | ||
| const response = await tryFallbackAccounts(input, init, mainResponse, preselectedFallbackAccounts, trace); | ||
| trace.done('return_response', { status: response.status }); | ||
| return createStrippedStream(response); | ||
| }, | ||
| }; | ||
| } | ||
| return {}; | ||
| }, | ||
| methods: [ | ||
| { | ||
| label: 'Claude Pro/Max', | ||
| type: 'oauth', | ||
| authorize: async () => { | ||
| const result = await authorize('max'); | ||
| return { | ||
| url: result.url, | ||
| instructions: 'Paste the authorization code here:', | ||
| method: 'code', | ||
| callback: async (code) => { | ||
| return exchange(code, result.verifier, result.redirectUri, result.state); | ||
| }, | ||
| }; | ||
| }, | ||
| }, | ||
| { | ||
| label: 'Create an API Key', | ||
| type: 'oauth', | ||
| authorize: async () => { | ||
| const result = await authorize('console'); | ||
| return { | ||
| url: result.url, | ||
| instructions: 'Paste the authorization code here:', | ||
| method: 'code', | ||
| callback: async (code) => { | ||
| const credentials = await exchange(code, result.verifier, result.redirectUri, result.state); | ||
| if (credentials.type === 'failed') | ||
| return credentials; | ||
| const apiKey = await fetch(`https://api.anthropic.com/api/oauth/claude_cli/create_api_key`, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| authorization: `Bearer ${credentials.access}`, | ||
| }, | ||
| }).then((r) => r.json()); | ||
| return { type: 'success', key: apiKey.raw_key }; | ||
| }, | ||
| }; | ||
| }, | ||
| }, | ||
| { | ||
| provider: 'anthropic', | ||
| label: 'Manually enter API Key', | ||
| type: 'api', | ||
| }, | ||
| ], | ||
| }, | ||
| // biome-ignore lint/suspicious/noExplicitAny: Plugin type doesn't include undocumented auth/hooks | ||
| }; | ||
| }; | ||
| import{$ as H1,A as Q1,B as L1,C as x0,D as j0,E as v1,F as J0,G as N1,H as _1,I as H0,J as F1,K as q0,L as M1,M as K0,N as Z0,O as $0,P as l,Q as B1,R as G0,S as s0,T as e0,U as S0,V as j1,W as J1,X as K1,Y as D0,Z as U1,_ as x1,a as p0,aa as q1,b as c0,ba as S1,c as a,ca as A1,d as r,da as E1,e as o0,ea as V0,f as u,fa as P0,g as t0,ga as R1,h as a0,ha as A0,i as r0,ia as X0,j as N0,ja as f1,k as W1,ka as W0,l as _0,la as C1,m as F0,ma as E0,n as H,na as Y0,oa as T1,p as S,pa as y1,q as M0,qa as k1,r as Y1,s,sa as g1,t as e,u as w1,v as z1,w as B0,x as I1,y as U0,z as O1}from"./index-sebye7nt.js";import{randomUUID as Pj}from"node:crypto";function m(j){return typeof j==="object"&&j!==null}function a1(j){if(Array.isArray(j))return j;if(m(j)&&Array.isArray(j.data))return j.data;return[]}function r1(j){if(!m(j)||!m(j.info))return;return typeof j.info.role==="string"?j.info.role:void 0}function m0(j){if(!m(j)||!m(j.info))return null;let J=j.info,K=m(J.model)?J.model:void 0,Z=typeof J.agent==="string"?J.agent:void 0,G=typeof K?.providerID==="string"?K.providerID:typeof J.providerID==="string"?J.providerID:void 0,D=typeof K?.modelID==="string"?K.modelID:typeof J.modelID==="string"?J.modelID:void 0,F=typeof K?.variant==="string"?K.variant:typeof J.variant==="string"?J.variant:void 0;if(!Z&&(!G||!D)&&!F)return null;let A={};if(Z)A.agent=Z;if(G&&D)A.model={providerID:G,modelID:D};if(F)A.variant=F;return A}function b0(j,J){return{agent:j.agent??J.agent,model:j.model??J.model,variant:j.variant??J.variant}}function h0(j){return Boolean(j.agent&&j.model&&j.variant)}async function u0(j,J){if(!j||!J)return null;let K=j;if(typeof K.session?.messages!=="function")return null;let Z=[];try{Z=a1(await Promise.resolve(K.session.messages({path:{id:J},query:{limit:100}})))}catch{return null}if(Z.length===0)return null;let G={};for(let D=Z.length-1;D>=0;D--){if(r1(Z[D])!=="assistant")continue;let F=m0(Z[D]);if(!F)continue;if(G=b0(G,F),h0(G))return G}for(let D=Z.length-1;D>=0;D--){let F=m0(Z[D]);if(!F)continue;if(G=b0(G,F),h0(G))return G}if(!G.agent&&!G.model&&!G.variant)return null;return G}function d0(j){return`${p0}${j.charAt(0).toUpperCase()}${j.slice(1)}`}function s1(j){if(j==="StructuredOutput")return j;return`${j.charAt(0).toLowerCase()}${j.slice(1)}`}function Z1(j,J){let K=new Headers;if(j instanceof Request)j.headers.forEach((G,D)=>{K.set(D,G)});let Z=J?.headers;if(Z){if(Z instanceof Headers)Z.forEach((G,D)=>{K.set(D,G)});else if(Array.isArray(Z))for(let G of Z){let[D,F]=G;if(typeof F<"u")K.set(D,String(F))}else for(let[G,D]of Object.entries(Z))if(typeof D<"u")K.set(G,String(D))}return K}function L0(j){return j.set("anthropic-beta",a(j.get("anthropic-beta"),[c0])),j}function d(j,J,K={}){return J1(j,J,K)}function e1(j){if(j.tools&&Array.isArray(j.tools))j.tools=j.tools.map((J)=>({...J,name:J.name?d0(J.name):J.name}));if(j.messages&&Array.isArray(j.messages))j.messages=j.messages.map((J)=>{if(J.content&&Array.isArray(J.content))J.content=J.content.map((K)=>{if(K.type==="tool_use"&&K.name)return{...K,name:d0(K.name)};return K});return J});return JSON.stringify(K1(j))}function O0(j){return j.replace(/"name"\s*:\s*"mcp_([^"]+)"/g,(J,K)=>`"name": "${s1(K)}"`)}function n0(j,J=!1){if(J)return{ready:O0(j),pending:""};let K=j.length,Z='"name"',G=Math.max(0,j.length-Z.length+1);for(let F=G;F<j.length;F++)if(Z.startsWith(j.slice(F))){K=Math.min(K,F);break}let D=j.lastIndexOf(Z);if(D!==-1){let F=j.slice(D);if(/^"name"\s*(?::\s*(?:"[^"]*)?)?$/.test(F))K=Math.min(K,D)}if(K<j.length)return{ready:O0(j.slice(0,K)),pending:j.slice(K)};return{ready:O0(j),pending:""}}function $1(){if(!process.env.ANTHROPIC_BASE_URL?.trim())return!1;let j=process.env.ANTHROPIC_INSECURE?.trim();return j==="1"||j==="true"}function jj(){let j=process.env.ANTHROPIC_BASE_URL?.trim();if(!j)return null;try{let J=new URL(j);if(J.protocol!=="http:"&&J.protocol!=="https:"||J.username||J.password)return null;return J}catch{return null}}function G1(j){let J=null;try{if(typeof j==="string"||j instanceof URL)J=new URL(j.toString());else if(j instanceof Request)J=new URL(j.url)}catch{J=null}if(!J)return{input:j,url:null};let K=J.href,Z=jj();if(Z)J.protocol=Z.protocol,J.host=Z.host;if(J.pathname==="/v1/messages"&&!J.searchParams.has("beta"))J.searchParams.set("beta","true");if(J.href===K)return{input:j,url:J};return{input:j instanceof Request?new Request(J.toString(),j):J,url:J}}function c(j){let Z=j.split(/\n\n+/).filter((G)=>{if(G.includes(o0))return!1;for(let D of a0)if(G.includes(D))return!1;return!0}).join(` | ||
| `);for(let G of r0)Z=Z.replace(G.match,G.replacement);return Z.trim()}function E(j){return j!=null&&typeof j==="object"&&!Array.isArray(j)}function Jj(j){let J={type:"text",text:u};if(j==null)return[J];if(typeof j==="string"){let Z=c(j);if(Z===u)return[J];return[J,{type:"text",text:Z}]}if(E(j)){let Z=typeof j.type==="string"?j.type:"text",G=typeof j.text==="string"?j.text:"";return[J,{...j,type:Z,text:c(G)}]}if(!Array.isArray(j))return[J];let K=j.map((Z)=>{if(typeof Z==="string")return{type:"text",text:c(Z)};if(E(Z)&&Z.type==="text"&&typeof Z.text==="string")return{...Z,type:"text",text:c(Z.text)};return{type:"text",text:String(Z)}});if(K[0]?.text===u)return K;return[J,...K]}var D1={type:"ephemeral",ttl:"1h"};function Kj(j){if(E(j.cache_control))return j.cache_control;if(E(j.cacheControl))return j.cacheControl;return null}function t(j,J){if(!E(j))return!1;return delete j.cacheControl,j.cache_control=J?{...D1}:{type:"ephemeral"},!0}function i0(j){if(!E(j))return;delete j.cache_control,delete j.cacheControl}function Zj(j){if(Array.isArray(j))return j;if(typeof j==="string")return[{type:"text",text:j}];return null}function o(j,J){if(!E(j))return;let K=Kj(j);if(!K||K.type!=="ephemeral")return;if(J)K.ttl="1h";else delete K.ttl}function l0(j,J){if(Array.isArray(j.system))for(let K of j.system)o(K,J);else o(j.system,J);if(!Array.isArray(j.messages))return;for(let K of j.messages)if(o(K,J),E(K)&&Array.isArray(K.content))for(let Z of K.content)o(Z,J)}function $j(j,J){if(Array.isArray(j.system))for(let K of j.system)J(K);else J(j.system);if(!Array.isArray(j.messages))return;for(let K of j.messages)if(J(K),E(K)&&Array.isArray(K.content))for(let Z of K.content)J(Z)}function V1(j){i0(j),$j(j,i0)}function Gj(j){V1(j),j.cache_control={...D1}}function Q0(j){if(!E(j))return!1;let J=Zj(j.content);if(!J?.length)return t(j,!0);j.content=J;let K=[...J].reverse().find((Z)=>E(Z)&&Z.type!=="thinking");return t(K??j,!0)}function Dj(j){if(V1(j),Array.isArray(j.system)){let K=j.system.findIndex((D)=>E(D)&&D.text===u),Z=j.system.slice(K>=0?K+1:0).filter(E),G=Z[Z.length-1];t(G,!0)}else t(j.system,!0);if(!Array.isArray(j.messages))return;Q0(j.messages[0]),Q0(j.messages[1]);let J=j.messages.length-2;if(J>1)Q0(j.messages[J])}function Vj(j,J){if(!J.enabled){l0(j,!1),delete j.cache_control,delete j.cacheControl;return}if(J.mode==="automatic"){Gj(j);return}if(J.mode==="hybrid"){Dj(j);return}l0(j,!0),delete j.cacheControl}async function P1(j,J={}){try{let K=JSON.parse(j),Z=Array.isArray(K.messages)&&K.messages.some((G)=>G.role==="user")?e0(K.messages,void 0,t0):null;if(K.system=Jj(K.system),Z&&Array.isArray(K.system))K.system.unshift({type:"text",text:Z});if(Vj(K,{enabled:J.cache1hEnabled??!1,mode:J.cache1hMode??"explicit"}),J.fastModeEnabled&&r(K.model))K.speed="fast";else if(K.speed==="fast")delete K.speed;if(J.identity)j1(K,J.identity);return await s0(e1(K))}catch{return j}}function v0(j){if(!j.body)return j;let J=j.body.getReader(),K=new TextDecoder,Z=new TextEncoder,G="",D=new ReadableStream({async pull(F){let{done:A,value:T}=await J.read();if(A){let h=n0(`${G}${K.decode()}`,!0);if(h.ready)F.enqueue(Z.encode(h.ready));F.close();return}let z0=G+K.decode(T,{stream:!0}),b=n0(z0);if(G=b.pending,b.ready)F.enqueue(Z.encode(b.ready))}});return new Response(D,{status:j.status,statusText:j.statusText,headers:j.headers})}var Xj="__OPENCODE_ANTHROPIC_AUTH_COMMAND_HANDLED__",Wj=60000,m1=240,Yj=m1,wj=1,X1=!1;function w0(){return process.env.OPENCODE_ANTHROPIC_AUTH_PERF==="1"}function M(){return performance.now()}function q(j){return Math.round(j*10)/10}function zj(){if(X1||!1||!w0())return;X1=!0;let j=100,J=250,K=M()+j;setInterval(()=>{let Z=M(),G=Z-K;if(K=Z+j,G<J)return;H("[perf] opencode event_loop_lag",{lagMs:q(G),thresholdMs:J})},j).unref?.()}function Ij(j){let J=M(),K={requestId:String(wj++),start:J,last:J,mark(Z,G){let D=M();if(w0())H("[perf] opencode request stage",{requestId:K.requestId,stage:Z,deltaMs:q(D-K.last),totalMs:q(D-K.start),...G});K.last=D},done(Z,G){let D=M();if(w0())H("[perf] opencode request done",{requestId:K.requestId,stage:Z,deltaMs:q(D-K.last),totalMs:q(D-K.start),...G});K.last=D}};if(w0())H("[perf] opencode request start",{requestId:K.requestId,...j});return K}async function n(j,J,K){let Z=j.client.session,G=await u0(j.client,J),D={path:{id:J},body:{noReply:!0,parts:[{type:"text",text:K,ignored:!0}]}};if(G?.agent)D.body.agent=G.agent;if(G?.model)D.body.model=G.model;if(G?.variant)D.body.variant=G.variant;if(typeof Z?.promptAsync==="function"){await Z.promptAsync(D);return}if(typeof Z?.prompt==="function"){await Promise.resolve(Z.prompt(D));return}throw Error("OpenCode session prompt API is unavailable for ignored replies.")}function i(){throw Error(Xj)}var Bj=async(j)=>{zj();let{client:J}=j,K=new M1;K.startBackgroundRefresh();let Z=null,G=new E1({loadStorage:()=>S(),prepareHeaders:async($,O)=>{if(!A)return $;let L=await A();if(L.type!=="oauth")return $;if(!L.access||L.expires&&L.expires<Date.now()){if(!Z)return $;L.access=await Z()}if(!L.access)return $;try{let Q=JSON.parse(O.bodyText),U=await S0(L.access,typeof Q.model==="string"?Q.model:void 0);if($.delete("anthropic-beta"),d($,L.access,{body:Q,identity:U}),$.set("anthropic-beta",a($.get("anthropic-beta"),[U1])),Q.speed==="fast")L0($)}catch{d($,L.access)}return $},log:H}),D=await S(),F=k1(D);l({enabled:s(D),mode:e(D)}),P0(B0(D)),W0(U0(D));let A=null,T=null;function z0($){let O=$?.refresh?.refreshBeforeExpiryMinutes??Yj;return Math.max(m1,O)*60000}function b($){return $?.refresh?.enabled!==!1}async function h($){if(!$)return;let O=await S(),L=O?.refresh?.mainLastRefreshError;if(!O?.refresh||!L?.tokenHash)return;let Q=j0($);if(L.tokenHash===Q)return;O.refresh.mainLastRefreshError=void 0,await M0(O),H("[refresh] opencode main oauth cleared stale backoff after token rotation",{previousCheckedAt:L.checkedAt,previousNextRetryAt:L.nextRetryAt,previousRetryCount:L.retryCount})}async function b1(){let $=[];if(A)try{let Q=await A();if(Q.type==="oauth"&&Q.access)$.push({name:"OpenCode anthropic",role:"main",quota:await q0({accessToken:Q.access})});else if(Q.type==="oauth")$.push({name:"OpenCode anthropic",role:"main",error:"missing access token; send a request first or reconnect auth"})}catch(Q){$.push({name:"OpenCode anthropic",role:"main",error:Q instanceof Error?Q.message:String(Q)})}let{storage:O,errors:L}=await K.refreshQuotaForAllAccounts();if($.push(...T1(O,new Map(L.map((Q)=>[Q.accountId,Q.message])))),!A)$.unshift({name:"OpenCode anthropic",role:"main",error:"auth loader has not run yet; send a request first"});return y1({accounts:$,refreshedAt:Date.now()})}async function h1($){let O=B1($);if(O.type==="enable"||O.type==="disable"){let f=O.type==="enable",y=await w1(f),k=e(y);return l({enabled:f,mode:k}),G0({argumentsText:$,enabled:f,mode:k})}if(O.type==="mode"){let f=await z1(O.mode),y=s(f);return l({enabled:y,mode:O.mode}),G0({argumentsText:$,enabled:y,mode:O.mode})}let L=await S(),Q=s(L),U=e(L);return l({enabled:Q,mode:U}),G0({argumentsText:$,enabled:Q,mode:U})}async function u1($){let O=x1($),L=await S();if(O.type==="window")L=await Q1(O.startHour,O.endHour);else if(O.type==="disable")L=await L1(!1);let Q=H1(L),U=G.stats(Q);return A1({argumentsText:$,enabled:q1(L),window:Q,hybridActive:S1(L),trackedSessions:U.trackedSessions,nextPrewarmAt:U.nextPrewarmAt})}async function d1($){let O=R1($);if(O.type==="enable"||O.type==="disable"){let U=O.type==="enable";return await I1(U),P0(U),A0({argumentsText:$,enabled:U})}let L=await S(),Q=B0(L);return P0(Q),A0({argumentsText:$,enabled:Q})}async function n1($){let O=C1($);if(O.type==="enable"||O.type==="disable"){let U=O.type==="enable";return await O1(U),W0(U),E0({argumentsText:$,enabled:U})}let L=await S(),Q=U0(L);return W0(Q),E0({argumentsText:$,enabled:Q})}return{config:async($)=>{$.command={...$.command??{},[K0]:{template:K0,description:"Show or toggle 1-hour Anthropic ephemeral prompt cache TTL."},[D0]:{template:D0,description:"Keep hybrid Claude cache warm for recently used sessions during a local time window."},[Y0]:{template:Y0,description:"Show current Claude OAuth quota usage for all accounts."},[V0]:{template:V0,description:"Show or toggle Anthropic request dump capture for debugging."},[X0]:{template:X0,description:"Show or toggle Anthropic fast mode for supported Opus models."}}},"command.execute.before":async($)=>{if($.command===K0)await n(j,$.sessionID,await h1($.arguments)),i();if($.command===D0)await n(j,$.sessionID,await u1($.arguments)),i();if($.command===Y0)await n(j,$.sessionID,await b1()),i();if($.command===V0)await n(j,$.sessionID,await d1($.arguments)),i();if($.command===X0)await n(j,$.sessionID,await n1($.arguments)),i()},auth:{provider:"anthropic",async loader($,O){if(A=$,(await $()).type==="oauth"){let i1=function(){if(T)clearInterval(T),T=null;let V=async()=>{try{let X=await S();if(!b(X))return;let Y=await $();if(Y.type!=="oauth")return;if(await h(Y.refresh),!Y.expires)return;let I=Y.expires-Date.now(),B=z0(X);if(I>B)return;if(H("[refresh] opencode main oauth background due",{expiresInMs:I,refreshBeforeMs:B}),Y.refresh&&J0(X?.refresh?.mainLastRefreshError,Y.refresh,Date.now())){H("[refresh] opencode main oauth background skipped backoff",{nextRetryAt:X?.refresh?.mainLastRefreshError?.nextRetryAt,retryCount:X?.refresh?.mainLastRefreshError?.retryCount});return}if(Y.refresh&&X?.refresh?.mainRefreshLeaseUntil&&X.refresh.mainRefreshLeaseUntil>Date.now()&&X.refresh.mainRefreshLeaseTokenHash===j0(Y.refresh))return;await k(),H("[refresh] opencode main oauth refreshed in background",{expires:Y.expires})}catch(X){H("[refresh] opencode main oauth refresh failed",{message:X instanceof Error?X.message:String(X)})}};if(T=setInterval(()=>{V()},Wj),"unref"in T)T.unref()},R0=function(V,X){if(V instanceof Request&&V.body)return!1;return X==null||typeof X==="string"},l1=function(V){return V.has("x-parent-session-id")},f0=function(V){return V.includes("rate_limit_error")||/exceed your account'?s rate limit/i.test(V)},p1=function(V){return V?.quota?.enabled===!0},c1=function(V,X){let Y=Date.now();if(f||Y<y)return;f=y0(V,X).catch((I)=>{throw y=Y+_1(X),I}).finally(()=>{f=null}),f.catch(()=>{})};for(let V of Object.values(O.models))V.cost={input:0,output:0,cache:{read:0,write:0}};let Q=null,U=null,f=null,y=0;async function k(){if(!Q)Q=(async()=>{let Y=null,I=null,B=null;async function _(z){let v=await S()??{version:1,main:{type:"opencode",provider:"anthropic"},accounts:[]};v.refresh=v.refresh??{},z(v),await M0(v)}for(let z=0;z<=2;z++){let v=null;try{if(z>0){let x=500*2**(z-1);await new Promise((I0)=>setTimeout(I0,x))}if(v=await $(),!v.refresh)throw Error("Token refresh failed: missing refresh token");let P=await S(),W=j0(v.refresh),w=P?.refresh?.mainLastRefreshError;if(H("[refresh] opencode main oauth refresh check",{attempt:z,expiresInMs:v.expires?v.expires-Date.now():void 0,hasBackoff:Boolean(w),backoffActive:w?J0(w,v.refresh,Date.now()):!1,retryCount:w?.retryCount,nextRetryAt:w?.nextRetryAt}),w&&J0(w,v.refresh,Date.now()))throw H("[refresh] opencode main oauth refresh skipped backoff",{nextRetryAt:w.nextRetryAt,retryCount:w.retryCount}),Error(N1(w,Date.now()));if(P?.refresh?.mainRefreshLeaseUntil&&P.refresh.mainRefreshLeaseUntil>Date.now()&&P.refresh.mainRefreshLeaseTokenHash===W)throw H("[refresh] opencode main oauth refresh skipped lease",{leaseUntil:P.refresh.mainRefreshLeaseUntil}),Error("Claude OAuth refresh is already in progress");let N=await Y1({name:"opencode-main-oauth-refresh",ttlMs:120000});if(!N)throw H("[refresh] opencode main oauth refresh skipped file lock"),Error("Claude OAuth refresh is already in progress");B=N.release,Y=Pj(),I=W,await _((x)=>{x.refresh=x.refresh??{},x.refresh.mainRefreshLeaseId=Y??void 0,x.refresh.mainRefreshLeaseUntil=Date.now()+120000,x.refresh.mainRefreshLeaseTokenHash=W});let C=await S();if(H("[refresh] opencode main oauth refresh lease acquired",{attempt:z,leaseUntil:Date.now()+120000}),C?.refresh?.mainRefreshLeaseId!==Y||C.refresh.mainRefreshLeaseTokenHash!==W)throw Error("Claude OAuth refresh is already in progress");H("[refresh] opencode main oauth refresh request start",{attempt:z});let R=await W1({refreshToken:v.refresh,maxRetries:0});return await J.auth.set({path:{id:"anthropic"},body:{type:"oauth",refresh:R.refresh,access:R.access,expires:R.expires}}),await _((x)=>{if(!x?.refresh)return;if(x.refresh.mainLastRefreshError=void 0,x.refresh.mainRefreshLeaseId===Y)x.refresh.mainRefreshLeaseId=void 0,x.refresh.mainRefreshLeaseUntil=void 0,x.refresh.mainRefreshLeaseTokenHash=void 0}),H("[refresh] opencode main oauth refresh succeeded",{attempt:z,expiresInMs:R.expires-Date.now()}),R.access}catch(P){let W=P instanceof Error&&(P.message.includes("fetch failed")||("code"in P)&&(P.code==="ECONNRESET"||P.code==="ECONNREFUSED"||P.code==="ETIMEDOUT"||P.code==="UND_ERR_CONNECT_TIMEOUT"));if(z<2&&(W||P instanceof N0&&P.status>=500))continue;H("[refresh] opencode main oauth refresh attempt failed",{attempt:z,error:P instanceof Error?P.message:String(P),transient:W});let w=v?.refresh;if(w&&P instanceof N0)await _((N)=>{N.refresh=N.refresh??{},N.refresh.mainLastRefreshError=v1({error:P,now:Date.now(),refreshToken:w,previous:N.refresh.mainLastRefreshError})});throw P}finally{if(Y)await _((P)=>{if(!P?.refresh)return;if(P.refresh.mainRefreshLeaseId===Y&&P.refresh.mainRefreshLeaseTokenHash===I)P.refresh.mainRefreshLeaseId=void 0,P.refresh.mainRefreshLeaseUntil=void 0,P.refresh.mainRefreshLeaseTokenHash=void 0}).catch(()=>{});await B?.().catch(()=>{})}}throw Error("Token refresh exhausted all retries")})().finally(()=>{Q=null});return Q}Z=k,i1();async function C0(V,X){if(!V.body||V.status!==200)return X?.mark("inspect_stream_skip",{status:V.status}),{response:V,rateLimited:!1};if(V.headers.get("x-cortexkit-relay-optimistic")==="true")return X?.mark("inspect_stream_skip",{status:V.status,reason:"optimistic_relay"}),{response:V,rateLimited:!1};let Y=M(),I=V.body.getReader(),B=[],_=new TextDecoder,z="",v=0;while(!z.includes(` | ||
| `)&&z.length<65536){let{done:W,value:w}=await I.read();if(W)break;if(B.push(w),v+=w.byteLength,z+=_.decode(w,{stream:!0}),f0(z))break}if(f0(z))return await I.cancel().catch(()=>{}),X?.mark("inspect_stream_first_event",{ms:q(M()-Y),bytes:v,rateLimited:!0}),{response:V,rateLimited:!0};let P=new ReadableStream({start(W){for(let w of B)W.enqueue(w)},async pull(W){let{done:w,value:N}=await I.read();if(w){W.close();return}W.enqueue(N)},cancel(W){return I.cancel(W)}});return X?.mark("inspect_stream_first_event",{ms:q(M()-Y),bytes:v,rateLimited:!1}),{response:new Response(P,{status:V.status,statusText:V.statusText,headers:V.headers}),rateLimited:!1}}async function T0(V,X,Y,I,B="unknown"){let _=M(),z=Z1(V,X),v=z.get("x-session-affinity")||z.get("x-opencode-session"),P=l1(z);z.delete("x-parent-session-id"),z.delete("x-session-affinity"),z.delete("x-opencode-session");let W=X?.body,w;if(W&&typeof W==="string")try{let g=JSON.parse(W);if(typeof g.model==="string")w=g.model}catch{}let N=await S0(Y,w),C=typeof W==="string"?W.length:void 0;if(W&&typeof W==="string"){let g=M(),p=(()=>{if(!f1())return!1;try{return r(JSON.parse(W).model)}catch{return!1}})();W=await P1(W,{cache1hEnabled:!P&&Z0(),cache1hMode:$0(),fastModeEnabled:p,identity:N});try{d(z,Y,{body:JSON.parse(W),identity:N})}catch{d(z,Y,{identity:N})}if(p)L0(z);I?.mark("rewrite_body",{route:B,ms:q(M()-g),originalBytes:C,rewrittenBytes:W.length,cacheEnabled:!P&&Z0(),cacheMode:$0(),fastModeEnabled:p,subagent:P})}let R=G1(V);if(B==="main"&&typeof W==="string"&&!P&&Z0()&&$0()==="hybrid"){let g=await S();if((await G.track({sessionId:v,url:R.url?.toString()??R.input.toString(),headers:z,bodyText:W,storage:g,cacheMode:"hybrid"})).tracked)I?.mark("cachekeep_track",{session:v})}let x=()=>fetch(R.input,{...X,body:W,headers:z,...$1()&&{tls:{rejectUnauthorized:!1}}}),I0=M(),g0=await g1({config:F,input:R.input,init:X,headers:z,body:W,fallback:x,affinity:v,optimisticResponse:F?.transport==="websocket"});return I?.mark("send_headers_received",{route:B,ms:q(M()-I0),status:g0.status,relayConfigured:F!=null,totalSendWithAccessMs:q(M()-_)}),g0}async function y0(V,X){let Y=Date.now(),I=await q0({accessToken:V});return U={accessToken:V,refreshAfter:F1(I,X,Y),quota:I},I}async function o1(V,X){let Y=Date.now();if(U?.accessToken!==V)return await y0(V,X);if(Y>=U.refreshAfter)c1(V,X);return U.quota}async function k0(V,X,Y,I,B,_){if(!Y.length)return B??null;await B?.body?.cancel().catch(()=>{});let z=B??null;for(let[v,P]of Y.entries()){let W=P.access;if(!W)continue;let w=await T0(V,X,W,_,`fallback_${v}`);z=w;let N=x0(w.status,I);if(!N){let C=await C0(w,_);w=C.response,z=w,N=C.rateLimited}if(!N)return await K.markUsed(P),w;if(v<Y.length-1)await w.body?.cancel().catch(()=>{})}return z}async function t1(V,X,Y,I,B,_){if(!R0(V,X?.body))return Y;let z=M(),v=_??await S();B?.mark("fallback_load_storage",{ms:q(M()-z),cached:!!_});let P=Y,W=x0(P.status,v);if(!W){let N=await C0(P,B);P=N.response,W=N.rateLimited}if(!W)return P;let w=I;if(!w){let N=M();w=await K.getUsableFallbackAccounts(),B?.mark("fallback_get_accounts",{ms:q(M()-N),accounts:w.length})}return await k0(V,X,w,v,P,B)??P}return{apiKey:"",async fetch(V,X){let Y=X?.body,I=Ij({bodyBytes:typeof Y==="string"?Y.length:void 0,relayConfigured:F!=null}),B=M(),_=await $();if(I.mark("get_auth",{ms:q(M()-B),authType:_.type,hasAccess:Boolean(_.access)}),_.type!=="oauth"){let N=await fetch(V,X);return I.done("non_oauth_passthrough",{status:N.status}),N}if(await h(_.refresh),!_.access||!_.expires||_.expires<Date.now()){H("[refresh] opencode main oauth refresh required for request",{hasAccess:Boolean(_.access),expiresInMs:_.expires?_.expires-Date.now():void 0});let N=M();_.access=await k(),I.mark("refresh_main_access",{ms:q(M()-N)})}if(!_.access)throw I.done("missing_access_error"),Error("OAuth access token is missing after refresh");let z=M(),v=await S();I.mark("load_storage",{ms:q(M()-z)});let P;if(R0(V,X?.body)&&p1(v))try{let N=M(),C=await o1(_.access,v);if(I.mark("main_quota_for_routing",{ms:q(M()-N),passes:H0(C,v)}),!H0(C,v)){let R=M();P=await K.getUsableFallbackAccounts(),I.mark("preselect_fallback_accounts",{ms:q(M()-R),accounts:P.length});let x=await k0(V,X,P,v,void 0,I);if(x)return I.done("return_preselected_fallback",{status:x.status}),v0(x)}}catch(N){I.mark("main_quota_for_routing_error",{error:N instanceof Error?N.message:String(N)})}let W=await T0(V,X,_.access,I,"main"),w=await t1(V,X,W,P,I,v);return I.done("return_response",{status:w.status}),v0(w)}}}return{}},methods:[{label:"Claude Pro/Max",type:"oauth",authorize:async()=>{let $=await _0("max");return{url:$.url,instructions:"Paste the authorization code here:",method:"code",callback:async(O)=>{return F0(O,$.verifier,$.redirectUri,$.state)}}}},{label:"Create an API Key",type:"oauth",authorize:async()=>{let $=await _0("console");return{url:$.url,instructions:"Paste the authorization code here:",method:"code",callback:async(O)=>{let L=await F0(O,$.verifier,$.redirectUri,$.state);if(L.type==="failed")return L;return{type:"success",key:(await fetch("https://api.anthropic.com/api/oauth/claude_cli/create_api_key",{method:"POST",headers:{"Content-Type":"application/json",authorization:`Bearer ${L.access}`}}).then((U)=>U.json())).raw_key}}}}},{provider:"anthropic",label:"Manually enter API Key",type:"api"}]}}};export{Bj as AnthropicAuthPlugin}; |
+5
-4
| { | ||
| "name": "@cortexkit/opencode-anthropic-auth", | ||
| "version": "1.1.3", | ||
| "version": "1.2.0", | ||
| "type": "module", | ||
@@ -22,3 +22,3 @@ "repository": { | ||
| "engines": { | ||
| "bun": "1.3.11" | ||
| "bun": "1.3.14" | ||
| }, | ||
@@ -31,3 +31,4 @@ "files": [ | ||
| "scripts": { | ||
| "build": "rm -rf dist && tsc -p tsconfig.build.json", | ||
| "build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin --minify && tsc -p tsconfig.build.json --emitDeclarationOnly", | ||
| "build:dev": "rm -rf dist && tsc -p tsconfig.build.json", | ||
| "dev": "bun ../../scripts/dev.ts", | ||
@@ -45,4 +46,4 @@ "dev:clean": "bun ../../scripts/dev-clean.ts", | ||
| "dependencies": { | ||
| "@cortexkit/anthropic-auth-core": "1.1.3" | ||
| "@cortexkit/anthropic-auth-core": "1.2.0" | ||
| } | ||
| } |
+19
-0
@@ -484,2 +484,21 @@ # CortexKit Anthropic Auth for OpenCode and Pi | ||
| ### Build modes | ||
| Two build modes are available: | ||
| ```bash | ||
| bun run build # Deploy: bun build → bundled dist/ with all deps inlined (core + xxhash-wasm) | ||
| bun run build:dev # Dev: tsc → individual dist/*.js files (requires workspace node_modules) | ||
| ``` | ||
| The default `build` uses `bun build` to bundle `@cortexkit/anthropic-auth-core` and all transitive dependencies into self-contained output files. No `node_modules/` needed at runtime — the plugin works via `file://` path in OpenCode config: | ||
| ```json | ||
| { | ||
| "plugin": ["file:///path/to/anthropic-auth/packages/opencode"] | ||
| } | ||
| ``` | ||
| `@opencode-ai/plugin` remains external (peer dep provided by OpenCode). | ||
| Inspect package contents: | ||
@@ -486,0 +505,0 @@ |
| function isRecord(value) { | ||
| return typeof value === 'object' && value !== null; | ||
| } | ||
| function extractMessages(response) { | ||
| if (Array.isArray(response)) | ||
| return response; | ||
| if (isRecord(response) && Array.isArray(response.data)) | ||
| return response.data; | ||
| return []; | ||
| } | ||
| function getRole(message) { | ||
| if (!isRecord(message) || !isRecord(message.info)) | ||
| return undefined; | ||
| return typeof message.info.role === 'string' ? message.info.role : undefined; | ||
| } | ||
| function extractFromMessage(message) { | ||
| if (!isRecord(message) || !isRecord(message.info)) | ||
| return null; | ||
| const info = message.info; | ||
| const modelInfo = isRecord(info.model) ? info.model : undefined; | ||
| const agent = typeof info.agent === 'string' ? info.agent : undefined; | ||
| const providerID = typeof modelInfo?.providerID === 'string' | ||
| ? modelInfo.providerID | ||
| : typeof info.providerID === 'string' | ||
| ? info.providerID | ||
| : undefined; | ||
| const modelID = typeof modelInfo?.modelID === 'string' | ||
| ? modelInfo.modelID | ||
| : typeof info.modelID === 'string' | ||
| ? info.modelID | ||
| : undefined; | ||
| const variant = typeof modelInfo?.variant === 'string' | ||
| ? modelInfo.variant | ||
| : typeof info.variant === 'string' | ||
| ? info.variant | ||
| : undefined; | ||
| if (!agent && (!providerID || !modelID) && !variant) | ||
| return null; | ||
| const context = {}; | ||
| if (agent) | ||
| context.agent = agent; | ||
| if (providerID && modelID) | ||
| context.model = { providerID, modelID }; | ||
| if (variant) | ||
| context.variant = variant; | ||
| return context; | ||
| } | ||
| function mergeContexts(base, patch) { | ||
| return { | ||
| agent: base.agent ?? patch.agent, | ||
| model: base.model ?? patch.model, | ||
| variant: base.variant ?? patch.variant, | ||
| }; | ||
| } | ||
| function isComplete(context) { | ||
| return Boolean(context.agent && context.model && context.variant); | ||
| } | ||
| export async function resolvePromptContext(client, sessionId) { | ||
| if (!client || !sessionId) | ||
| return null; | ||
| const typedClient = client; | ||
| if (typeof typedClient.session?.messages !== 'function') | ||
| return null; | ||
| let messages = []; | ||
| try { | ||
| messages = extractMessages(await Promise.resolve(typedClient.session.messages({ | ||
| path: { id: sessionId }, | ||
| query: { limit: 100 }, | ||
| }))); | ||
| } | ||
| catch { | ||
| return null; | ||
| } | ||
| if (messages.length === 0) | ||
| return null; | ||
| let result = {}; | ||
| for (let index = messages.length - 1; index >= 0; index--) { | ||
| if (getRole(messages[index]) !== 'assistant') | ||
| continue; | ||
| const context = extractFromMessage(messages[index]); | ||
| if (!context) | ||
| continue; | ||
| result = mergeContexts(result, context); | ||
| if (isComplete(result)) | ||
| return result; | ||
| } | ||
| for (let index = messages.length - 1; index >= 0; index--) { | ||
| const context = extractFromMessage(messages[index]); | ||
| if (!context) | ||
| continue; | ||
| result = mergeContexts(result, context); | ||
| if (isComplete(result)) | ||
| return result; | ||
| } | ||
| if (!result.agent && !result.model && !result.variant) | ||
| return null; | ||
| return result; | ||
| } |
| import { applyClaudeCodeHeaders, applyClaudeCodeMetadata, buildBillingHeaderValue, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_IDENTITY, FAST_MODE_BETA, isFastModeSupportedModel, mergeAnthropicBetas, OPENCODE_IDENTITY_PREFIX, orderClaudeCodeBody, PARAGRAPH_REMOVAL_ANCHORS, REQUIRED_BETAS, signRequestBody, TEXT_REPLACEMENTS, TOOL_PREFIX, } from '@cortexkit/anthropic-auth-core'; | ||
| /** | ||
| * Prefix a tool name with TOOL_PREFIX and uppercase the first character. | ||
| * Claude Code uses PascalCase tool names (e.g. mcp_Bash, mcp_Read); | ||
| * lowercase names (mcp_bash, mcp_read) are flagged as non-Claude-Code clients. | ||
| */ | ||
| function prefixName(name) { | ||
| return `${TOOL_PREFIX}${name.charAt(0).toUpperCase()}${name.slice(1)}`; | ||
| } | ||
| /** | ||
| * Reverse prefixName: strip TOOL_PREFIX and restore the original leading case. | ||
| */ | ||
| function unprefixName(name) { | ||
| // StructuredOutput is still used as StructuredOutput | ||
| if (name === 'StructuredOutput') { | ||
| return name; | ||
| } | ||
| return `${name.charAt(0).toLowerCase()}${name.slice(1)}`; | ||
| } | ||
| /** | ||
| * Merge headers from a Request object and/or a RequestInit headers value | ||
| * into a single Headers instance. | ||
| */ | ||
| export function mergeHeaders(input, init) { | ||
| const headers = new Headers(); | ||
| if (input instanceof Request) { | ||
| input.headers.forEach((value, key) => { | ||
| headers.set(key, value); | ||
| }); | ||
| } | ||
| const initHeaders = init?.headers; | ||
| if (initHeaders) { | ||
| if (initHeaders instanceof Headers) { | ||
| initHeaders.forEach((value, key) => { | ||
| headers.set(key, value); | ||
| }); | ||
| } | ||
| else if (Array.isArray(initHeaders)) { | ||
| for (const entry of initHeaders) { | ||
| const [key, value] = entry; | ||
| if (typeof value !== 'undefined') { | ||
| headers.set(key, String(value)); | ||
| } | ||
| } | ||
| } | ||
| else { | ||
| for (const [key, value] of Object.entries(initHeaders)) { | ||
| if (typeof value !== 'undefined') { | ||
| headers.set(key, String(value)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return headers; | ||
| } | ||
| /** | ||
| * Merge incoming beta headers with the required OAuth betas, deduplicating. | ||
| */ | ||
| export function mergeBetaHeaders(headers) { | ||
| const incomingBeta = headers.get('anthropic-beta') || ''; | ||
| const incomingBetasList = incomingBeta | ||
| .split(',') | ||
| .map((b) => b.trim()) | ||
| .filter(Boolean); | ||
| return [...new Set([...REQUIRED_BETAS, ...incomingBetasList])].join(','); | ||
| } | ||
| export function addFastModeBetaHeader(headers) { | ||
| headers.set('anthropic-beta', mergeAnthropicBetas(headers.get('anthropic-beta'), [FAST_MODE_BETA])); | ||
| return headers; | ||
| } | ||
| /** | ||
| * Set OAuth-required headers on the request: authorization, beta, user-agent. | ||
| * Removes x-api-key since we're using OAuth. | ||
| */ | ||
| export function setOAuthHeaders(headers, accessToken, options = {}) { | ||
| return applyClaudeCodeHeaders(headers, accessToken, options); | ||
| } | ||
| /** | ||
| * Add TOOL_PREFIX to tool names in the request body. | ||
| * Prefixes both tool definitions and tool_use blocks in messages. | ||
| */ | ||
| export function prefixToolNames(parsed) { | ||
| if (parsed.tools && Array.isArray(parsed.tools)) { | ||
| parsed.tools = parsed.tools.map((tool) => ({ | ||
| ...tool, | ||
| name: tool.name ? prefixName(tool.name) : tool.name, | ||
| })); | ||
| } | ||
| if (parsed.messages && Array.isArray(parsed.messages)) { | ||
| parsed.messages = parsed.messages.map((msg) => { | ||
| if (msg.content && Array.isArray(msg.content)) { | ||
| msg.content = msg.content.map((block) => { | ||
| if (block.type === 'tool_use' && block.name) { | ||
| return { ...block, name: prefixName(block.name) }; | ||
| } | ||
| return block; | ||
| }); | ||
| } | ||
| return msg; | ||
| }); | ||
| } | ||
| return JSON.stringify(orderClaudeCodeBody(parsed)); | ||
| } | ||
| /** | ||
| * Strip TOOL_PREFIX from tool names in streaming response text. | ||
| */ | ||
| export function stripToolPrefix(text) { | ||
| return text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, (_match, name) => `"name": "${unprefixName(name)}"`); | ||
| } | ||
| function splitToolPrefixRewriteBuffer(buffer, flush = false) { | ||
| if (flush) | ||
| return { ready: stripToolPrefix(buffer), pending: '' }; | ||
| let keepFrom = buffer.length; | ||
| const marker = '"name"'; | ||
| const partialMarkerStart = Math.max(0, buffer.length - marker.length + 1); | ||
| for (let index = partialMarkerStart; index < buffer.length; index++) { | ||
| if (marker.startsWith(buffer.slice(index))) { | ||
| keepFrom = Math.min(keepFrom, index); | ||
| break; | ||
| } | ||
| } | ||
| const lastName = buffer.lastIndexOf(marker); | ||
| if (lastName !== -1) { | ||
| const tail = buffer.slice(lastName); | ||
| if (/^"name"\s*(?::\s*(?:"[^"]*)?)?$/.test(tail)) { | ||
| keepFrom = Math.min(keepFrom, lastName); | ||
| } | ||
| } | ||
| if (keepFrom < buffer.length) { | ||
| return { | ||
| ready: stripToolPrefix(buffer.slice(0, keepFrom)), | ||
| pending: buffer.slice(keepFrom), | ||
| }; | ||
| } | ||
| return { ready: stripToolPrefix(buffer), pending: '' }; | ||
| } | ||
| /** | ||
| * Check if TLS verification should be skipped for custom API endpoints. | ||
| * Only effective when ANTHROPIC_BASE_URL is also set. | ||
| */ | ||
| export function isInsecure() { | ||
| if (!process.env.ANTHROPIC_BASE_URL?.trim()) | ||
| return false; | ||
| const raw = process.env.ANTHROPIC_INSECURE?.trim(); | ||
| return raw === '1' || raw === 'true'; | ||
| } | ||
| /** | ||
| * Parse ANTHROPIC_BASE_URL from the environment. | ||
| * Returns a valid HTTP(S) URL or null if unset/invalid. | ||
| */ | ||
| function resolveBaseUrl() { | ||
| const raw = process.env.ANTHROPIC_BASE_URL?.trim(); | ||
| if (!raw) | ||
| return null; | ||
| try { | ||
| const baseUrl = new URL(raw); | ||
| if ((baseUrl.protocol !== 'http:' && baseUrl.protocol !== 'https:') || | ||
| baseUrl.username || | ||
| baseUrl.password) { | ||
| return null; | ||
| } | ||
| return baseUrl; | ||
| } | ||
| catch { | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * Rewrite the request URL to add ?beta=true for /v1/messages requests. | ||
| * When ANTHROPIC_BASE_URL is set, overrides the origin (protocol + host) | ||
| * for all API requests flowing through the fetch wrapper. | ||
| * Returns the modified input and URL (if applicable). | ||
| */ | ||
| export function rewriteUrl(input) { | ||
| let requestUrl = null; | ||
| try { | ||
| if (typeof input === 'string' || input instanceof URL) { | ||
| requestUrl = new URL(input.toString()); | ||
| } | ||
| else if (input instanceof Request) { | ||
| requestUrl = new URL(input.url); | ||
| } | ||
| } | ||
| catch { | ||
| requestUrl = null; | ||
| } | ||
| if (!requestUrl) | ||
| return { input, url: null }; | ||
| const originalHref = requestUrl.href; | ||
| const baseUrl = resolveBaseUrl(); | ||
| if (baseUrl) { | ||
| requestUrl.protocol = baseUrl.protocol; | ||
| requestUrl.host = baseUrl.host; | ||
| } | ||
| if (requestUrl.pathname === '/v1/messages' && | ||
| !requestUrl.searchParams.has('beta')) { | ||
| requestUrl.searchParams.set('beta', 'true'); | ||
| } | ||
| if (requestUrl.href === originalHref) { | ||
| return { input, url: requestUrl }; | ||
| } | ||
| const newInput = input instanceof Request | ||
| ? new Request(requestUrl.toString(), input) | ||
| : requestUrl; | ||
| return { input: newInput, url: requestUrl }; | ||
| } | ||
| /** | ||
| * Sanitize OpenCode-branded strings from the system prompt text. | ||
| * | ||
| * 1. Removes the OPENCODE_IDENTITY paragraph. | ||
| * 2. Removes any paragraph (text between blank lines) that contains | ||
| * one of the PARAGRAPH_REMOVAL_ANCHORS — typically URLs that | ||
| * identify OpenCode-specific content. | ||
| * 3. Applies TEXT_REPLACEMENTS for inline occurrences of "OpenCode" | ||
| * inside paragraphs we want to keep. | ||
| * | ||
| * This approach is resilient to upstream rewording of the OpenCode | ||
| * prompt — as long as the anchor strings (URLs, etc.) still appear | ||
| * somewhere in the paragraph, the removal works. | ||
| */ | ||
| export function sanitizeSystemText(text) { | ||
| // Split into paragraphs (separated by one or more blank lines) | ||
| const paragraphs = text.split(/\n\n+/); | ||
| const filtered = paragraphs.filter((paragraph) => { | ||
| if (paragraph.includes(OPENCODE_IDENTITY_PREFIX)) { | ||
| // If the paragraph contains the identity, drop it entirely | ||
| return false; | ||
| } | ||
| // Remove paragraphs containing any removal anchor | ||
| for (const anchor of PARAGRAPH_REMOVAL_ANCHORS) { | ||
| if (paragraph.includes(anchor)) | ||
| return false; | ||
| } | ||
| return true; | ||
| }); | ||
| let result = filtered.join('\n\n'); | ||
| // Apply inline text replacements | ||
| for (const rule of TEXT_REPLACEMENTS) { | ||
| result = result.replace(rule.match, rule.replacement); | ||
| } | ||
| return result.trim(); | ||
| } | ||
| function isRecord(value) { | ||
| return value != null && typeof value === 'object' && !Array.isArray(value); | ||
| } | ||
| /** | ||
| * Sanitize system prompt and prepend Claude Code identity. | ||
| * Handles all Anthropic API system formats: undefined, string, or array of text blocks. | ||
| */ | ||
| export function prependClaudeCodeIdentity(system) { | ||
| const identityBlock = { | ||
| type: 'text', | ||
| text: CLAUDE_CODE_IDENTITY, | ||
| }; | ||
| if (system == null) | ||
| return [identityBlock]; | ||
| if (typeof system === 'string') { | ||
| const sanitized = sanitizeSystemText(system); | ||
| if (sanitized === CLAUDE_CODE_IDENTITY) | ||
| return [identityBlock]; | ||
| return [identityBlock, { type: 'text', text: sanitized }]; | ||
| } | ||
| if (isRecord(system)) { | ||
| const type = typeof system.type === 'string' ? system.type : 'text'; | ||
| const text = typeof system.text === 'string' ? system.text : ''; | ||
| return [identityBlock, { ...system, type, text: sanitizeSystemText(text) }]; | ||
| } | ||
| if (!Array.isArray(system)) | ||
| return [identityBlock]; | ||
| const sanitized = system.map((item) => { | ||
| if (typeof item === 'string') { | ||
| return { type: 'text', text: sanitizeSystemText(item) }; | ||
| } | ||
| if (isRecord(item) && | ||
| item.type === 'text' && | ||
| typeof item.text === 'string') { | ||
| return { | ||
| ...item, | ||
| type: 'text', | ||
| text: sanitizeSystemText(item.text), | ||
| }; | ||
| } | ||
| return { type: 'text', text: String(item) }; | ||
| }); | ||
| // Idempotency: don't double-prepend if first block already has the identity | ||
| if (sanitized[0]?.text === CLAUDE_CODE_IDENTITY) { | ||
| return sanitized; | ||
| } | ||
| return [identityBlock, ...sanitized]; | ||
| } | ||
| const CACHE_1H_CONTROL = { type: 'ephemeral', ttl: '1h' }; | ||
| function getCacheControl(value) { | ||
| if (isRecord(value.cache_control)) | ||
| return value.cache_control; | ||
| if (isRecord(value.cacheControl)) | ||
| return value.cacheControl; | ||
| return null; | ||
| } | ||
| function setWireCacheControl(value, withTtl) { | ||
| if (!isRecord(value)) | ||
| return false; | ||
| delete value.cacheControl; | ||
| value.cache_control = withTtl | ||
| ? { ...CACHE_1H_CONTROL } | ||
| : { type: 'ephemeral' }; | ||
| return true; | ||
| } | ||
| function removeCacheControl(value) { | ||
| if (!isRecord(value)) | ||
| return; | ||
| delete value.cache_control; | ||
| delete value.cacheControl; | ||
| } | ||
| function normalizeContentToArray(content) { | ||
| if (Array.isArray(content)) | ||
| return content; | ||
| if (typeof content === 'string') | ||
| return [{ type: 'text', text: content }]; | ||
| return null; | ||
| } | ||
| function updateCacheControlTtl(value, cache1hEnabled) { | ||
| if (!isRecord(value)) | ||
| return; | ||
| const cacheControl = getCacheControl(value); | ||
| if (!cacheControl || cacheControl.type !== 'ephemeral') | ||
| return; | ||
| if (cache1hEnabled) { | ||
| cacheControl.ttl = '1h'; | ||
| } | ||
| else { | ||
| delete cacheControl.ttl; | ||
| } | ||
| } | ||
| function applyCache1hTtl(parsed, cache1hEnabled) { | ||
| if (Array.isArray(parsed.system)) { | ||
| for (const block of parsed.system) | ||
| updateCacheControlTtl(block, cache1hEnabled); | ||
| } | ||
| else { | ||
| updateCacheControlTtl(parsed.system, cache1hEnabled); | ||
| } | ||
| if (!Array.isArray(parsed.messages)) | ||
| return; | ||
| for (const message of parsed.messages) { | ||
| updateCacheControlTtl(message, cache1hEnabled); | ||
| if (isRecord(message) && Array.isArray(message.content)) { | ||
| for (const block of message.content) { | ||
| updateCacheControlTtl(block, cache1hEnabled); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| function walkCacheControlTargets(parsed, visitor) { | ||
| if (Array.isArray(parsed.system)) { | ||
| for (const block of parsed.system) | ||
| visitor(block); | ||
| } | ||
| else { | ||
| visitor(parsed.system); | ||
| } | ||
| if (!Array.isArray(parsed.messages)) | ||
| return; | ||
| for (const message of parsed.messages) { | ||
| visitor(message); | ||
| if (isRecord(message) && Array.isArray(message.content)) { | ||
| for (const block of message.content) | ||
| visitor(block); | ||
| } | ||
| } | ||
| } | ||
| function removeAllCacheControls(parsed) { | ||
| removeCacheControl(parsed); | ||
| walkCacheControlTargets(parsed, removeCacheControl); | ||
| } | ||
| function applyAutomaticCache1h(parsed) { | ||
| removeAllCacheControls(parsed); | ||
| parsed.cache_control = { ...CACHE_1H_CONTROL }; | ||
| } | ||
| function setMessageCacheAnchor(message) { | ||
| if (!isRecord(message)) | ||
| return false; | ||
| const content = normalizeContentToArray(message.content); | ||
| if (!content?.length) | ||
| return setWireCacheControl(message, true); | ||
| message.content = content; | ||
| const lastCacheableBlock = [...content] | ||
| .reverse() | ||
| .find((block) => isRecord(block) && block.type !== 'thinking'); | ||
| return setWireCacheControl(lastCacheableBlock ?? message, true); | ||
| } | ||
| function applyHybridCache1h(parsed) { | ||
| removeAllCacheControls(parsed); | ||
| if (Array.isArray(parsed.system)) { | ||
| const identityIndex = parsed.system.findIndex((block) => isRecord(block) && block.text === CLAUDE_CODE_IDENTITY); | ||
| const cacheableSystemBlocks = parsed.system | ||
| .slice(identityIndex >= 0 ? identityIndex + 1 : 0) | ||
| .filter(isRecord); | ||
| const lastSystemBlock = cacheableSystemBlocks[cacheableSystemBlocks.length - 1]; | ||
| setWireCacheControl(lastSystemBlock, true); | ||
| } | ||
| else { | ||
| setWireCacheControl(parsed.system, true); | ||
| } | ||
| if (!Array.isArray(parsed.messages)) | ||
| return; | ||
| setMessageCacheAnchor(parsed.messages[0]); | ||
| setMessageCacheAnchor(parsed.messages[1]); | ||
| const movingMessageIndex = parsed.messages.length - 2; | ||
| if (movingMessageIndex > 1) { | ||
| setMessageCacheAnchor(parsed.messages[movingMessageIndex]); | ||
| } | ||
| } | ||
| function applyCache1hStrategy(parsed, options) { | ||
| if (!options.enabled) { | ||
| applyCache1hTtl(parsed, false); | ||
| delete parsed.cache_control; | ||
| delete parsed.cacheControl; | ||
| return; | ||
| } | ||
| if (options.mode === 'automatic') { | ||
| applyAutomaticCache1h(parsed); | ||
| return; | ||
| } | ||
| if (options.mode === 'hybrid') { | ||
| applyHybridCache1h(parsed); | ||
| return; | ||
| } | ||
| applyCache1hTtl(parsed, true); | ||
| delete parsed.cacheControl; | ||
| } | ||
| /** | ||
| * Rewrite the full request body: sanitize system prompt and prefix tool names. | ||
| */ | ||
| export async function rewriteRequestBody(body, options = {}) { | ||
| try { | ||
| const parsed = JSON.parse(body); | ||
| const billingHeader = Array.isArray(parsed.messages) && | ||
| parsed.messages.some((message) => message.role === 'user') | ||
| ? buildBillingHeaderValue(parsed.messages, undefined, CLAUDE_CODE_ENTRYPOINT) | ||
| : null; | ||
| // Sanitize system prompt and prepend Claude Code identity | ||
| parsed.system = prependClaudeCodeIdentity(parsed.system); | ||
| // Prepend the billing header as a separate system block so the | ||
| // final layout is: [billing header, identity, ...rest] | ||
| if (billingHeader && Array.isArray(parsed.system)) { | ||
| parsed.system.unshift({ type: 'text', text: billingHeader }); | ||
| } | ||
| applyCache1hStrategy(parsed, { | ||
| enabled: options.cache1hEnabled ?? false, | ||
| mode: options.cache1hMode ?? 'explicit', | ||
| }); | ||
| if (options.fastModeEnabled && isFastModeSupportedModel(parsed.model)) { | ||
| parsed.speed = 'fast'; | ||
| } | ||
| else if (parsed.speed === 'fast') { | ||
| delete parsed.speed; | ||
| } | ||
| if (options.identity) | ||
| applyClaudeCodeMetadata(parsed, options.identity); | ||
| return await signRequestBody(prefixToolNames(parsed)); | ||
| } | ||
| catch { | ||
| return body; | ||
| } | ||
| } | ||
| /** | ||
| * Create a streaming response that strips the tool prefix from tool names. | ||
| */ | ||
| export function createStrippedStream(response) { | ||
| if (!response.body) | ||
| return response; | ||
| const reader = response.body.getReader(); | ||
| const decoder = new TextDecoder(); | ||
| const encoder = new TextEncoder(); | ||
| let pending = ''; | ||
| const stream = new ReadableStream({ | ||
| async pull(controller) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| const flushed = splitToolPrefixRewriteBuffer(`${pending}${decoder.decode()}`, true); | ||
| if (flushed.ready) | ||
| controller.enqueue(encoder.encode(flushed.ready)); | ||
| controller.close(); | ||
| return; | ||
| } | ||
| const text = pending + decoder.decode(value, { stream: true }); | ||
| const rewritten = splitToolPrefixRewriteBuffer(text); | ||
| pending = rewritten.pending; | ||
| if (rewritten.ready) | ||
| controller.enqueue(encoder.encode(rewritten.ready)); | ||
| }, | ||
| }); | ||
| return new Response(stream, { | ||
| status: response.status, | ||
| statusText: response.statusText, | ||
| headers: response.headers, | ||
| }); | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
128285
11.27%560
3.51%10
-9.09%725
-63.84%4
300%12
33.33%13
160%+ Added
+ Added
+ Added
- Removed
- Removed
- Removed