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

* fix(cost): treat dotless container hostnames as local (free)

getModelCost() substring-matches model names against a cloud price table, so a self-hosted 'nemotron'/'llama' model was billed at cloud rates. isLocalEndpoint() only recognized IPs / localhost / .local, not bare Docker service names (nim-nano, llamaswap), so the local-is-free guard missed them. A single-label hostname (no dot) can never be a public API -> treat as local.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cost): isLocalEndpoint classifies service names local, cloud FQDNs billable

Covers @pewdiepie-archdaemon's requested cases: llamaswap/nim-nano + localhost/private-IPs/.local => local (free); api.openai.com/openrouter.ai/etc => not local. Drives the real function via node --input-type=module (same approach as test_reply_recipients_js.py), skips when node is absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
nsgds
2026-06-02 10:47:58 +08:00
committed by GitHub
parent 649cacfa05
commit a857d2016d
2 changed files with 69 additions and 0 deletions

View File

@@ -659,6 +659,12 @@ export function isLocalEndpoint(url) {
if (!host) return true;
if (host === 'localhost' || host === '0.0.0.0' || host === 'host.docker.internal' || host.endsWith('.local')) return true;
if (typeof window !== 'undefined' && window.location && host === window.location.hostname) return true;
// A single-label hostname (no dot) is an internal/Docker service name
// (e.g. "nim-nano", "llamaswap", "nemotron-super-49b") or a LAN shortname —
// never a public API, which always needs an FQDN. Treat as local → free.
// (Without this, container-name endpoints get billed at cloud rates because
// the pricing table matches on a name substring, e.g. "nemotron".)
if (!host.includes('.')) return true;
if (/^127\./.test(host)) return true;
if (/^10\./.test(host)) return true;
if (/^192\.168\./.test(host)) return true;

View File

@@ -0,0 +1,63 @@
"""Pin the billing/display classifier `isLocalEndpoint` in chatRenderer.js.
Self-hosted endpoints reached by a bare Docker/Compose service name (e.g.
`http://llamaswap:8000`) must classify as LOCAL so they aren't priced at cloud
rates against the substring-matched MODEL_PRICING table. Cloud FQDNs must stay
billable.
Driven through `node --input-type=module` against the real function (extracted
from source — chatRenderer.js can't be imported standalone since it pulls in
browser-only modules), same spirit as test_reply_recipients_js.py. Skips when
`node` is not installed rather than failing.
"""
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
_REPO = Path(__file__).resolve().parent.parent
_SRC = _REPO / "static" / "js" / "chatRenderer.js"
_HAS_NODE = shutil.which("node") is not None
def _is_local(url: str) -> bool:
src = _SRC.read_text(encoding="utf-8")
m = re.search(r"export function isLocalEndpoint\(.*?\n\}", src, re.DOTALL)
assert m, "isLocalEndpoint not found in chatRenderer.js"
fn = m.group(0).replace("export function", "function", 1)
js = fn + f"\nconsole.log(JSON.stringify(isLocalEndpoint({json.dumps(url)})));"
proc = subprocess.run(
["node", "--input-type=module"],
input=js, capture_output=True, text=True, cwd=str(_REPO), timeout=30,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout.strip())
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
@pytest.mark.parametrize("url", [
"http://llamaswap:8000", # bare Docker/Compose service name
"http://nim-nano:8000/v1",
"http://localhost:7000",
"http://127.0.0.1:11434",
"http://192.168.50.244", # private ranges
"http://10.0.0.5:8080",
"http://172.16.0.9",
"http://server.local", # mDNS / .local
])
def test_self_hosted_endpoints_classify_local(url):
assert _is_local(url) is True, f"{url} should be treated as local (free)"
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
@pytest.mark.parametrize("url", [
"https://api.openai.com/v1",
"https://openrouter.ai/api/v1",
"https://api.anthropic.com",
"https://generativelanguage.googleapis.com",
])
def test_cloud_endpoints_classify_billable(url):
assert _is_local(url) is False, f"{url} should NOT be treated as local"