Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ GOOGLE_AISTUDIO_API_KEY=
CEREBRAS_API_KEY=
OLLAMA_API_KEY=
OPENROUTER_API_KEY=
# NVIDIA NIM (build.nvidia.com). A personal account comes with free credits; the key looks like
# nvapi-... . The catalogue is public, but without a key the provider is skipped like any other -
# listing models this deployment cannot call would be advertising something that does not work.
NVIDIA_NIM_API_KEY=

# Cloudflare Workers AI needs both
CLOUDFLARE_API_KEY=
Expand Down
14 changes: 9 additions & 5 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ and this file is a bug.

## 1. Identity

**Product:** real-time market intelligence for free LLM models across 7 providers, plus an
OpenAI-compatible proxy and a chat client for them.
**Product:** real-time market intelligence for free LLM models across every provider in
`src/config/providers.ts`, plus an OpenAI-compatible proxy and a chat client for them. The count is
derived from the registry wherever it is shown - it was hardcoded as "7 providers" and stayed that
way through two additions.

**Personality:** warm-editorial density. Information-first, but not cold - a serif display face over
a dense data table, on ivory rather than clinical grey.
Expand Down Expand Up @@ -79,10 +81,12 @@ hardcoded hue-265 blue left behind by the palette that no longer exists.
### Provider tokens - avatars and charts only, never UI accents

`openrouter` 35 · `ollama` 200 · `aistudio` 90 · `groq` 25 · `cerebras` 290 · `cloudflare` 60 ·
`local` 145.
`local` 145 · `nvidia` 130.

`local` is your own machine. Its green is deliberately the quietest of the seven: it is the only
tier that is free without qualification, and it should read as calm, not as a status badge.
`local` is your own machine. Its green is deliberately the quietest of the set: it is the only tier
that is free without qualification, and it should read as calm, not as a status badge. `nvidia` sits
next to it in hue on purpose - NVIDIA's own brand green - but at much higher chroma, so the two
never read as the same provider at a glance.

---

Expand Down
27 changes: 26 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@
strip_reasoning,
strip_reasoning_chunk,
)
from providers.nvidia import (
fetch_nvidia_models,
build_nvidia_market_stats,
is_nvidia_model,
strip_nvidia_prefix,
get_nvidia_api_key,
get_nvidia_base_url,
NVIDIA_DEFAULT_BASE_URL,
)
from providers.cloudflare import (
fetch_cloudflare_models,
build_cloudflare_market_stats,
Expand Down Expand Up @@ -343,6 +352,16 @@ def resolve_model(
"api_key": user_key or env_key,
}

if is_nvidia_model(model_id):
# litellm has a native nvidia_nim provider, so the base URL is its concern - api_base is
# passed only when the operator has overridden it.
slug = strip_nvidia_prefix(model_id)
nvidia_extra: dict[str, Any] = {"api_key": user_key or get_nvidia_api_key()}
base = get_nvidia_base_url()
if base != NVIDIA_DEFAULT_BASE_URL:
nvidia_extra["api_base"] = base
return f"nvidia_nim/{slug}", nvidia_extra

if is_local_model(model_id):
# Local server speaks the OpenAI API, so it rides the generic openai/ path with
# an api_base. No BYOK: the endpoint is the user's own machine.
Expand Down Expand Up @@ -683,22 +702,28 @@ async def _compute_rankings() -> list[dict]:
cerebras_raw,
cloudflare_raw,
local_raw,
nvidia_raw,
) = await asyncio.gather(
fetch_market_data(),
fetch_unified_market_stats(),
fetch_groq_models(),
fetch_cerebras_models(),
fetch_cloudflare_models(),
fetch_local_models(),
fetch_nvidia_models(),
)

ollama_stats, aistudio_stats = unified_extra
groq_stats = build_groq_market_stats(groq_raw)
cerebras_stats = build_cerebras_market_stats(cerebras_raw)
cloudflare_stats = build_cloudflare_market_stats(cloudflare_raw)
local_stats = build_local_market_stats(local_raw)
nvidia_stats = build_nvidia_market_stats(nvidia_raw)

extras = ollama_stats + aistudio_stats + groq_stats + cerebras_stats + cloudflare_stats + local_stats
extras = (
ollama_stats + aistudio_stats + groq_stats + cerebras_stats
+ cloudflare_stats + local_stats + nvidia_stats
)
all_stats = openrouter_stats + extras
if not all_stats:
return []
Expand Down
14 changes: 14 additions & 0 deletions backend/providers/authors.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@
"moonshotai": "Moonshot AI",
"zai-org": "Z.AI",
"z-ai": "Z.AI",
# NVIDIA NIM ids carry an author segment too, and these are the ones whose title-cased form
# would be wrong or ugly ("Ai21Labs", "01 Ai", "Nv-Mistralai").
"meta": "Meta",
"ai21labs": "AI21",
"01-ai": "01.AI",
"aisingapore": "AI Singapore",
"bigcode": "BigCode",
"deepseek-ai": "DeepSeek",
"minimaxai": "MiniMax",
"moonshotai": "Moonshot AI",
"nv-mistralai": "NVIDIA + Mistral",
"stepfun-ai": "StepFun",
"thinkingmachines": "Thinking Machines",
"ibm": "IBM",
}


