feat(usage): show OpenRouter credit balance in the usage meter - #182

Merged
pandec merged 3 commits into
devfrom
feat/openrouter-credits
Aug 31, 2026
Merged

feat(usage): show OpenRouter credit balance in the usage meter#182
pandec merged 3 commits into
devfrom
feat/openrouter-credits

Conversation

@pandec

@pandecpandec commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The usage meter had no view of OpenRouter spend, even though gateway-routed models bill against that account's prepaid credits.

The server now stores an OpenRouter management key per environment (secret store, never echoed to clients) and reads GET /api/v1/credits behind a process-wide 60-second cache with single-flight and a 10-second failure cache, matching OpenRouter's own cache window. The endpoint rejects regular inference keys, so the settings copy and the 401/403 error both point at openrouter.ai/settings/management-keys. Two new WS RPCs carry it: openRouterCredits.read (read scope) and openRouterCredits.configure (operate scope). A failed read keeps the last snapshot and reports the reason instead of blanking the number; an unreadable secret store reports that, not "no key".

On web/desktop, Settings → Extras → Provider usage gains an opt-in switch (showOpenRouterCredits, default off) plus a key field that applies to every connected environment and lists each one's stored-key state and current balance. With the switch on, the composer usage popover shows an "OpenRouter credits" row with the remaining balance, refreshed on open at most once a minute; an unconfigured environment points at the settings page, and a server predating the RPC renders nothing.

Two review rounds ran (Fable write-capable, then Sol read-only). Fixed from them: request timeout now covers the response body read (a hung body would have wedged the single-flight gate for the process), configure refreshes the read query so settings rows update immediately, failure caching, payload single-flight keys, non-finite payload rejection, open-time clock reads for the popover refresh gate, and honest error reporting in the settings status rows. Deliberately not done: the meter button still only appears when the thread has context-window or provider-usage data (a credits-only meter would render an empty ring), and mobile is not covered.

Focused tests: apps/server/src/provider/openRouterCredits.test.ts (11 cases: key storage, caching, failure cache, stale retention, hung-body timeout, auth rejection, unreadable store, non-finite payloads). vp check and vp run typecheck pass.

Implemented by Claude Fable 5 in Claude Code, with review rounds by Claude Fable 5 and gpt-5.6-sol.

- failure cache stops queued readers behind the single-flight gate from
serially re-spending doomed requests during an outage
- secret-store read failures report as an error instead of "no key"
- non-finite credit totals are rejected at the schema
- configure single-flight keys include the payload so a save can't join a
racing remove and claim success
- popover refresh gate reads the clock at open time, not render time
- interrupted per-environment saves count as failures in the settings toast
- stale balances in settings say the last read failed
- copy: the credits endpoint needs an OpenRouter management key
@pandec
pandec merged commit e0758f0 into devAug 31, 2026
4 checks passed
@pandec
pandec deleted the feat/openrouter-credits branch August 31, 2026 05:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(usage): show OpenRouter credit balance in the usage meter - #182

Merged
pandec merged 3 commits into
devfrom
feat/openrouter-credits
Aug 31, 2026
Merged

feat(usage): show OpenRouter credit balance in the usage meter#182
pandec merged 3 commits into
devfrom
feat/openrouter-credits

Conversation

@pandec

@pandecpandec commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The usage meter had no view of OpenRouter spend, even though gateway-routed models bill against that account's prepaid credits.

The server now stores an OpenRouter management key per environment (secret store, never echoed to clients) and reads GET /api/v1/credits behind a process-wide 60-second cache with single-flight and a 10-second failure cache, matching OpenRouter's own cache window. The endpoint rejects regular inference keys, so the settings copy and the 401/403 error both point at openrouter.ai/settings/management-keys. Two new WS RPCs carry it: openRouterCredits.read (read scope) and openRouterCredits.configure (operate scope). A failed read keeps the last snapshot and reports the reason instead of blanking the number; an unreadable secret store reports that, not "no key".

