diff --git a/README.md b/README.md index b1773b2..b4b8455 100644 --- a/README.md +++ b/README.md @@ -1284,6 +1284,8 @@ for human review. | `api` | medium | Calls each provider's OpenAI-compatible `/v1/models` endpoint when `_API_KEY` is set in your environment. Used to detect *removals* (a believed-free model that's no longer listed). | | `litellm_cost_map` | medium | Reads the public [litellm](https://github.com/BerriAI/litellm) pricing map: flags zero-priced models as free **and** snapshots per-token prices for paid ones into the sidecar `pricing` block (used by the proxy to cost tokens offline — see [Token + cost accounting](#usage-accounting)). | | `together` | high | When `TOGETHER_API_KEY` is set, reads Together's `/v1/models` pricing — zero-priced models are free; paid models contribute per-token prices to the `pricing` block. | +| `fireworks` | high | When `FIREWORKS_API_KEY` is set, reads Fireworks' `/inference/v1/models` and flags models marked `is_free`/`serverless_billing: free` or zero-priced as free. | +| `requesty` | high | When `REQUESTY_API_KEY` is set, reads Requesty's `/v1/models` pricing — zero-priced models are free; paid models contribute per-token prices to the `pricing` block. | | `community` | low | Pulls the [tashfeenahmed/freellmapi](https://github.com/tashfeenahmed/freellmapi) community list as a sanity signal. | | `probe` | high · **opt-in** | Sends a tiny real chat request to each `believed_free` model and flags any that report a cost. Off by default; enable with `probe_cost: true` in `config.json` or the `--probe` flag. Spends a little quota. | diff --git a/config.example.json b/config.example.json index b34d0a4..f8c43f8 100644 --- a/config.example.json +++ b/config.example.json @@ -90,6 +90,11 @@ "api_key": "sk-or-...", "model_filter": null }, + "requesty": { + "base_url": "https://router.requesty.ai/v1", + "api_key": "...", + "model_filter": null + }, "ollama-cloud": { "base_url": "https://ollama.com/v1", "api_key": "...", diff --git a/llmproxy/providers.json b/llmproxy/providers.json index 27739fd..eaf583a 100644 --- a/llmproxy/providers.json +++ b/llmproxy/providers.json @@ -25,6 +25,7 @@ "deepseek", "cohere", "openrouter", + "requesty", "ollama-cloud", "moonshot", "minimax", @@ -1391,6 +1392,17 @@ "free_limits": {}, "fallback_models": [] }, + "requesty": { + "display": "Requesty (LLM router)", + "base_url": "https://router.requesty.ai/v1", + "key_required": true, + "key_hint": "Get your API key at app.requesty.ai", + "believed_free": [], + "model_reasoning": {}, + "model_capabilities": {}, + "free_limits": {}, + "fallback_models": [] + }, "opencode-zen": { "display": "OpenCode Zen (free gateway)", "base_url": "https://opencode.ai/zen/v1", diff --git a/scripts/sources/__init__.py b/scripts/sources/__init__.py index 1006110..b1e0786 100644 --- a/scripts/sources/__init__.py +++ b/scripts/sources/__init__.py @@ -12,6 +12,7 @@ from .fireworks import FireworksSource from .litellm_cost_map import LiteLLMCostMapSource from .openrouter import OpenRouterSource +from .requesty import RequestySource from .together import TogetherSource ALL_SOURCES: dict[str, type[Source]] = { @@ -21,6 +22,7 @@ "litellm_cost_map": LiteLLMCostMapSource, "together": TogetherSource, "fireworks": FireworksSource, + "requesty": RequestySource, # Active cost probe. Excluded from the default source set because it sends # real requests; opt in via free_tier.cost_probe.enabled or --cost-probe. "cost_probe": CostProbeSource, diff --git a/scripts/sources/requesty.py b/scripts/sources/requesty.py new file mode 100644 index 0000000..6ef06f9 --- /dev/null +++ b/scripts/sources/requesty.py @@ -0,0 +1,95 @@ +"""Requesty source — pricing-aware /v1/models detection. + +Requesty's `/v1/models` endpoint (OpenAI-compatible) returns pricing +metadata per model as flat top-level fields — `input_price`, `output_price`, +`cached_price` — not nested under a `pricing` sub-object. Models with zero +input/output cost are genuinely free (Requesty publishes a small set of +$0-cost models from upstream providers that host them for free, e.g. some +Nemotron and Poolside variants). Requires ``REQUESTY_API_KEY`` in the +environment; without it, the source yields nothing. + +Model ids are already namespaced by upstream provider (e.g. +"anthropic/claude-opus-4-7"); we prefix with our own provider key for the +fully-qualified id, consistent with every other source. + +Prices are plain per-token dollar amounts (confirmed against a live +response), consistent with OpenRouter's convention rather than Together's +per-million-token convention. +""" + +from __future__ import annotations + +import os + +import requests + +from .base import Evidence, Source + +REQUESTY_URL = "https://router.requesty.ai/v1/models" +TIMEOUT = (5, 15) + + +class RequestySource(Source): + name = "requesty" + + def __init__(self, url: str = REQUESTY_URL): + self.url = url + + def fetch(self) -> list[Evidence]: + api_key = os.environ.get("REQUESTY_API_KEY") + if not api_key: + return [] + resp = requests.get( + self.url, + headers={"Authorization": f"Bearer {api_key}"}, + timeout=TIMEOUT, + ) + resp.raise_for_status() + data = resp.json() + models = data.get("data", data) if isinstance(data, dict) else data + if not isinstance(models, list): + return [] + + out: list[Evidence] = [] + for model in models: + if not isinstance(model, dict): + continue + mid = model.get("id") + if not mid: + continue + in_cost = _to_float(model.get("input_price")) + out_cost = _to_float(model.get("output_price")) + is_free = (in_cost == 0.0 and out_cost == 0.0) + out.append(Evidence( + provider="requesty", + model_id=f"requesty/{mid}", + is_free=is_free, + source=self.name, + confidence="high", + url=self.url, + pricing=_pricing(in_cost, out_cost), + notes=f"input_price={model.get('input_price')!r} output_price={model.get('output_price')!r}", + )) + return out + + +def _pricing(in_cost: float, out_cost: float) -> dict | None: + """Per-token pricing for a PAID model, or None when free/unknown.""" + in_ok = in_cost != float("inf") + out_ok = out_cost != float("inf") + if not in_ok and not out_ok: + return None + in_cost = in_cost if in_ok else 0.0 + out_cost = out_cost if out_ok else 0.0 + if in_cost == 0.0 and out_cost == 0.0: + return None + return {"input_cost_per_token": in_cost, "output_cost_per_token": out_cost} + + +def _to_float(v) -> float: + if v is None: + return float("inf") + try: + return float(v) + except (TypeError, ValueError): + return float("inf") diff --git a/tests/fixtures/requesty_models.json b/tests/fixtures/requesty_models.json new file mode 100644 index 0000000..36b5a4d --- /dev/null +++ b/tests/fixtures/requesty_models.json @@ -0,0 +1,41 @@ +[ + { + "id": "nvidia/nemotron-3-super-120b-a12b", + "object": "model", + "owned_by": "nvidia", + "input_price": 0, + "output_price": 0, + "cached_price": 0 + }, + { + "id": "nvidia/nemotron-3-nano-30b-a3b", + "object": "model", + "owned_by": "nvidia", + "input_price": 0, + "output_price": 0, + "cached_price": 0 + }, + { + "id": "google/gemma-4-31b-it", + "object": "model", + "owned_by": "google", + "input_price": 0, + "output_price": 0, + "cached_price": 0 + }, + { + "id": "anthropic/claude-opus-4-7", + "object": "model", + "owned_by": "anthropic", + "input_price": 0.000015, + "output_price": 0.000075, + "cached_price": 0.0000015 + }, + { + "id": "openai/gpt-5.2", + "object": "model", + "owned_by": "openai", + "input_price": 0.000002, + "output_price": 0.000008 + } +] diff --git a/tests/test_scraper/test_requesty_source.py b/tests/test_scraper/test_requesty_source.py new file mode 100644 index 0000000..f686781 --- /dev/null +++ b/tests/test_scraper/test_requesty_source.py @@ -0,0 +1,73 @@ +"""Requesty source: $0 models → is_free=True, paid → is_free=False.""" + +from __future__ import annotations + +from pathlib import Path + +import responses + +from scripts.sources.requesty import REQUESTY_URL, RequestySource + + +def _set_key(monkeypatch): + monkeypatch.setenv("REQUESTY_API_KEY", "req-test") + + +@responses.activate +def test_free_models_emitted(fixtures_dir: Path, monkeypatch): + _set_key(monkeypatch) + body = (fixtures_dir / "requesty_models.json").read_text() + responses.add(responses.GET, REQUESTY_URL, body=body, status=200, + content_type="application/json") + evs = RequestySource().fetch() + by_id = {e.model_id: e for e in evs} + assert by_id["requesty/nvidia/nemotron-3-super-120b-a12b"].is_free is True + assert by_id["requesty/nvidia/nemotron-3-nano-30b-a3b"].is_free is True + assert by_id["requesty/google/gemma-4-31b-it"].is_free is True + assert by_id["requesty/anthropic/claude-opus-4-7"].is_free is False + assert by_id["requesty/openai/gpt-5.2"].is_free is False + + +@responses.activate +def test_paid_models_carry_per_token_pricing(fixtures_dir: Path, monkeypatch): + _set_key(monkeypatch) + body = (fixtures_dir / "requesty_models.json").read_text() + responses.add(responses.GET, REQUESTY_URL, body=body, status=200, + content_type="application/json") + by_id = {e.model_id: e for e in RequestySource().fetch()} + assert by_id["requesty/anthropic/claude-opus-4-7"].pricing == { + "input_cost_per_token": 0.000015, + "output_cost_per_token": 0.000075, + } + # Free models carry no pricing opinion. + assert by_id["requesty/nvidia/nemotron-3-super-120b-a12b"].pricing is None + + +def test_no_api_key_returns_empty(monkeypatch): + """Without REQUESTY_API_KEY, the source yields nothing (no HTTP call).""" + monkeypatch.delenv("REQUESTY_API_KEY", raising=False) + assert RequestySource().fetch() == [] + + +@responses.activate +def test_all_evidence_high_confidence(fixtures_dir: Path, monkeypatch): + _set_key(monkeypatch) + body = (fixtures_dir / "requesty_models.json").read_text() + responses.add(responses.GET, REQUESTY_URL, body=body, status=200) + evs = RequestySource().fetch() + assert evs + for e in evs: + assert e.confidence == "high" + assert e.source == "requesty" + + +@responses.activate +def test_auth_header_present(fixtures_dir: Path, monkeypatch): + """Verify the source sends the Bearer auth header.""" + _set_key(monkeypatch) + body = (fixtures_dir / "requesty_models.json").read_text() + rsp = responses.add(responses.GET, REQUESTY_URL, body=body, status=200) + RequestySource().fetch() + assert rsp.call_count == 1 + auth = responses.calls[0].request.headers.get("Authorization", "") + assert auth.startswith("Bearer ")