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>

a028bec87b53f617ff7aa3652ad0473bcb0d1529

Stagnating <stagnantarchives@gmail.com>

Signed
3 files changed, +62 -2Ignore whitespace
public/index.html+4 -2
@@ -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">
public/scripts/secrets.js+23 -0
@@ -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}
src/endpoints/openrouter.js+35 -0
@@ -100,6 +100,41 @@ router.post('/models/image', async (_req, res) => {
100 }100 }
101});101});
102102
103router.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
103router.post('/image/generate', async (req, res) => {138router.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);