On web/desktop, Settings → Extras → Provider usage gains an opt-in switch (showOpenRouterCredits, default off) plus a key field that applies to every connected environment and lists each one's stored-key state and current balance. With the switch on, the composer usage popover shows an "OpenRouter credits" row with the remaining balance, refreshed on open at most once a minute; an unconfigured environment points at the settings page, and a server predating the RPC renders nothing.

Two review rounds ran (Fable write-capable, then Sol read-only). Fixed from them: request timeout now covers the response body read (a hung body would have wedged the single-flight gate for the process), configure refreshes the read query so settings rows update immediately, failure caching, payload single-flight keys, non-finite payload rejection, open-time clock reads for the popover refresh gate, and honest error reporting in the settings status rows. Deliberately not done: the meter button still only appears when the thread has context-window or provider-usage data (a credits-only meter would render an empty ring), and mobile is not covered.

Focused tests: apps/server/src/provider/openRouterCredits.test.ts (11 cases: key storage, caching, failure cache, stale retention, hung-body timeout, auth rejection, unreadable store, non-finite payloads). vp check and vp run typecheck pass.

Implemented by Claude Fable 5 in Claude Code, with review rounds by Claude Fable 5 and gpt-5.6-sol.

- failure cache stops queued readers behind the single-flight gate from
serially re-spending doomed requests during an outage
- secret-store read failures report as an error instead of "no key"
- non-finite credit totals are rejected at the schema
- configure single-flight keys include the payload so a save can't join a
racing remove and claim success
- popover refresh gate reads the clock at open time, not render time
- interrupted per-environment saves count as failures in the settings toast
- stale balances in settings say the last read failed
- copy: the credits endpoint needs an OpenRouter management key
@pandec
pandec merged commit e0758f0 into devAug 31, 2026
4 checks passed
@pandec
pandec deleted the feat/openrouter-credits branch August 31, 2026 05:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(usage): show OpenRouter credit balance in the usage meter - #182

Merged
pandec merged 3 commits into
devfrom
feat/openrouter-credits
Aug 31, 2026
Merged

feat(usage): show OpenRouter credit balance in the usage meter#182
pandec merged 3 commits into
devfrom
feat/openrouter-credits

Conversation

@pandec

@pandecpandec commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The usage meter had no view of OpenRouter spend, even though gateway-routed models bill against that account's prepaid credits.

The server now stores an OpenRouter management key per environment (secret store, never echoed to clients) and reads GET /api/v1/credits behind a process-wide 60-second cache with single-flight and a 10-second failure cache, matching OpenRouter's own cache window. The endpoint rejects regular inference keys, so the settings copy and the 401/403 error both point at openrouter.ai/settings/management-keys. Two new WS RPCs carry it: openRouterCredits.read (read scope) and openRouterCredits.configure (operate scope). A failed read keeps the last snapshot and reports the reason instead of blanking the number; an unreadable secret store reports that, not "no key".

On web/desktop, Settings → Extras → Provider usage gains an opt-in switch (showOpenRouterCredits, default off) plus a key field that applies to every connected environment and lists each one's stored-key state and current balance. With the switch on, the composer usage popover shows an "OpenRouter credits" row with the remaining balance, refreshed on open at most once a minute; an unconfigured environment points at the settings page, and a server predating the RPC renders nothing.

Two review rounds ran (Fable write-capable, then Sol read-only). Fixed from them: request timeout now covers the response body read (a hung body would have wedged the single-flight gate for the process), configure refreshes the read query so settings rows update immediately, failure caching, payload single-flight keys, non-finite payload rejection, open-time clock reads for the popover refresh gate, and honest error reporting in the settings status rows. Deliberately not done: the meter button still only appears when the thread has context-window or provider-usage data (a credits-only meter would render an empty ring), and mobile is not covered.

