Display OpenRouter credit balance in UI (#5513) * Display OpenRouter credit balance in UI Adds a "View Remaining Credits" click handler that fetches the current balance from the OpenRouter /credits endpoint via a new server-side /api/openrouter/credits route, and renders it next to the link. The anchor still points at openrouter.ai/account so middle-click / right-click "open in new tab" keeps working. * Return 500 on OpenRouter credits failure * Reduce to two decimals * Update view credits URL --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -2446,7 +2446,8 @@ | ||
| 2446 | 2446 | <small data-i18n="Click Authorize below or get the key from"> |
| 2447 | 2447 | Click "Authorize" below or get the key from </small> <a target="_blank" href="https://openrouter.ai/keys/">OpenRouter</a>. |
| 2448 | 2448 | <br> |
| 2449 | 2449 | <a href="https://openrouter.ai/accountsettings/credits" target="_blank" class="openrouter_view_credits" data-i18n="View Remaining Credits">View Remaining Credits</a> |
| 2450 | + <span class="openrouter_credits_display marginLeft5"></span> | |
| 2450 | 2451 | </div> |
| 2451 | 2452 | <div class="flex-container"> |
| 2452 | 2453 | <input id="api_key_openrouter-tg" name="api_key_openrouter" class="text_pole flex1 api_key_openrouter" value="" type="text" autocomplete="off"> |
| @@ -3179,7 +3180,8 @@ | ||
| 3179 | 3180 | <small data-i18n="Click Authorize below or get the key from"> |
| 3180 | 3181 | Click "Authorize" below or get the key from </small> <a target="_blank" href="https://openrouter.ai/keys/">OpenRouter</a>. |
| 3181 | 3182 | <br> |
| 3182 | 3183 | <a href="https://openrouter.ai/accountsettings/credits" target="_blank" class="openrouter_view_credits" data-i18n="View Remaining Credits">View Remaining Credits</a> |
| 3184 | + <span class="openrouter_credits_display marginLeft5"></span> | |
| 3183 | 3185 | </div> |
| 3184 | 3186 | <div class="flex-container"> |
| 3185 | 3187 | <input id="api_key_openrouter" name="api_key_openrouter" class="text_pole flex1 api_key_openrouter" value="" type="text" autocomplete="off"> |
| @@ -1120,5 +1120,28 @@ export async function initSecrets() { | ||
| 1120 | 1120 | warningElement.toggle(value.length > 0); |
| 1121 | 1121 | }); |
| 1122 | 1122 | $('.openrouter_authorize').on('click', authorizeOpenRouter); |
| 1123 | + $(document).on('click', '.openrouter_view_credits', async function (event) { | |
| 1124 | + event.preventDefault(); | |
| 1125 | + const display = $(this).siblings('.openrouter_credits_display').first(); | |
| 1126 | + display.text(t`Loading…`); | |
| 1127 | + try { | |
| 1128 | + const response = await fetch('/api/openrouter/credits', { | |
| 1129 | + method: 'POST', | |
| 1130 | + headers: getRequestHeaders(), | |
| 1131 | + }); | |
| 1132 | + if (!response.ok) { | |
| 1133 | + throw new Error(`HTTP ${response.status}`); | |
| 1134 | + } | |
| 1135 | + const data = await response.json(); | |
| 1136 | + if (typeof data.remaining !== 'number') { | |
| 1137 | + throw new Error('Invalid response'); | |
| 1138 | + } | |
| 1139 | + display.text(`$${data.remaining.toFixed(2)}`); | |
| 1140 | + } catch (error) { | |
| 1141 | + console.error('Failed to fetch OpenRouter credits:', error); | |
| 1142 | + display.text(''); | |
| 1143 | + toastr.error(t`Could not fetch OpenRouter credits. Please try again.`); | |
| 1144 | + } | |
| 1145 | + }); | |
| 1123 | 1146 | registerSecretSlashCommands(); |
| 1124 | 1147 | } |
| @@ -100,6 +100,41 @@ router.post('/models/image', async (_req, res) => { | ||
| 100 | 100 | } |
| 101 | 101 | }); |
| 102 | 102 | |
| 103 | +router.post('/credits', async (req, res) => { | |
| 104 | + try { | |
| 105 | + const key = readSecret(req.user.directories, SECRET_KEYS.OPENROUTER); | |
| 106 | + | |
| 107 | + if (!key) { | |
| 108 | + console.warn('OpenRouter API key not found'); | |
| 109 | + return res.sendStatus(400); | |
| 110 | + } | |
| 111 | + | |
| 112 | + const response = await fetch(`${API_OPENROUTER}/credits`, { | |
| 113 | + method: 'GET', | |
| 114 | + headers: { | |
| 115 | + 'Accept': 'application/json', | |
| 116 | + 'Authorization': `Bearer ${key}`, | |
| 117 | + }, | |
| 118 | + }); | |
| 119 | + | |
| 120 | + if (!response.ok) { | |
| 121 | + console.warn('OpenRouter credits request failed', response.statusText); | |
| 122 | + return res.sendStatus(500); | |
| 123 | + } | |
| 124 | + | |
| 125 | + /** @type {any} */ | |
| 126 | + const data = await response.json(); | |
| 127 | + const totalCredits = data.data?.total_credits ?? 0; | |
| 128 | + const totalUsage = data.data?.total_usage ?? 0; | |
| 129 | + const remaining = totalCredits - totalUsage; | |
| 130 | + | |
| 131 | + return res.json({ remaining, total_credits: totalCredits, total_usage: totalUsage }); | |
| 132 | + } catch (error) { | |
| 133 | + console.error(error); | |
| 134 | + return res.sendStatus(500); | |
| 135 | + } | |
| 136 | +}); | |
| 137 | + | |
| 103 | 138 | router.post('/image/generate', async (req, res) => { |
| 104 | 139 | try { |
| 105 | 140 | const key = readSecret(req.user.directories, SECRET_KEYS.OPENROUTER); |