feat: add exe.dev usage tracking

This commit is contained in:
Bohdan Triapitsyn
2026-09-03 11:49:34 +03:00
parent 62a18f07a0
commit da6ab52a65
30 changed files with 300 additions and 16 deletions
@@ -5,21 +5,23 @@ import { toast } from '@/components/ui';
import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
type ProviderId = 'ollama-cloud' | 'cursor';
type ProviderId = 'exe-dev' | 'ollama-cloud' | 'cursor';
type Status = { configured: boolean; secretMasked?: string };
type CredentialPayload = { usageToken?: string; cookie?: string; accessToken?: string; refreshToken?: string };
const EXE_DEV_TOKEN_COMMAND = `ssh exe.dev "ssh-key generate-api-key --label=openchamber --exp=30d --cmds='billing credits usage'"`;
export const QuotaCredentials: React.FC<{ providerId: ProviderId; providerName: string }> = ({ providerId, providerName }) => {
const { t } = useI18n();
const [status, setStatus] = React.useState<Status | null>(null);
const [values, setValues] = React.useState<Record<string, string>>({});
const [values, setValues] = React.useState<CredentialPayload>({});
const [busy, setBusy] = React.useState(false);
const route = `/api/quota/credentials/${providerId}`;
React.useEffect(() => { void runtimeFetch(route).then(async (response) => {
if (!response.ok) throw new Error();
const next = await response.json() as Status;
const next: Status = await response.json();
setStatus(next); setValues({});
}).catch(() => setStatus({ configured: false })); }, [route]);
const request = async (path: string, method: string, body?: object) => {
const request = async (path: string, method: string, body?: CredentialPayload) => {
setBusy(true);
try {
const response = await runtimeFetch(path, { method, headers: body ? { 'Content-Type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined });
@@ -31,11 +33,16 @@ export const QuotaCredentials: React.FC<{ providerId: ProviderId; providerName:
} catch (error) { toast.error(error instanceof Error && error.message ? error.message : t('settings.providers.page.openCodeGo.saveFailed')); }
finally { setBusy(false); }
};
const field = (name: string, label: string, placeholder: string) => <label className="block typography-ui-label text-foreground">{label}<Input className="mt-1 h-7 font-mono text-xs" type="password" autoComplete="off" value={values[name] ?? ''} onChange={(event) => setValues((current) => ({ ...current, [name]: event.target.value }))} placeholder={status?.secretMasked ?? placeholder} /></label>;
const field = (name: keyof CredentialPayload, label: string, placeholder: string) => <label className="block typography-ui-label text-foreground">{label}<Input className="mt-1 h-7 font-mono text-xs" type="password" autoComplete="off" value={values[name] ?? ''} onChange={(event) => setValues((current) => ({ ...current, [name]: event.target.value }))} placeholder={status?.secretMasked ?? placeholder} /></label>;
return <div data-settings-item={`usage.${providerId}-credentials`} className="mb-8">
<div className="mb-1 px-1"><h3 className="typography-ui-header font-medium text-foreground">{providerName}</h3></div>
<section className="space-y-3 px-2 pb-2 pt-0">
{providerId === 'exe-dev' && <div className="space-y-1.5">
<p className="typography-meta text-muted-foreground">{t('settings.providers.page.quotaCredentials.exeDevTokenInstructions')}</p>
<code className="typography-code block whitespace-pre-wrap break-all rounded bg-muted/50 px-2 py-1.5 text-xs text-foreground">{EXE_DEV_TOKEN_COMMAND}</code>
</div>}
{providerId === 'ollama-cloud' && field('cookie', t('settings.providers.page.openCodeGo.authCookie'), 'session=...')}
{providerId === 'exe-dev' && field('usageToken', t('settings.providers.page.quotaCredentials.usageToken'), t('settings.providers.page.quotaCredentials.tokenPlaceholder'))}
{providerId === 'cursor' && field('accessToken', t('settings.providers.page.quotaCredentials.accessToken'), t('settings.providers.page.quotaCredentials.tokenPlaceholder'))}
{providerId === 'cursor' && field('refreshToken', t('settings.providers.page.quotaCredentials.refreshToken'), t('settings.providers.page.quotaCredentials.tokenPlaceholder'))}
<div className="flex flex-wrap gap-2">
@@ -80,7 +80,7 @@ export const UsagePage: React.FC = () => {
? selectedResult.error
: null;
const showInDropdown = selectedProviderId ? dropdownProviderIds.includes(selectedProviderId) : false;
const hasCredentialsForm = selectedProviderId === 'ollama-cloud' || selectedProviderId === 'cursor';
const hasCredentialsForm = selectedProviderId === 'exe-dev' || selectedProviderId === 'ollama-cloud' || selectedProviderId === 'cursor';
const handleDropdownToggle = React.useCallback((enabled: boolean) => {
if (!selectedProviderId) {
return;
@@ -204,7 +204,7 @@ export const UsagePage: React.FC = () => {
</div>
)}
{(selectedProviderId === 'ollama-cloud' || selectedProviderId === 'cursor') && (
{(selectedProviderId === 'exe-dev' || selectedProviderId === 'ollama-cloud' || selectedProviderId === 'cursor') && (
<QuotaCredentials providerId={selectedProviderId} providerName={providerName} />
)}
@@ -2136,6 +2136,8 @@ export const settingsDict = {
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Warteschlange',
'settings.providers.page.quotaCredentials.accessToken': 'Zugriffstoken',
'settings.providers.page.quotaCredentials.usageToken': 'Nutzungs-API-Token',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': 'Führen Sie diesen Befehl im Terminal aus und fügen Sie dann das Token unten ein. Es kann nur die LLM-Guthabennutzung lesen und läuft nach 30 Tagen ab.',
'settings.providers.page.quotaCredentials.refreshToken': 'Aktualisierungstoken',
'settings.providers.page.quotaCredentials.tokenPlaceholder': 'Token einfügen',
'settings.view.nav.group.general': 'OpenChamber',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': 'Delete',
'settings.providers.page.quotaCredentials.saved': '{provider} credentials saved.',
'settings.providers.page.quotaCredentials.accessToken': 'Access token',
'settings.providers.page.quotaCredentials.usageToken': 'Usage API token',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': 'Run this command in your terminal, then paste the token below. It can only read LLM credit usage and expires after 30 days.',
'settings.providers.page.quotaCredentials.refreshToken': 'Refresh token',
'settings.providers.page.quotaCredentials.tokenPlaceholder': 'Paste token',
'settings.providers.page.openCodeGo.saveFailed': 'Could not validate OpenCode Go credentials.',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': 'Eliminar',
'settings.providers.page.quotaCredentials.saved': 'Credenciales de {provider} guardadas.',
'settings.providers.page.quotaCredentials.accessToken': 'Token de acceso',
'settings.providers.page.quotaCredentials.usageToken': 'Token de API de uso',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': 'Ejecuta este comando en tu terminal y pega el token abajo. Solo puede leer el uso de créditos de LLM y caduca después de 30 días.',
'settings.providers.page.quotaCredentials.refreshToken': 'Token de actualización',
'settings.providers.page.quotaCredentials.tokenPlaceholder': 'Pega el token',
'settings.providers.page.openCodeGo.saveFailed': 'No se pudieron validar las credenciales de OpenCode Go.',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': 'Supprimer',
'settings.providers.page.quotaCredentials.saved': 'Identifiants de {provider} enregistrés.',
'settings.providers.page.quotaCredentials.accessToken': 'Jeton daccès',
'settings.providers.page.quotaCredentials.usageToken': 'Jeton API dutilisation',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': 'Exécutez cette commande dans votre terminal, puis collez le jeton ci-dessous. Il peut uniquement lire lutilisation des crédits LLM et expire après 30 jours.',
'settings.providers.page.quotaCredentials.refreshToken': 'Jeton dactualisation',
'settings.providers.page.quotaCredentials.tokenPlaceholder': 'Collez le jeton',
'settings.providers.page.openCodeGo.saveFailed': 'Impossible de valider les identifiants OpenCode Go.',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': '削除',
'settings.providers.page.quotaCredentials.saved': '{provider} の認証情報を保存しました。',
'settings.providers.page.quotaCredentials.accessToken': 'アクセストークン',
'settings.providers.page.quotaCredentials.usageToken': '使用量 API トークン',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': 'このコマンドをターミナルで実行し、下にトークンを貼り付けてください。LLM クレジット使用量の読み取りのみが可能で、30 日後に期限切れになります。',
'settings.providers.page.quotaCredentials.refreshToken': '更新トークン',
'settings.providers.page.quotaCredentials.tokenPlaceholder': 'トークンを貼り付け',
'settings.providers.page.openCodeGo.saveFailed': 'OpenCode Go の認証情報を検証できませんでした。',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': '삭제',
'settings.providers.page.quotaCredentials.saved': '{provider} 인증 정보를 저장했습니다.',
'settings.providers.page.quotaCredentials.accessToken': '액세스 토큰',
'settings.providers.page.quotaCredentials.usageToken': '사용량 API 토큰',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': '터미널에서 이 명령을 실행한 다음 아래에 토큰을 붙여 넣으세요. LLM 크레딧 사용량만 읽을 수 있으며 30일 후 만료됩니다.',
'settings.providers.page.quotaCredentials.refreshToken': '새로 고침 토큰',
'settings.providers.page.quotaCredentials.tokenPlaceholder': '토큰 붙여넣기',
'settings.providers.page.openCodeGo.saveFailed': 'OpenCode Go 인증 정보를 검증할 수 없습니다.',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': 'Usuń',
'settings.providers.page.quotaCredentials.saved': 'Dane uwierzytelniające {provider} zostały zapisane.',
'settings.providers.page.quotaCredentials.accessToken': 'Token dostępu',
'settings.providers.page.quotaCredentials.usageToken': 'Token API użycia',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': 'Uruchom to polecenie w terminalu, a następnie wklej token poniżej. Może on tylko odczytywać użycie środków LLM i wygasa po 30 dniach.',
'settings.providers.page.quotaCredentials.refreshToken': 'Token odświeżania',
'settings.providers.page.quotaCredentials.tokenPlaceholder': 'Wklej token',
'settings.providers.page.openCodeGo.saveFailed': 'Nie udało się sprawdzić danych OpenCode Go.',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': 'Excluir',
'settings.providers.page.quotaCredentials.saved': 'Credenciais de {provider} salvas.',
'settings.providers.page.quotaCredentials.accessToken': 'Token de acesso',
'settings.providers.page.quotaCredentials.usageToken': 'Token da API de uso',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': 'Execute este comando no terminal e cole o token abaixo. Ele só pode ler o uso de créditos de LLM e expira após 30 dias.',
'settings.providers.page.quotaCredentials.refreshToken': 'Token de atualização',
'settings.providers.page.quotaCredentials.tokenPlaceholder': 'Cole o token',
'settings.providers.page.openCodeGo.saveFailed': 'Não foi possível validar as credenciais do OpenCode Go.',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': 'Sil',
'settings.providers.page.quotaCredentials.saved': '{provider} kimlik bilgileri kaydedildi.',
'settings.providers.page.quotaCredentials.accessToken': 'Erişim token\'ı',
'settings.providers.page.quotaCredentials.usageToken': 'Kullanım API token\'ı',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': 'Bu komutu terminalde çalıştırın, ardından token\'ı aşağıya yapıştırın. Yalnızca LLM kredi kullanımını okuyabilir ve 30 gün sonra sona erer.',
'settings.providers.page.quotaCredentials.refreshToken': 'Yenileme token\'ı',
'settings.providers.page.quotaCredentials.tokenPlaceholder': 'Token\'ı yapıştır',
'settings.providers.page.openCodeGo.saveFailed': 'OpenCode Go kimlik bilgileri doğrulanamadı.',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': 'Видалити',
'settings.providers.page.quotaCredentials.saved': 'Облікові дані {provider} збережено.',
'settings.providers.page.quotaCredentials.accessToken': 'Токен доступу',
'settings.providers.page.quotaCredentials.usageToken': 'Токен API використання',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': 'Виконайте цю команду в терміналі, а потім вставте токен нижче. Він може лише читати використання LLM-кредитів і діє 30 днів.',
'settings.providers.page.quotaCredentials.refreshToken': 'Токен оновлення',
'settings.providers.page.quotaCredentials.tokenPlaceholder': 'Вставте токен',
'settings.providers.page.openCodeGo.saveFailed': 'Не вдалося перевірити дані OpenCode Go.',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': '删除',
'settings.providers.page.quotaCredentials.saved': '已保存 {provider} 凭据。',
'settings.providers.page.quotaCredentials.accessToken': '访问令牌',
'settings.providers.page.quotaCredentials.usageToken': '用量 API 令牌',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': '在终端中运行此命令,然后在下方粘贴令牌。该令牌只能读取 LLM 积分用量,并将在 30 天后过期。',
'settings.providers.page.quotaCredentials.refreshToken': '刷新令牌',
'settings.providers.page.quotaCredentials.tokenPlaceholder': '粘贴令牌',
'settings.providers.page.openCodeGo.saveFailed': '无法验证 OpenCode Go 凭据。',
@@ -13,6 +13,8 @@ export const settingsDict = {
'settings.providers.page.openCodeGo.delete': '刪除',
'settings.providers.page.quotaCredentials.saved': '已儲存 {provider} 憑證。',
'settings.providers.page.quotaCredentials.accessToken': '存取權杖',
'settings.providers.page.quotaCredentials.usageToken': '用量 API 權杖',
'settings.providers.page.quotaCredentials.exeDevTokenInstructions': '在終端機中執行此命令,然後在下方貼上權杖。該權杖只能讀取 LLM 點數用量,並將在 30 天後到期。',
'settings.providers.page.quotaCredentials.refreshToken': '重新整理權杖',
'settings.providers.page.quotaCredentials.tokenPlaceholder': '貼上權杖',
'settings.providers.page.openCodeGo.saveFailed': '無法驗證 OpenCode Go 憑證。',
@@ -23,6 +23,7 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [
{ id: 'opencode-go', name: 'OpenCode Go' },
{ id: 'crof', name: 'CrofAI' },
{ id: 'deepseek', name: 'DeepSeek' },
{ id: 'exe-dev', name: 'exe.dev' },
{ id: 'neuralwatt', name: 'NeuralWatt' },
{ id: 'xai', name: 'xAI' },
];
+1
View File
@@ -18,6 +18,7 @@ export type QuotaProviderId =
| 'opencode-go'
| 'crof'
| 'deepseek'
| 'exe-dev'
| 'neuralwatt'
| 'xai';