summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/credentials/opencode.ts
blob: b8a9d4d03945fc22e7911081811eb77aac850f9d (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
127
128
129
130
131
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;
	}
}