feat(quota): add OpenCode Go usage tracking (#2155)

* feat(quota): add OpenCode Go usage tracking

* fix(quota): align OpenCode Go VS Code parsing
This commit is contained in:
Bohdan Triapitsyn
2026-07-12 14:48:50 +03:00
committed by GitHub
parent b4f50e0a01
commit 3d90eddcaf
26 changed files with 658 additions and 1 deletions
@@ -6,6 +6,7 @@ import { randomUUID } from 'crypto';
import { removeProviderConfig, getProviderSources } from './opencodeConfig';
import { getProviderAuth, removeProviderAuth } from './opencodeAuth';
import { fetchQuotaForProvider, listConfiguredQuotaProviders } from './quotaProviders';
import { deleteOpenCodeGoCredential, fetchOpenCodeGoUsage, getOpenCodeGoCredentialStatus, normalizeOpenCodeGoCredential, readOpenCodeGoCredential, writeOpenCodeGoCredential } from './opencodeGoQuota';
import { getSessionActivitySnapshot } from './sessionActivityWatcher';
import type { BridgeContext, BridgeResponse } from './bridge';
@@ -481,6 +482,29 @@ export async function handleSystemBridgeMessage(
}
}
case 'api:quota:opencode-go-credentials': {
const { method, credential: input } = (payload || {}) as { method?: string; credential?: unknown };
try {
if (method === 'GET') return { id, type, success: true, data: getOpenCodeGoCredentialStatus() };
if (method === 'DELETE') { deleteOpenCodeGoCredential(); return { id, type, success: true, data: { configured: false } }; }
if (method === 'PUT') {
const credential = normalizeOpenCodeGoCredential(input);
if (!credential) return { id, type, success: false, error: 'Workspace ID and auth cookie are required' };
await fetchOpenCodeGoUsage(credential);
return { id, type, success: true, data: writeOpenCodeGoCredential(credential) };
}
if (method === 'VALIDATE') {
const credential = readOpenCodeGoCredential();
if (!credential) return { id, type, success: false, error: 'Not configured' };
await fetchOpenCodeGoUsage(credential);
return { id, type, success: true, data: { valid: true } };
}
return { id, type, success: false, error: 'Unsupported method' };
} catch (error) {
return { id, type, success: false, error: error instanceof Error ? error.message : String(error) };
}
}
case 'api:quota:get': {
const { providerId } = (payload || {}) as { providerId?: string };
if (!providerId) {
+76
View File
@@ -0,0 +1,76 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
type OpenCodeGoCredential = { workspaceId: string; authCookie: string };
const targetPath = () => path.join(process.env.OPENCHAMBER_DATA_DIR ? path.resolve(process.env.OPENCHAMBER_DATA_DIR) : path.join(os.homedir(), '.config', 'openchamber'), 'quota', 'opencode-go.json');
export const normalizeOpenCodeGoCredential = (value: unknown): OpenCodeGoCredential | null => {
if (!value || typeof value !== 'object') return null;
const data = value as Record<string, unknown>;
const workspaceId = typeof data.workspaceId === 'string' ? data.workspaceId.trim() : '';
let authCookie = typeof data.authCookie === 'string' ? data.authCookie.trim() : '';
if (authCookie.startsWith('auth=')) authCookie = authCookie.slice(5).trim();
return workspaceId && authCookie && !/[\r\n]/.test(workspaceId + authCookie) ? { workspaceId, authCookie } : null;
};
export const readOpenCodeGoCredential = (): OpenCodeGoCredential | null => {
try {
return normalizeOpenCodeGoCredential(JSON.parse(fs.readFileSync(targetPath(), 'utf8')));
} catch (error) {
if ((error as { code?: string }).code !== 'ENOENT') {
console.warn('Failed to read OpenCode Go credentials');
}
return null;
}
};
export const getOpenCodeGoCredentialStatus = () => {
const value = readOpenCodeGoCredential();
return value ? { configured: true, workspaceId: value.workspaceId, authCookieMasked: '••••••••' } : { configured: false };
};
export const writeOpenCodeGoCredential = (value: OpenCodeGoCredential) => {
const target = targetPath();
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
try {
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
fs.chmodSync(temporary, 0o600);
fs.renameSync(temporary, target);
fs.chmodSync(target, 0o600);
} finally {
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
}
return getOpenCodeGoCredentialStatus();
};
export const deleteOpenCodeGoCredential = () => { try { fs.unlinkSync(targetPath()); } catch (error) { if ((error as { code?: string }).code !== 'ENOENT') throw error; } };
const toWindow = (usedPercent: number, resetInSec: number) => ({
usedPercent: Math.min(100, Math.max(0, usedPercent)),
remainingPercent: 100 - Math.min(100, Math.max(0, usedPercent)),
windowSeconds: null,
resetAfterSeconds: Math.max(0, resetInSec),
resetAt: Date.now() + Math.max(0, resetInSec) * 1000,
resetAtFormatted: null,
resetAfterFormatted: null,
});
export const fetchOpenCodeGoUsage = async (credential: OpenCodeGoCredential) => {
const response = await fetch(`https://opencode.ai/workspace/${encodeURIComponent(credential.workspaceId)}/go`, { headers: { Accept: 'text/html', Cookie: `auth=${credential.authCookie}` }, signal: AbortSignal.timeout(15_000) });
if (response.status === 401 || response.status === 403 || (response.redirected && /\/auth(?:\/|$|\?)/.test(new URL(response.url).pathname))) throw new Error('OpenCode Go authentication failed');
if (!response.ok) throw new Error(`OpenCode Go dashboard returned HTTP ${response.status}`);
const html = (await response.text()).replaceAll('&quot;', '"').replaceAll('&#34;', '"').replaceAll('\\u0022', '"').replaceAll('\\"', '"');
const windows: Record<string, ReturnType<typeof toWindow>> = {};
for (const [key, field] of Object.entries({ '5h': 'rollingUsage', weekly: 'weeklyUsage', monthly: 'monthlyUsage' })) {
const body = html.match(new RegExp(`["']?${field}["']?\\s*:\\s*(?:\\$R\\[\\d+\\]\\s*=\\s*)?\\{([^{}]*)\\}`, 's'))?.[1];
if (!body) continue;
const used = Number(body.match(/usagePercent\s*:\s*["']?(-?\d+(?:\.\d+)?)/)?.[1]);
const reset = Number(body.match(/resetInSec\s*:\s*["']?(-?\d+(?:\.\d+)?)/)?.[1]);
if (Number.isFinite(used) && Number.isFinite(reset)) windows[key] = toWindow(used, reset);
}
if (!Object.keys(windows).length) throw new Error('OpenCode Go usage data could not be parsed');
return windows;
};
+11
View File
@@ -1,6 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fetchOpenCodeGoUsage, readOpenCodeGoCredential } from './opencodeGoQuota';
type AuthEntry = Record<string, unknown> | string;
type AuthFile = Record<string, AuthEntry>;
@@ -388,6 +389,7 @@ const durationToSeconds = (duration?: number, unit?: string) => {
export const listConfiguredQuotaProviders = () => {
const auth = readAuthFile();
const configured = new Set<string>();
if (readOpenCodeGoCredential()) configured.add('opencode-go');
const anthropicAuth = normalizeAuthEntry(getAuthEntry(auth, ['anthropic', 'claude']));
if (anthropicAuth && ((anthropicAuth as Record<string, unknown>).access || (anthropicAuth as Record<string, unknown>).token)) {
@@ -1892,6 +1894,15 @@ export const fetchQuotaForProvider = async (providerId: string): Promise<Provide
return fetchZhipuaiCodingPlanQuota();
case 'wafer':
return fetchWaferQuota();
case 'opencode-go': {
const credential = readOpenCodeGoCredential();
if (!credential) return buildResult({ providerId, providerName: 'OpenCode Go', ok: false, configured: false, error: 'Not configured' });
try {
return buildResult({ providerId, providerName: 'OpenCode Go', ok: true, configured: true, usage: { windows: await fetchOpenCodeGoUsage(credential) } });
} catch (error) {
return buildResult({ providerId, providerName: 'OpenCode Go', ok: false, configured: true, error: error instanceof Error ? error.message : 'Request failed' });
}
}
default:
return buildResult({
providerId,
+12
View File
@@ -1041,6 +1041,18 @@ const handleLocalApiRequest = async (input: RequestInfo | URL, url: URL, init: R
}
}
if (pathname === '/api/quota/credentials/opencode-go' || pathname === '/api/quota/credentials/opencode-go/validate') {
try {
const body = method === 'PUT' ? await extractJsonBody(input, init, method) : undefined;
const bridgeMethod = pathname.endsWith('/validate') ? 'VALIDATE' : method;
const data = await sendBridgeMessage('api:quota:opencode-go-credentials', { method: bridgeMethod, credential: body });
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return new Response(JSON.stringify({ error: message }), { status: 400, headers: { 'Content-Type': 'application/json' } });
}
}
const quotaMatch = pathname.match(/^\/api\/quota\/([^/]+)$/);
if (quotaMatch && method === 'GET') {
const providerId = decodeURIComponent(quotaMatch[1]);