Focused tests: apps/server/src/provider/openRouterCredits.test.ts (11 cases: key storage, caching, failure cache, stale retention, hung-body timeout, auth rejection, unreadable store, non-finite payloads). vp check and vp run typecheck pass.

Implemented by Claude Fable 5 in Claude Code, with review rounds by Claude Fable 5 and gpt-5.6-sol.

- failure cache stops queued readers behind the single-flight gate from
serially re-spending doomed requests during an outage
- secret-store read failures report as an error instead of "no key"
- non-finite credit totals are rejected at the schema
- configure single-flight keys include the payload so a save can't join a
racing remove and claim success
- popover refresh gate reads the clock at open time, not render time
- interrupted per-environment saves count as failures in the settings toast
- stale balances in settings say the last read failed
- copy: the credits endpoint needs an OpenRouter management key
@pandec
pandec merged commit e0758f0 into devAug 31, 2026
4 checks passed
@pandec
pandec deleted the feat/openrouter-credits branch August 31, 2026 05:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(usage): show OpenRouter credit balance in the usage meter - #182

Merged
pandec merged 3 commits into
devfrom
feat/openrouter-credits
Aug 31, 2026
Merged

feat(usage): show OpenRouter credit balance in the usage meter#182
pandec merged 3 commits into
devfrom
feat/openrouter-credits

Conversation

@pandec

@pandecpandec commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The usage meter had no view of OpenRouter spend, even though gateway-routed models bill against that account's prepaid credits.

The server now stores an OpenRouter management key per environment (secret store, never echoed to clients) and reads GET /api/v1/credits behind a process-wide 60-second cache with single-flight and a 10-second failure cache, matching OpenRouter's own cache window. The endpoint rejects regular inference keys, so the settings copy and the 401/403 error both point at openrouter.ai/settings/management-keys. Two new WS RPCs carry it: openRouterCredits.read (read scope) and openRouterCredits.configure (operate scope). A failed read keeps the last snapshot and reports the reason instead of blanking the number; an unreadable secret store reports that, not "no key".

On web/desktop, Settings → Extras → Provider usage gains an opt-in switch (showOpenRouterCredits, default off) plus a key field that applies to every connected environment and lists each one's stored-key state and current balance. With the switch on, the composer usage popover shows an "OpenRouter credits" row with the remaining balance, refreshed on open at most once a minute; an unconfigured environment points at the settings page, and a server predating the RPC renders nothing.

Two review rounds ran (Fable write-capable, then Sol read-only). Fixed from them: request timeout now covers the response body read (a hung body would have wedged the single-flight gate for the process), configure refreshes the read query so settings rows update immediately, failure caching, payload single-flight keys, non-finite payload rejection, open-time clock reads for the popover refresh gate, and honest error reporting in the settings status rows. Deliberately not done: the meter button still only appears when the thread has context-window or provider-usage data (a credits-only meter would render an empty ring), and mobile is not covered.

Focused tests: apps/server/src/provider/openRouterCredits.test.ts (11 cases: key storage, caching, failure cache, stale retention, hung-body timeout, auth rejection, unreadable store, non-finite payloads). vp check and vp run typecheck pass.

Implemented by Claude Fable 5 in Claude Code, with review rounds by Claude Fable 5 and gpt-5.6-sol.

- failure cache stops queued readers behind the single-flight gate from
serially re-spending doomed requests during an outage
- secret-store read failures report as an error instead of "no key"
- non-finite credit totals are rejected at the schema
- configure single-flight keys include the payload so a save can't join a
racing remove and claim success
- popover refresh gate reads the clock at open time, not render time
- interrupted per-environment saves count as failures in the settings toast
- stale balances in settings say the last read failed
- copy: the credits endpoint needs an OpenRouter management key
@pandec
pandec merged commit e0758f0 into devAug 31, 2026
4 checks passed
@pandec
pandec deleted the feat/openrouter-credits branch August 31, 2026 05:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(usage): show OpenRouter credit balance in the usage meter - #182

