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 | <small data-i18n="Click Authorize below or get the key from"> | 2446 | <small data-i18n="Click Authorize below or get the key from"> |
| 2447 | Click "Authorize" below or get the key from </small> <a target="_blank" href="https://openrouter.ai/keys/">OpenRouter</a>. | 2447 | Click "Authorize" below or get the key from </small> <a target="_blank" href="https://openrouter.ai/keys/">OpenRouter</a>. |
| 2448 | <br> | 2448 | <br> |
| 2449 | <a href="https://openrouter.ai/account" target="_blank" data-i18n="View Remaining Credits">View Remaining Credits</a> | 2449 | <a href="https://openrouter.ai/settings/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 | </div> | 2451 | </div> |
| 2451 | <div class="flex-container"> | 2452 | <div class="flex-container"> |
| 2452 | <input id="api_key_openrouter-tg" name="api_key_openrouter" class="text_pole flex1 api_key_openrouter" value="" type="text" autocomplete="off"> | 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 | <small data-i18n="Click Authorize below or get the key from"> | 3180 | <small data-i18n="Click Authorize below or get the key from"> |
| 3180 | Click "Authorize" below or get the key from </small> <a target="_blank" href="https://openrouter.ai/keys/">OpenRouter</a>. | 3181 | Click "Authorize" below or get the key from </small> <a target="_blank" href="https://openrouter.ai/keys/">OpenRouter</a>. |
| 3181 | <br> | 3182 | <br> |
| 3182 | <a href="https://openrouter.ai/account" target="_blank" data-i18n="View Remaining Credits">View Remaining Credits</a> | 3183 | <a href="https://openrouter.ai/settings/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 | </div> | 3185 | </div> |
| 3184 | <div class="flex-container"> | 3186 | <div class="flex-container"> |
| 3185 | <input id="api_key_openrouter" name="api_key_openrouter" class="text_pole flex1 api_key_openrouter" value="" type="text" autocomplete="off"> | 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 | warningElement.toggle(value.length > 0); | 1120 | warningElement.toggle(value.length > 0); |
| 1121 | }); | 1121 | }); |
| 1122 | $('.openrouter_authorize').on('click', authorizeOpenRouter); | 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 | registerSecretSlashCommands(); | 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 | router.post('/image/generate', async (req, res) => { | 138 | router.post('/image/generate', async (req, res) => { |
| 104 | try { | 139 | try { |
| 105 | const key = readSecret(req.user.directories, SECRET_KEYS.OPENROUTER); | 140 | const key = readSecret(req.user.directories, SECRET_KEYS.OPENROUTER); |