Skip to content

Commit f25c461

Browse files
feat: add Requesty provider (#1145)
## Summary Adds **Requesty** as a new named LLM provider, mirroring the existing OpenRouter provider exactly. Requesty is an OpenAI-compatible LLM router (base URL `https://router.requesty.ai/v1`) that uses the same `provider/model` naming scheme as OpenRouter (e.g. `openai/gpt-4o-mini`). Because Requesty is OpenAI-compatible and behaves like OpenRouter (including the same optional reasoning directive shape), the implementation is a structural 1:1 mirror of `src/any_llm/providers/openrouter/`, changing only the provider id, class name, base URL, env var, and documentation URL. ## What changed - **`src/any_llm/providers/requesty/`** (new package): `requesty.py`, `__init__.py`, `utils.py` mirroring the OpenRouter provider. - `API_BASE = "https://router.requesty.ai/v1"` - `ENV_API_KEY_NAME` = `REQUESTY_API_KEY`, `ENV_API_BASE_NAME = "REQUESTY_API_BASE"` - `PROVIDER_NAME = "requesty"`, docs at `https://docs.requesty.ai` - Subclasses `BaseOpenAIProvider`, same reasoning-directive handling and `_convert_models_list` model-normalization as OpenRouter. - **`src/any_llm/constants.py`**: added `REQUESTY = "requesty"` to the `LLMProvider` enum. - **`pyproject.toml`**: added the `requesty = []` optional-dependency group and included `requesty` in the `all` extra (the OpenAI-compatible base requires no extra SDK, same as OpenRouter). - **`tests/unit/providers/test_requesty_provider.py`** (new): full mirror of `test_openrouter_provider.py` (reasoning directive, streaming, max_tokens remap, model-list normalization, round-trip serialization). The factory (`AnyLLM._create_provider`) discovers providers dynamically from the enum value, so no factory/mapping edit is needed; the enum + matching package name + pyproject group is the full registration surface, which the repo's exhaustiveness tests enforce. ## Note on the README The README does not contain a committed supported-provider list/table (the provider list is generated to GitBook via `scripts/convert_to_gitbook.py` and published to `docs.mozilla.ai`, not committed). The only OpenRouter mention in the README is the "Proxy Only Solutions" comparison paragraph, where adding Requesty would be inappropriate. Happy to add a docs/provider entry wherever the maintainers prefer. ## Usage ```python from any_llm import AnyLLM provider = AnyLLM.create("requesty", api_key="<REQUESTY_API_KEY>") completion = provider.completion( model="openai/gpt-4o-mini", messages=[{"role": "user", "content": "Hello!"}], ) ``` Set `REQUESTY_API_KEY` in your environment, or pass `api_key=` directly. ## Testing & linting - `uv run pytest tests/unit/providers/test_requesty_provider.py tests/unit/providers/test_openrouter_provider.py tests/unit/test_provider.py tests/unit/test_provider_pyproject_options.py -q` -> **127 passed, 9 skipped** (includes the enum/pyproject/load exhaustiveness tests that load every provider). - `ruff@0.15.12 check` and `ruff@0.15.12 format --check` (the pinned pre-commit version) -> clean on the new files. - `uv run mypy src/any_llm/providers/requesty/` (strict) -> `Success: no issues found`. ## Disclosure I work at Requesty. Happy to adjust scope/naming or close if out of scope. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for the Requesty provider, including completion streaming. * Enabled reasoning support for Requesty-based completions, with correct request shaping. * **Tests** * Added comprehensive unit tests for Requesty completion parameters, reasoning injection, and model-list conversion. * **Chores** * Updated optional dependencies to include the Requesty provider extra (as a newly available provider option). <!-- end of auto-generated comment: release notes by coderabbit.ai --> ## PR Type - 🆕 New Feature ## Relevant issues N/A ## Checklist - [x] I understand the code I am submitting. - [x] I have added unit tests that prove my fix/feature works - [x] I have run this code locally and verified it fixes the issue. - [x] New and existing tests pass locally - [x] Documentation was updated where necessary - [x] I have read and followed the [contribution guidelines](https://github.com/mozilla-ai/any-llm/blob/main/CONTRIBUTING.md) - [x] **AI Usage:** - [x] AI was used for drafting/refactoring. ## AI Usage Information - AI Model used: Claude Opus - AI Developer Tool used: CLI agent - Any other info you'd like to share: The provider is a structural 1:1 mirror of the existing OpenRouter provider; I reviewed every line, ran the unit tests (new + openrouter) and the enum/pyproject exhaustiveness tests locally, and confirmed ruff + mypy pass before submitting. - [ ] I am an AI Agent filling out this form (check box if true)
1 parent 50c5d4e commit f25c461

6 files changed

Lines changed: 527 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ dependencies = [
2121
[project.optional-dependencies]
2222

2323
all = [
24-
"any-llm-sdk[mistral,anthropic,huggingface,gemini,vertexai,vertexaianthropic,cohere,cerebras,fireworks,groq,bedrock,azure,azureanthropic,azureopenai,watsonx,together,sambanova,ollama,moonshot,neosantara,nebius,xai,databricks,deepseek,inception,openai,otari,openrouter,portkey,qiniu,lmstudio,llama,voyage,perplexity,platform,llamafile,llamacpp,sagemaker,github,zai,minimax,mzai,vllm,dashscope,deepinfra]"
24+
"any-llm-sdk[mistral,anthropic,huggingface,gemini,vertexai,vertexaianthropic,cohere,cerebras,fireworks,groq,bedrock,azure,azureanthropic,azureopenai,watsonx,together,sambanova,ollama,moonshot,neosantara,nebius,xai,databricks,deepseek,inception,openai,otari,openrouter,portkey,qiniu,requesty,lmstudio,llama,voyage,perplexity,platform,llamafile,llamacpp,sagemaker,github,zai,minimax,mzai,vllm,dashscope,deepinfra]"
2525
]
2626

2727
platform = [
@@ -128,6 +128,7 @@ openai = []
128128
openrouter = []
129129
portkey = []
130130
qiniu = []
131+
requesty = []
131132
sambanova = []
132133
minimax = []
133134
mzai = []

src/any_llm/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class LLMProvider(StrEnum):
4646
OPENROUTER = "openrouter"
4747
PORTKEY = "portkey"
4848
QINIU = "qiniu"
49+
REQUESTY = "requesty"
4950
SAMBANOVA = "sambanova"
5051
SAGEMAKER = "sagemaker"
5152
TOGETHER = "together"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .requesty import RequestyProvider
2+
3+
__all__ = ["RequestyProvider"]
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from collections.abc import Sequence
2+
from typing import Any
3+
4+
from typing_extensions import override
5+
6+
from any_llm.providers.openai.base import BaseOpenAIProvider
7+
from any_llm.providers.requesty.utils import _convert_models_list, build_reasoning_directive
8+
from any_llm.types.completion import CompletionParams
9+
from any_llm.types.model import Model
10+
11+
12+
class RequestyProvider(BaseOpenAIProvider):
13+
API_BASE = "https://router.requesty.ai/v1"
14+
ENV_API_KEY_NAME = "REQUESTY_API_KEY"
15+
ENV_API_BASE_NAME = "REQUESTY_API_BASE"
16+
PROVIDER_NAME = "requesty"
17+
PROVIDER_DOCUMENTATION_URL = "https://docs.requesty.ai"
18+
19+
SUPPORTS_COMPLETION_STREAMING = True
20+
SUPPORTS_COMPLETION = True
21+
SUPPORTS_RESPONSES = False
22+
SUPPORTS_COMPLETION_REASONING = True
23+
SUPPORTS_EMBEDDING = True
24+
# Requesty does not expose a moderation endpoint.
25+
SUPPORTS_MODERATION = False
26+
27+
@staticmethod
28+
@override
29+
def _convert_list_models_response(response: Any) -> Sequence[Model]:
30+
"""Convert Requesty list models response to valid Model objects."""
31+
return _convert_models_list(response)
32+
33+
@staticmethod
34+
@override
35+
def _convert_completion_params(params: CompletionParams, **kwargs: Any) -> dict[str, Any]:
36+
"""Convert CompletionParams to kwargs for Requesty API, including reasoning directive."""
37+
# Extract ``reasoning`` before delegating so the base converter does not
38+
# forward it as a top-level kwarg (unsupported by the OpenAI SDK's
39+
# ``create()``). Requesty expects it nested under ``extra_body`` instead.
40+
reasoning = kwargs.pop("reasoning", None)
41+
converted_params = BaseOpenAIProvider._convert_completion_params(params, **kwargs)
42+
43+
reasoning_directive = build_reasoning_directive(
44+
reasoning=reasoning,
45+
reasoning_effort=params.reasoning_effort,
46+
)
47+
48+
if reasoning_directive is not None:
49+
converted_params.pop("reasoning_effort", None)
50+
extra_body = converted_params.get("extra_body", {}).copy()
51+
extra_body["reasoning"] = reasoning_directive
52+
converted_params["extra_body"] = extra_body
53+
54+
return converted_params
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Requesty Provider Utilities.
2+
3+
Utilities for building Requesty-specific request parameters, with explicit
4+
opt-in handling for reasoning capabilities.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from typing import TYPE_CHECKING, Any
10+
11+
from any_llm.types.model import Model
12+
13+
if TYPE_CHECKING:
14+
from collections.abc import Sequence
15+
16+
17+
def build_reasoning_directive(
18+
*,
19+
reasoning: Any | None = None,
20+
reasoning_effort: str | None = None,
21+
) -> dict[str, Any] | None:
22+
"""Build Requesty's reasoning directive from user parameters.
23+
24+
Args:
25+
reasoning: Direct reasoning config (dict/object) for advanced control
26+
reasoning_effort: Standard effort level ("low"/"medium"/"high")
27+
28+
Returns:
29+
Dict for request body "reasoning" key, or None to omit
30+
"""
31+
if reasoning is not None:
32+
return _normalize_reasoning_obj(reasoning)
33+
34+
if reasoning_effort and reasoning_effort not in ("auto", "none"):
35+
level = str(reasoning_effort).lower()
36+
if level in {"low", "medium", "high"}:
37+
return {"effort": level}
38+
39+
return None
40+
41+
42+
def _convert_models_list(response: Any) -> Sequence[Model]:
43+
"""Convert Requesty list models response to valid Model objects.
44+
45+
Requesty's ``/v1/models`` endpoint omits the ``object`` and
46+
``owned_by`` fields that the OpenAI ``Model`` schema requires. The OpenAI
47+
SDK silently accepts those missing fields via ``model_construct()``, but the
48+
resulting objects fail round-trip serialization. This function fills the
49+
missing required fields while preserving any extra Requesty attributes
50+
(e.g. ``name``, ``pricing``) that may already be present on the SDK objects.
51+
"""
52+
raw_models = response.data if hasattr(response, "data") else response
53+
result: list[Model] = []
54+
for model in raw_models:
55+
data: dict[str, Any] = model.model_dump() if hasattr(model, "model_dump") else dict(vars(model))
56+
if data.get("object") is None:
57+
data["object"] = "model"
58+
if data.get("owned_by") is None:
59+
data["owned_by"] = "requesty"
60+
if data.get("created") is None:
61+
data["created"] = 0
62+
result.append(Model.model_validate(data))
63+
return result
64+
65+
66+
def _normalize_reasoning_obj(obj: Any) -> dict[str, Any]:
67+
"""Normalize reasoning config to Requesty format."""
68+
69+
def _get(o: Any, k: str) -> Any:
70+
return o.get(k) if isinstance(o, dict) else getattr(o, k, None)
71+
72+
out: dict[str, Any] = {}
73+
74+
effort = _get(obj, "effort")
75+
if effort is not None:
76+
out["effort"] = str(effort).lower()
77+
78+
max_tokens = _get(obj, "max_tokens")
79+
if max_tokens is not None:
80+
out["max_tokens"] = int(max_tokens)
81+
82+
exclude = _get(obj, "exclude")
83+
if exclude is not None:
84+
out["exclude"] = bool(exclude)
85+
86+
enabled = _get(obj, "enabled")
87+
if enabled is not None:
88+
out["enabled"] = bool(enabled)
89+
90+
return out

0 commit comments

Comments
 (0)