Merged
pandec merged 3 commits into
devfrom
feat/openrouter-credits
Aug 31, 2026
Merged

feat(usage): show OpenRouter credit balance in the usage meter#182
pandec merged 3 commits into
devfrom
feat/openrouter-credits

Conversation

@pandec

@pandecpandec commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The usage meter had no view of OpenRouter spend, even though gateway-routed models bill against that account's prepaid credits.

The server now stores an OpenRouter management key per environment (secret store, never echoed to clients) and reads GET /api/v1/credits behind a process-wide 60-second cache with single-flight and a 10-second failure cache, matching OpenRouter's own cache window. The endpoint rejects regular inference keys, so the settings copy and the 401/403 error both point at openrouter.ai/settings/management-keys. Two new WS RPCs carry it: openRouterCredits.read (read scope) and openRouterCredits.configure (operate scope). A failed read keeps the last snapshot and reports the reason instead of blanking the number; an unreadable secret store reports that, not "no key".

On web/desktop, Settings → Extras → Provider usage gains an opt-in switch (showOpenRouterCredits, default off) plus a key field that applies to every connected environment and lists each one's stored-key state and current balance. With the switch on, the composer usage popover shows an "OpenRouter credits" row with the remaining balance, refreshed on open at most once a minute; an unconfigured environment points at the settings page, and a server predating the RPC renders nothing.

Two review rounds ran (Fable write-capable, then Sol read-only). Fixed from them: request timeout now covers the response body read (a hung body would have wedged the single-flight gate for the process), configure refreshes the read query so settings rows update immediately, failure caching, payload single-flight keys, non-finite payload rejection, open-time clock reads for the popover refresh gate, and honest error reporting in the settings status rows. Deliberately not done: the meter button still only appears when the thread has context-window or provider-usage data (a credits-only meter would render an empty ring), and mobile is not covered.

Focused tests: apps/server/src/provider/openRouterCredits.test.ts (11 cases: key storage, caching, failure cache, stale retention, hung-body timeout, auth rejection, unreadable store, non-finite payloads). vp check and vp run typecheck pass.

Implemented by Claude Fable 5 in Claude Code, with review rounds by Claude Fable 5 and gpt-5.6-sol.

- failure cache stops queued readers behind the single-flight gate from
serially re-spending doomed requests during an outage
- secret-store read failures report as an error instead of "no key"
- non-finite credit totals are rejected at the schema
- configure single-flight keys include the payload so a save can't join a
racing remove and claim success
- popover refresh gate reads the clock at open time, not render time
- interrupted per-environment saves count as failures in the settings toast
- stale balances in settings say the last read failed
- copy: the credits endpoint needs an OpenRouter management key
@pandec
pandec merged commit e0758f0 into devAug 31, 2026
4 checks passed
@pandec
pandec deleted the feat/openrouter-credits branch August 31, 2026 05:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(usage): show OpenRouter credit balance in the usage meter - #182

Merged
pandec merged 3 commits into
devfrom
feat/openrouter-credits
Aug 31, 2026
Merged

feat(usage): show OpenRouter credit balance in the usage meter#182
pandec merged 3 commits into
devfrom
feat/openrouter-credits

Conversation

@pandec

@pandecpandec commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The usage meter had no view of OpenRouter spend, even though gateway-routed models bill against that account's prepaid credits.

The server now stores an OpenRouter management key per environment (secret store, never echoed to clients) and reads GET /api/v1/credits behind a process-wide 60-second cache with single-flight and a 10-second failure cache, matching OpenRouter's own cache window. The endpoint rejects regular inference keys, so the settings copy and the 401/403 error both point at openrouter.ai/settings/management-keys. Two new WS RPCs carry it: openRouterCredits.read (read scope) and openRouterCredits.configure (operate scope). A failed read keeps the last snapshot and reports the reason instead of blanking the number; an unreadable secret store reports that, not "no key".

