Files
Bohdan Triapitsyn a8f0f99c3d fix(quota): normalize OpenCode Go reset timestamps
Go returns ISO reset dates, but the shared usage contract requires numeric timestamps. Convert dates to milliseconds before building usage windows.

Verified all three windows against the live Go API. Added numeric timestamp and partial-window regressions; 14 focused tests, web type-check, lint, and syntax checks pass.
2026-09-09 23:23:22 +03:00

72 lines
2.8 KiB
JavaScript

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 windowsByApiKey = {
'5h': 'rolling',
weekly: 'weekly',
monthly: 'monthly',
};
export const parseOpenCodeGoUsage = (payload) => {
const usage = payload && typeof payload === 'object' ? payload.usage : null;
if (!usage || typeof usage !== 'object') return {};
const windows = {};
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: new Date(resetAt).getTime(),
windowSeconds: null,
});
}
return windows;
};
export const fetchOpenCodeGoUsage = async (apiKey, fetchImpl = fetch) => {
const response = await fetchImpl('https://opencode.ai/zen/go/v1/usage', {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
'x-opencode-session': 'openchamber-usage',
'User-Agent': 'OpenChamber quota provider',
},
signal: AbortSignal.timeout(15_000),
});
if (response.status === 401 || response.status === 403) {
throw new Error('OpenCode Go authentication failed');
}
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;
};
const getApiKey = () => {
const entry = normalizeAuthEntry(getAuthEntry(readAuthFile(), aliases));
return entry?.key ?? entry?.token ?? null;
};
export const isConfigured = () => Boolean(getApiKey());
export const fetchQuota = async () => {
try {
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' });
}
};