Expand Down
25 changes: 24 additions & 1 deletion backend/providers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,19 +152,30 @@ async def fetch_unified_models() -> dict:
from .cerebras import fetch_cerebras_models
from .cloudflare import fetch_cloudflare_models
from .local import fetch_local_models
from .nvidia import NVIDIA_MODEL_PREFIX, fetch_nvidia_models

async with httpx.AsyncClient() as client:
ollama_task = _fetch_ollama_models(client)
aistudio_task = _fetch_aistudio_models(client)
openrouter_task = _fetch_openrouter_models(client)
ollama_raw, aistudio_raw, openrouter_raw, groq_raw, cerebras_raw, cloudflare_raw, local_raw = await asyncio.gather(
(
ollama_raw,
aistudio_raw,
openrouter_raw,
groq_raw,
cerebras_raw,
cloudflare_raw,
local_raw,
nvidia_raw,
) = await asyncio.gather(
ollama_task,
aistudio_task,
openrouter_task,
fetch_groq_models(),
fetch_cerebras_models(),
fetch_cloudflare_models(),
fetch_local_models(),
fetch_nvidia_models(),
)

data = []
Expand All @@ -190,6 +201,18 @@ async def fetch_unified_models() -> dict:
if not slug:
continue
data.append({"id": f"local/{slug}", "object": "model", "created": m.get("created"), "owned_by": "local"})
for m in nvidia_raw:
slug = m.get("id", "")
if not slug:
continue
data.append({
"id": f"{NVIDIA_MODEL_PREFIX}{slug}",
"object": "model",
"created": m.get("created"),
# The author segment NIM already puts in its ids, kept rather than flattened to "nvidia":
# NVIDIA serves these, it did not build most of them.
"owned_by": m.get("owned_by") or "nvidia",
})
for m in openrouter_raw:
data.append({"id": m["id"], "object": "model", "created": m.get("created"), "owned_by": "openrouter"})

Expand Down
151 changes: 151 additions & 0 deletions backend/providers/nvidia.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""NVIDIA NIM (build.nvidia.com) as a provider.

NIM is OpenAI-compatible and routes through litellm's native `nvidia_nim/` provider, whose default
base is https://integrate.api.nvidia.com/v1.

Three things make this catalogue different from the other providers':

the prefix is `nim/`, not NVIDIA is an AUTHOR on OpenRouter, which serves ids like
`nvidia/` `nvidia/nemotron-3-super-120b-a12b`. A `nvidia/` route prefix would
have been indistinguishable from those: the market would list them
twice, the provider filter would claim OpenRouter's rows as NIM's,
and resolve_model would send an OpenRouter model to NIM, where it
does not exist. `nim/` collides with none of OpenRouter's 57 authors
and matches litellm's own provider id.

ids carry an author segment `meta/llama-3.1-8b-instruct`, exactly like OpenRouter. Prefixed for
the market that becomes `nim/meta/llama-3.1-8b-instruct`: the first
segment is the ROUTE, the second is the AUTHOR, and they are
genuinely different facts.

the catalogue is mixed /v1/models lists embedders, rerankers, OCR, speech and image models
alongside chat ones. Those are not chat-completable, so shipping them
would fill the market with rows that can only ever fail. They are
filtered out here rather than left for the availability prober to
discover one wasted call at a time.

Unlike every other provider here, the catalogue needs no key - so the models are listed even on a
deployment that cannot call them. Inference still requires NVIDIA_NIM_API_KEY, and without one the
provider is skipped, matching the "providers without a key are skipped" contract.
"""

import math
import os
import re

import httpx

from providers.authors import author_from_model_id
from providers.params import capability_params, parse_params

NVIDIA_MODEL_PREFIX = "nim/"
NVIDIA_PROVIDER_NAME = "NVIDIA NIM"
NVIDIA_DEFAULT_BASE_URL = "https://integrate.api.nvidia.com/v1"
NVIDIA_TIMEOUT_S = 10.0

# NIM does not publish a per-model context window anywhere in /v1/models. This is a conservative
# FLOOR used only so the capability heuristic has a number to work with - deliberately not the 128k
# most of these models actually support, because claiming a window we have not verified is the kind
# of confident-looking fiction this codebase keeps having to delete.
NVIDIA_DEFAULT_CTX = 32_768

# Families that are not chat-completable. Matched against the full id, so `nvidia/embed-qa-4` and
# `baai/bge-m3` both go, while `meta/llama-3.1-8b-instruct` stays.
_NON_CHAT_PATTERN = re.compile(
r"embed|bge|rerank|retriever|ocr|paddle|whisper|riva|asr|tts|speech|vila|clip|molmo|esm"
r"|diffusion|stable-|sdxl|flux|video|cosmos|genmol|protein|fold|dna|audio|parakeet|canary"
r"|table-structure|graphic-elements|page-elements|chart|deplot|florence",
re.I,
)

_REASONING_PATTERN = re.compile(
r"deepseek-r1|reasoning|thinking|qwen3|gpt-oss|nemotron-.*-reason|magistral|kimi", re.I
)


def get_nvidia_api_key() -> str:
"""The name litellm itself reads, so a key set for one is set for both."""
return os.getenv("NVIDIA_NIM_API_KEY", "")


def get_nvidia_base_url() -> str:
return os.getenv("NVIDIA_NIM_API_BASE", NVIDIA_DEFAULT_BASE_URL).rstrip("/")


def is_nvidia_model(model_id: str) -> bool:
return isinstance(model_id, str) and model_id.startswith(NVIDIA_MODEL_PREFIX)


def strip_nvidia_prefix(model_id: str) -> str:
return model_id[len(NVIDIA_MODEL_PREFIX):] if is_nvidia_model(model_id) else model_id


def is_chat_model(model_id: str) -> bool:
return not _NON_CHAT_PATTERN.search(model_id)


def _is_reasoning(model_id: str) -> bool:
return bool(_REASONING_PATTERN.search(model_id))


async def fetch_nvidia_models() -> list[dict]:
"""The chat-capable catalogue, or [] when no key is configured.

The listing endpoint is public, but a deployment with no key cannot actually CALL any of these -
so it must not advertise them. Same contract as every other provider: no key, no rows.
"""
if not get_nvidia_api_key():
return []
try:
async with httpx.AsyncClient() as client:
r = await client.get(
f"{get_nvidia_base_url()}/models",
headers={"Authorization": f"Bearer {get_nvidia_api_key()}"},
timeout=NVIDIA_TIMEOUT_S,
)
r.raise_for_status()
rows = r.json().get("data", [])
except Exception as e:
print(f"NVIDIA NIM models error: {e}")
return []

return [m for m in rows if m.get("id") and is_chat_model(m["id"])]


def build_nvidia_market_stats(raw: list[dict]) -> list[dict]:
result = []
for m in raw:
slug = m.get("id") or ""
if not slug:
continue
params = parse_params(slug)
ctx = NVIDIA_DEFAULT_CTX
capability = capability_params(params) * math.log10(ctx + 1)
result.append({
"id": f"{NVIDIA_MODEL_PREFIX}{slug}",
"name": slug,
"params": params,
"ctx": ctx,
# Free by allocation, like Cloudflare Workers AI: a personal NVIDIA account comes with
# free credits, and the endpoints are callable until those run out. Exhausted credits
# surface as a rate-limit/quota error, which availability.py classifies as TRANSIENT -
# so running out hides the models for a while rather than deleting them permanently.
"is_free": True,
"capability": capability,
"brain": _is_reasoning(slug),
# NIM publishes no per-model tool-calling flag, and the market's other key-based
# providers (Groq, Cerebras) make the same optimistic assumption. Kept consistent rather
# than quietly different; the agentic BENCH scores are what actually drive fit_agent.
"tools": True,
"open": True,
# NVIDIA publishes no throughput figures, so this stays absent rather than estimated.
"tps": None,
"uptime": None,
"provider": NVIDIA_PROVIDER_NAME,
# The id's first segment is the author, exactly as on OpenRouter - so the row can say
# who BUILT the model instead of only who serves it.
"author": author_from_model_id(slug),
"balanced": 0.0,
"value": 0.0,
})
return result
2 changes: 2 additions & 0 deletions backend/providers/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .groq import GROQ_MODEL_PREFIX, GROQ_PROVIDER_NAME
from .local import LOCAL_MODEL_PREFIX, LOCAL_PROVIDER_NAME
from .models import AISTUDIO_PROVIDER, OLLAMA_PROVIDER, OPENROUTER_PROVIDER
from .nvidia import NVIDIA_MODEL_PREFIX, NVIDIA_PROVIDER_NAME

AISTUDIO_MODEL_PREFIX = "aistudio/"
OLLAMA_MODEL_PREFIX = "ollama/"
Expand All @@ -28,6 +29,7 @@
CEREBRAS_MODEL_PREFIX: CEREBRAS_PROVIDER_NAME,
CLOUDFLARE_MODEL_PREFIX: CLOUDFLARE_PROVIDER_NAME,
LOCAL_MODEL_PREFIX: LOCAL_PROVIDER_NAME,
NVIDIA_MODEL_PREFIX: NVIDIA_PROVIDER_NAME,
AISTUDIO_MODEL_PREFIX: AISTUDIO_PROVIDER,
OLLAMA_MODEL_PREFIX: OLLAMA_PROVIDER,
OPENROUTER_MODEL_PREFIX: OPENROUTER_PROVIDER,
Expand Down
Loading