On web/desktop, Settings → Extras → Provider usage gains an opt-in switch (showOpenRouterCredits, default off) plus a key field that applies to every connected environment and lists each one's stored-key state and current balance. With the switch on, the composer usage popover shows an "OpenRouter credits" row with the remaining balance, refreshed on open at most once a minute; an unconfigured environment points at the settings page, and a server predating the RPC renders nothing.

Two review rounds ran (Fable write-capable, then Sol read-only). Fixed from them: request timeout now covers the response body read (a hung body would have wedged the single-flight gate for the process), configure refreshes the read query so settings rows update immediately, failure caching, payload single-flight keys, non-finite payload rejection, open-time clock reads for the popover refresh gate, and honest error reporting in the settings status rows. Deliberately not done: the meter button still only appears when the thread has context-window or provider-usage data (a credits-only meter would render an empty ring), and mobile is not covered.

Focused tests: apps/server/src/provider/openRouterCredits.test.ts (11 cases: key storage, caching, failure cache, stale retention, hung-body timeout, auth rejection, unreadable store, non-finite payloads). vp check and vp run typecheck pass.

Implemented by Claude Fable 5 in Claude Code, with review rounds by Claude Fable 5 and gpt-5.6-sol.

- failure cache stops queued readers behind the single-flight gate from
serially re-spending doomed requests during an outage
- secret-store read failures report as an error instead of "no key"
- non-finite credit totals are rejected at the schema
- configure single-flight keys include the payload so a save can't join a
racing remove and claim success
- popover refresh gate reads the clock at open time, not render time
- interrupted per-environment saves count as failures in the settings toast
- stale balances in settings say the last read failed
- copy: the credits endpoint needs an OpenRouter management key
@pandec
pandec merged commit e0758f0 into devAug 31, 2026
4 checks passed
@pandec
pandec deleted the feat/openrouter-credits branch August 31, 2026 05:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(usage): show OpenRouter credit balance in the usage meter - #182

Merged
pandec merged 3 commits into
devfrom
feat/openrouter-credits
Aug 31, 2026
Merged

feat(usage): show OpenRouter credit balance in the usage meter#182
pandec merged 3 commits into
devfrom
feat/openrouter-credits

Conversation

@pandec

@pandecpandec commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The usage meter had no view of OpenRouter spend, even though gateway-routed models bill against that account's prepaid credits.

The server now stores an OpenRouter management key per environment (secret store, never echoed to clients) and reads GET /api/v1/credits behind a process-wide 60-second cache with single-flight and a 10-second failure cache, matching OpenRouter's own cache window. The endpoint rejects regular inference keys, so the settings copy and the 401/403 error both point at openrouter.ai/settings/management-keys. Two new WS RPCs carry it: openRouterCredits.read (read scope) and openRouterCredits.configure (operate scope). A failed read keeps the last snapshot and reports the reason instead of blanking the number; an unreadable secret store reports that, not "no key".

On web/desktop, Settings → Extras → Provider usage gains an opt-in switch (showOpenRouterCredits, default off) plus a key field that applies to every connected environment and lists each one's stored-key state and current balance. With the switch on, the composer usage popover shows an "OpenRouter credits" row with the remaining balance, refreshed on open at most once a minute; an unconfigured environment points at the settings page, and a server predating the RPC renders nothing.

Two review rounds ran (Fable write-capable, then Sol read-only). Fixed from them: request timeout now covers the response body read (a hung body would have wedged the single-flight gate for the process), configure refreshes the read query so settings rows update immediately, failure caching, payload single-flight keys, non-finite payload rejection, open-time clock reads for the popover refresh gate, and honest error reporting in the settings status rows. Deliberately not done: the meter button still only appears when the thread has context-window or provider-usage data (a credits-only meter would render an empty ring), and mobile is not covered.

