diff options
Diffstat (limited to 'packages/core/src/credentials/opencode.ts')
| -rw-r--r-- | packages/core/src/credentials/opencode.ts | 129 |
1 files changed, 129 insertions, 0 deletions
diff --git a/packages/core/src/credentials/opencode.ts b/packages/core/src/credentials/opencode.ts new file mode 100644 index 0000000..7a74486 --- /dev/null +++ b/packages/core/src/credentials/opencode.ts @@ -0,0 +1,129 @@ +// ─── OpenCode Usage Tracking ────────────────────────────────── +// OpenCode has no public usage API. We scrape usage from the +// SolidStart SSR-rendered workspace page using a session cookie. +// Requires OPENCODE_COOKIE env var. +// Workspace IDs: OPENCODE_WS1_ID for opencode-1, OPENCODE_WS2_ID for opencode-2. + +export interface OpencodeUsageBucket { + utilization?: number; // 0-1 fraction + resetsAt?: number; // Unix timestamp ms +} + +export interface OpencodeUsageReport { + fiveHour?: OpencodeUsageBucket; + weekly?: OpencodeUsageBucket; + monthly?: OpencodeUsageBucket; +} + +function getWorkspaceId(keyId: string): string | undefined { + // Match ai-usage convention: opencode-1 → OPENCODE_WS1_ID, opencode-2 → OPENCODE_WS2_ID + const match = keyId.match(/opencode-(\d+)$/i); + if (match) { + const num = match[1]; + const specific = process.env[`OPENCODE_WS${num}_ID`]; + if (specific) return specific; + } + return process.env.OPENCODE_WS_ID; +} + +function parseOcDouble(html: string, key: string): number { + const idx = html.indexOf(`${key}:`); + if (idx === -1) return 0; + let start = idx + key.length + 1; + while (start < html.length && html[start] === " ") start++; + let end = start; + while (end < html.length && html[end] !== "," && html[end] !== "}") { + end++; + } + const val = parseFloat(html.slice(start, end)); + return Number.isNaN(val) ? 0 : val; +} + +function parseOcInt(html: string, key: string): number { + const idx = html.indexOf(`${key}:`); + if (idx === -1) return 0; + let i = idx + key.length + 1; + while (i < html.length && html[i] === " ") i++; + return parseInt(html.slice(i), 10) || 0; +} + +function parseOcBucket( + html: string, + bucketName: string, +): { utilization: number; resetsAt: number } | null { + const search = `${bucketName}:`; + const pos = html.indexOf(search); + if (pos === -1) return null; + + // Find the opening brace after the bucket name + const brace = html.indexOf("{", pos); + if (brace === -1) return null; + + const resetSecs = parseOcInt(html.slice(brace), "resetInSec"); + const usagePct = parseOcDouble(html.slice(brace), "usagePercent"); + + const utilization = usagePct / 100; // convert 0-100% to 0-1 fraction + const resetsAt = Date.now() + resetSecs * 1000; + + return { utilization, resetsAt }; +} + +export async function fetchOpencodeUsage( + keyId: string, +): Promise<OpencodeUsageReport | null> { + const cookie = process.env.OPENCODE_COOKIE; + const wsId = getWorkspaceId(keyId); + + if (!cookie || !wsId) { + return null; + } + + const url = `https://opencode.ai/workspace/${encodeURIComponent(wsId)}/go`; + + try { + const response = await fetch(url, { + headers: { + accept: "text/html", + cookie: `auth=${cookie}`, + }, + redirect: "follow", + }); + + if (!response.ok) return null; + + const html = await response.text(); + + // Auth redirect check + if ( + html.includes("/auth/authorize") || + html.includes('window.location="/auth/authorize"') + ) { + return null; + } + + // Find the lite.subscription data block. + // HTML contains: lite.subscription.get[\"<wsId>\"] + // We need literal backslashes; use \x5c (hex for backslash). + const wsKey = `lite.subscription.get[\x5c"${wsId}\x5c"]`; + const wsPos = html.indexOf(wsKey); + if (wsPos === -1) return null; + + // Search for the resolved data starting from the ws key position + const minePos = html.indexOf("mine:", wsPos); + const slice = minePos !== -1 ? html.slice(minePos) : ""; + + const fiveHour = parseOcBucket(slice, "rollingUsage"); + const weekly = parseOcBucket(slice, "weeklyUsage"); + const monthly = parseOcBucket(slice, "monthlyUsage"); + + if (!fiveHour && !weekly && !monthly) return null; + + return { + fiveHour: fiveHour ?? undefined, + weekly: weekly ?? undefined, + monthly: monthly ?? undefined, + }; + } catch { + return null; + } +} |
