feat: switch OpenCode Go usage to API

OpenCode Go now reads quota usage with a bearer API key from OpenCode auth.json
Removes the old workspace ID and browser cookie credential flow
Deletes legacy OpenCode Go credential files during upgrade
This commit is contained in:
Bohdan Triapitsyn
2026-08-12 01:49:22 +03:00
parent d1224213ca
commit 5917325d49
29 changed files with 192 additions and 182 deletions
@@ -1,68 +1,68 @@
import { readOpenCodeGoCredential } from '../opencode-go-credentials.js';
import { buildResult, toUsageWindow } from '../utils/index.js';
import { readAuthFile } from '../../opencode/auth.js';
import { deleteLegacyOpenCodeGoCredential } from '../credentials/store.js';
import { buildResult, getAuthEntry, normalizeAuthEntry, toUsageWindow } from '../utils/index.js';
export const providerId = 'opencode-go';
export const providerName = 'OpenCode Go';
export const aliases = ['opencode-go'];
const patterns = {
'5h': 'rollingUsage',
weekly: 'weeklyUsage',
monthly: 'monthlyUsage',
const windowsByApiKey = {
'5h': 'rolling',
weekly: 'weekly',
monthly: 'monthly',
};
const captureNumber = (name, body) => {
const match = body.match(new RegExp(`["']?${name}["']?\\s*:\\s*["']?(-?\\d+(?:\\.\\d+)?)`));
const value = match ? Number(match[1]) : null;
return Number.isFinite(value) ? value : null;
};
export const parseOpenCodeGoUsage = (html, now = Date.now()) => {
if (typeof html !== 'string') return {};
const normalized = html.replaceAll('"', '"').replaceAll('"', '"').replaceAll('\\u0022', '"').replaceAll('\\"', '"');
export const parseOpenCodeGoUsage = (payload) => {
const usage = payload && typeof payload === 'object' ? payload.usage : null;
if (!usage || typeof usage !== 'object') return {};
const windows = {};
for (const [key, field] of Object.entries(patterns)) {
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = normalized.match(new RegExp(`["']?${escaped}["']?\\s*:\\s*(?:\\$R\\[\\d+\\]\\s*=\\s*)?\\{([^{}]*)\\}`, 's'));
if (!match) continue;
const usedPercent = captureNumber('usagePercent', match[1]);
const resetInSec = captureNumber('resetInSec', match[1]);
if (usedPercent === null || resetInSec === null) continue;
for (const [key, apiKey] of Object.entries(windowsByApiKey)) {
const entry = usage[apiKey];
if (!entry || typeof entry !== 'object') continue;
const usedPercent = entry.percent;
const resetAt = entry.resetsAt;
if (typeof usedPercent !== 'number' || !Number.isFinite(usedPercent)) continue;
if (typeof resetAt !== 'string' || !Number.isFinite(new Date(resetAt).getTime())) continue;
windows[key] = toUsageWindow({
usedPercent: Math.min(100, Math.max(0, usedPercent)),
resetAt: now + Math.max(0, resetInSec) * 1000,
resetAt,
windowSeconds: null,
});
}
return windows;
};
export const fetchOpenCodeGoUsage = async (credential, fetchImpl = fetch) => {
const response = await fetchImpl(`https://opencode.ai/workspace/${encodeURIComponent(credential.workspaceId)}/go`, {
export const fetchOpenCodeGoUsage = async (apiKey, fetchImpl = fetch) => {
const response = await fetchImpl('https://opencode.ai/zen/go/v1/usage', {
headers: {
Accept: 'text/html,application/xhtml+xml',
Cookie: `auth=${credential.authCookie}`,
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
'User-Agent': 'OpenChamber quota provider',
},
redirect: 'manual',
signal: AbortSignal.timeout(15_000),
});
if (response.status === 401 || response.status === 403 || (response.status >= 300 && response.status < 400)) {
if (response.status === 401 || response.status === 403) {
throw new Error('OpenCode Go authentication failed');
}
if (!response.ok) throw new Error(`OpenCode Go dashboard returned HTTP ${response.status}`);
const windows = parseOpenCodeGoUsage(await response.text());
if (!response.ok) throw new Error(`OpenCode Go usage API returned HTTP ${response.status}`);
const windows = parseOpenCodeGoUsage(await response.json().catch(() => null));
if (Object.keys(windows).length === 0) throw new Error('OpenCode Go usage data could not be parsed');
return windows;
};
export const isConfigured = () => Boolean(readOpenCodeGoCredential());
const getApiKey = () => {
const entry = normalizeAuthEntry(getAuthEntry(readAuthFile(), aliases));
return entry?.key ?? entry?.token ?? null;
};
export const isConfigured = () => Boolean(getApiKey());
export const fetchQuota = async () => {
const credential = readOpenCodeGoCredential();
if (!credential) return buildResult({ providerId, providerName, ok: false, configured: false, error: 'Not configured' });
try {
const windows = await fetchOpenCodeGoUsage(credential);
deleteLegacyOpenCodeGoCredential();
const apiKey = getApiKey();
if (!apiKey) return buildResult({ providerId, providerName, ok: false, configured: false, error: 'Not configured' });
const windows = await fetchOpenCodeGoUsage(apiKey);
return buildResult({ providerId, providerName, ok: true, configured: true, usage: { windows } });
} catch (error) {
return buildResult({ providerId, providerName, ok: false, configured: true, error: error instanceof Error ? error.message : 'Request failed' });