fix: don't bill self-hosted models reached by a container/service hostname #596

Merged
nsgds merged 1 commit from nsgds/fix/local-endpoint-billing into main 2026-06-01 20:08:18 +02:00
nsgds commented 2026-06-01 17:31:16 +02:00 (Migrated from github.com)
after before ## What

isLocalEndpoint() (static/js/chatRenderer.js) decides whether a model is local — and therefore free. It recognizes IPs, localhost, and .local, but not a bare single-label hostname like a Docker/Compose service name (nim-nano, llamaswap, …). Because getModelCost() matches the pricing table on a name substring, a self-hosted model whose id contains e.g. nemotron or llama then gets billed at cloud rates — the cost badge and Message Stats show a phantom non-zero cost.

Fix

Treat any hostname with no dot as local. A public API always needs an FQDN, so a single-label host can only be internal:

if (!host.includes('.')) return true;

IPs (which contain dots) still fall through to the existing private-range checks, and real cloud FQDNs are unaffected.

Files

  • static/js/chatRenderer.js — one guard in isLocalEndpoint() (+6 lines).

Testing

  • Syntax: validated as an ES module (node --check on a .mjs copy — the file is browser ESM, so the literal node --check *.js misreads the import as CommonJS).
  • Manual: served a local model via a Compose service name (http://nim-nano:8000) whose id contains nemotron.

Before — local model billed at cloud rates:

After — correctly free ($0):

Complements #518 (cloud spend billing) — it reads the same cost path, so without this guard self-hosted models would also inflate its new spend graphs/budgets.

<img width="949" height="515" alt="after" src="https://github.com/user-attachments/assets/ab993bc2-51b3-44cc-831a-b37ffe3e4015" /> <img width="934" height="507" alt="before" src="https://github.com/user-attachments/assets/af748722-588e-44af-a24b-12fa59ddf221" /> ## What `isLocalEndpoint()` (`static/js/chatRenderer.js`) decides whether a model is local — and therefore free. It recognizes IPs, `localhost`, and `.local`, but **not a bare single-label hostname** like a Docker/Compose service name (`nim-nano`, `llamaswap`, …). Because `getModelCost()` matches the pricing table on a name **substring**, a self-hosted model whose id contains e.g. `nemotron` or `llama` then gets billed at cloud rates — the cost badge and Message Stats show a phantom non-zero cost. ## Fix Treat any hostname with no dot as local. A public API always needs an FQDN, so a single-label host can only be internal: ```js if (!host.includes('.')) return true; ``` IPs (which contain dots) still fall through to the existing private-range checks, and real cloud FQDNs are unaffected. ## Files - `static/js/chatRenderer.js` — one guard in `isLocalEndpoint()` (+6 lines). ## Testing - **Syntax:** validated as an ES module (`node --check` on a `.mjs` copy — the file is browser ESM, so the literal `node --check *.js` misreads the `import` as CommonJS). - **Manual:** served a local model via a Compose service name (`http://nim-nano:8000`) whose id contains `nemotron`. **Before** — local model billed at cloud rates: <!-- ⬇⬇ drag-drop your "before" screenshot on the line below ⬇⬇ --> **After** — correctly free ($0): <!-- ⬇⬇ drag-drop your "after" screenshot on the line below ⬇⬇ --> ## Related Complements #518 (cloud spend billing) — it reads the same cost path, so without this guard self-hosted models would also inflate its new spend graphs/budgets.
sleepy merged commit 7c60569944 into main 2026-06-01 20:08:18 +02:00
Owner

Merged via squash. Minimal 6-line fix. isLocalEndpoint() now treats dotless hostnames (Docker/K8s service names) as local, consistent with existing admin.js implementation.

Merged via squash. Minimal 6-line fix. `isLocalEndpoint()` now treats dotless hostnames (Docker/K8s service names) as local, consistent with existing `admin.js` implementation.
Sign in to join this conversation.
No description provided.