Focused tests: apps/server/src/provider/openRouterCredits.test.ts (11 cases: key storage, caching, failure cache, stale retention, hung-body timeout, auth rejection, unreadable store, non-finite payloads). vp check and vp run typecheck pass.

Implemented by Claude Fable 5 in Claude Code, with review rounds by Claude Fable 5 and gpt-5.6-sol.

- failure cache stops queued readers behind the single-flight gate from
serially re-spending doomed requests during an outage
- secret-store read failures report as an error instead of "no key"
- non-finite credit totals are rejected at the schema
- configure single-flight keys include the payload so a save can't join a
racing remove and claim success
- popover refresh gate reads the clock at open time, not render time
- interrupted per-environment saves count as failures in the settings toast
- stale balances in settings say the last read failed
- copy: the credits endpoint needs an OpenRouter management key
@pandec
pandec merged commit e0758f0 into devAug 31, 2026
4 checks passed
@pandec
pandec deleted the feat/openrouter-credits branch August 31, 2026 05:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(usage): show OpenRouter credit balance in the usage meter - #182

Merged
pandec merged 3 commits into
devfrom
feat/openrouter-credits
Aug 31, 2026
Merged

feat(usage): show OpenRouter credit balance in the usage meter#182
pandec merged 3 commits into
devfrom
feat/openrouter-credits

Conversation

@pandec

@pandecpandec commented Aug 31, 2026

Copy link
Copy Markdown
Owner

The usage meter had no view of OpenRouter spend, even though gateway-routed models bill against that account's prepaid credits.

The server now stores an OpenRouter management key per environment (secret store, never echoed to clients) and reads GET /api/v1/credits behind a process-wide 60-second cache with single-flight and a 10-second failure cache, matching OpenRouter's own cache window. The endpoint rejects regular inference keys, so the settings copy and the 401/403 error both point at openrouter.ai/settings/management-keys. Two new WS RPCs carry it: openRouterCredits.read (read scope) and openRouterCredits.configure (operate scope). A failed read keeps the last snapshot and reports the reason instead of blanking the number; an unreadable secret store reports that, not "no key".

On web/desktop, Settings → Extras → Provider usage gains an opt-in switch (showOpenRouterCredits, default off) plus a key field that applies to every connected environment and lists each one's stored-key state and current balance. With the switch on, the composer usage popover shows an "OpenRouter credits" row with the remaining balance, refreshed on open at most once a minute; an unconfigured environment points at the settings page, and a server predating the RPC renders nothing.

Two review rounds ran (Fable write-capable, then Sol read-only). Fixed from them: request timeout now covers the response body read (a hung body would have wedged the single-flight gate for the process), configure refreshes the read query so settings rows update immediately, failure caching, payload single-flight keys, non-finite payload rejection, open-time clock reads for the popover refresh gate, and honest error reporting in the settings status rows. Deliberately not done: the meter button still only appears when the thread has context-window or provider-usage data (a credits-only meter would render an empty ring), and mobile is not covered.

Focused tests: apps/server/src/provider/openRouterCredits.test.ts (11 cases: key storage, caching, failure cache, stale retention, hung-body timeout, auth rejection, unreadable store, non-finite payloads). vp check and vp run typecheck pass.

Implemented by Claude Fable 5 in Claude Code, with review rounds by Claude Fable 5 and gpt-5.6-sol.

- failure cache stops queued readers behind the single-flight gate from
serially re-spending doomed requests during an outage
- secret-store read failures report as an error instead of "no key"
- non-finite credit totals are rejected at the schema
- configure single-flight keys include the payload so a save can't join a
racing remove and claim success
- popover refresh gate reads the clock at open time, not render time
- interrupted per-environment saves count as failures in the settings toast
- stale balances in settings say the last read failed
- copy: the credits endpoint needs an OpenRouter management key
@pandec
pandec merged commit e0758f0 into devAug 31, 2026
4 checks passed
@pandec
pandec deleted the feat/openrouter-credits branch August 31, 2026 05:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@pandec