summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/credentials/opencode.ts
blob: d4d485183a8e92f207b25ad3cab0c5bd752bce0b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import { resolveApiKey } from "./api-keys.js";

// ─── 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 {
	// Check DB for workspace ID: stored as "opencode-ws1", "opencode-ws2", or "opencode-ws"
	const match = keyId.match(/opencode-(\d+)$/i);
	if (match) {
		const num = match[1];
		const specific = resolveApiKey(`opencode-ws${num}`);
		if (specific) return specific;
	}
	return resolveApiKey("opencode-ws") ?? undefined;
}

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 = resolveApiKey("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;
	}
}