From a94c82c9a1b53d838b23ef798eb27f97e4c54a17 Mon Sep 17 00:00:00 2001 From: Thibault Jaigu Date: Mon, 13 Jul 2026 10:14:22 +0100 Subject: [PATCH] feat(requesty): add Requesty provider Add Requesty (https://requesty.ai), an OpenAI-compatible LLM router, as a provider, mirroring the existing OpenRouter provider. - instructor/v2/providers/requesty/ (from_requesty factory + JSON-schema handler) - Provider.REQUESTY enum + get_provider URL detection - PROVIDER_SPECS spec entry + OpenAI-compat/parallel-tools registration - from_provider('requesty//') dispatch + _build_requesty - instructor.__init__ export + lazy import - docs/integrations/requesty.md + mkdocs nav + example + CHANGELOG Signed-off-by: Thibault Jaigu --- CHANGELOG.md | 3 + docs/integrations/requesty.md | 132 +++++++++++++++++++ examples/open_source_examples/requesty.py | 42 ++++++ instructor/__init__.py | 3 + instructor/v2/auto_client.py | 65 +++++++++ instructor/v2/core/provider_specs.py | 20 +++ instructor/v2/core/providers.py | 2 + instructor/v2/providers/openai/handlers.py | 2 + instructor/v2/providers/requesty/__init__.py | 6 + instructor/v2/providers/requesty/client.py | 51 +++++++ instructor/v2/providers/requesty/handlers.py | 48 +++++++ mkdocs.yml | 1 + 12 files changed, 375 insertions(+) create mode 100644 docs/integrations/requesty.md create mode 100644 examples/open_source_examples/requesty.py create mode 100644 instructor/v2/providers/requesty/__init__.py create mode 100644 instructor/v2/providers/requesty/client.py create mode 100644 instructor/v2/providers/requesty/handlers.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fcbd6f1b..3b880258d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) ## [Unreleased] +### Added +- **Requesty**: Add [Requesty](https://requesty.ai) as a provider (`from_requesty` / `from_provider("requesty/...")`), an OpenAI-compatible LLM router, mirroring the existing OpenRouter provider. + ### Fixed - **v2 message handling**: Preserve caller-owned message lists and nested content across request preparation and retries for OpenAI-compatible, Cohere, Mistral, OpenRouter, Writer, and xAI handlers. ([#2417](https://github.com/567-labs/instructor/issues/2417), [#2428](https://github.com/567-labs/instructor/issues/2428)) - **v2 JSON extraction**: Prefer the final complete top-level JSON value in text responses and retain every JSON object when multiple objects arrive in one streaming chunk. diff --git a/docs/integrations/requesty.md b/docs/integrations/requesty.md new file mode 100644 index 000000000..b90972ccb --- /dev/null +++ b/docs/integrations/requesty.md @@ -0,0 +1,132 @@ +--- +title: "Structured outputs with Requesty, a complete guide with instructor" +description: "Learn how to use Instructor with Requesty to access multiple LLM providers through a unified, OpenAI-compatible API. Get type-safe, structured outputs from various models." +--- + +# Structured outputs with Requesty, a complete guide with instructor + +[Requesty](https://requesty.ai) provides a unified, OpenAI-compatible API to access 400+ LLM models through a single endpoint, with smart routing, fallbacks, caching, and cost tracking. This guide shows you how to use Instructor with Requesty for type-safe, validated responses across various LLM providers. + +## Quick Start + +Instructor works with Requesty through the OpenAI client, so you don't need to install anything extra beyond the base package. + +```bash +pip install instructor +``` + +You'll need a Requesty API key, which you can create at [app.requesty.ai/api-keys](https://app.requesty.ai/api-keys). Set it as the `REQUESTY_API_KEY` environment variable. + +```bash +export REQUESTY_API_KEY="your-api-key" +``` + +Requesty uses `provider/model` naming (e.g. `openai/gpt-4o-mini`, `anthropic/claude-sonnet-4-5`, `google/gemini-2.5-flash`). + +## Simple User Example (Sync) + +```python +import instructor +from pydantic import BaseModel + + +class User(BaseModel): + name: str + age: int + + +client = instructor.from_provider("requesty/openai/gpt-4o-mini") + +resp = client.chat.completions.create( + messages=[ + { + "role": "user", + "content": "Ivan is 28 years old", + }, + ], + response_model=User, +) + +print(resp) +#> name='Ivan' age=28 +``` + +## Simple User Example (Async) + +```python +import asyncio +import instructor +from pydantic import BaseModel + + +class User(BaseModel): + name: str + age: int + + +client = instructor.from_provider("requesty/openai/gpt-4o-mini", async_client=True) + + +async def extract_user(): + return await client.chat.completions.create( + messages=[ + { + "role": "user", + "content": "Ivan is 28 years old", + }, + ], + response_model=User, + ) + + +user = asyncio.run(extract_user()) +print(user) +#> name='Ivan' age=28 +``` + +## Using the OpenAI client directly + +You can also point the OpenAI client at Requesty explicitly: + +```python +from openai import OpenAI +import instructor +from pydantic import BaseModel + + +class User(BaseModel): + name: str + age: int + + +client = instructor.from_openai( + OpenAI( + base_url="https://router.requesty.ai/v1", + api_key="", + ) +) + +resp = client.chat.completions.create( + messages=[{"role": "user", "content": "Ivan is 28 years old"}], + response_model=User, +) + +print(resp) +#> name='Ivan' age=28 +``` + +## Supported Modes + +Requesty supports the standard OpenAI-compatible modes: + +- `Mode.TOOLS` — tool/function calling (default) +- `Mode.JSON` — JSON output +- `Mode.MD_JSON` — Markdown-fenced JSON +- `Mode.PARALLEL_TOOLS` — parallel tool calls + +## Related Documentation + +- [OpenAI](./openai.md): The base OpenAI integration +- [OpenRouter](./openrouter.md): Another unified LLM router + +The [Requesty models listing](https://app.requesty.ai/router/list) shows the full catalogue. Keep in mind that some models do not support `json_schema` response formats. diff --git a/examples/open_source_examples/requesty.py b/examples/open_source_examples/requesty.py new file mode 100644 index 000000000..ef6b8609c --- /dev/null +++ b/examples/open_source_examples/requesty.py @@ -0,0 +1,42 @@ +import os +import instructor +from pydantic import BaseModel, Field +from typing import Optional +from instructor import Maybe + +# Requesty (https://requesty.ai) is an OpenAI-compatible LLM router. +# Set your API key: export REQUESTY_API_KEY="your-api-key" +assert os.environ.get("REQUESTY_API_KEY"), ( + "REQUESTY_API_KEY is not set in environment variables" +) + +# Requesty uses provider/model naming, e.g. openai/gpt-4o-mini +client = instructor.from_provider("requesty/openai/gpt-4o-mini") + +data = [ + "Brandon is 33 years old. He works as a solution architect.", + "Jason is 25 years old. He is the GOAT.", + "Dominic is 45 years old. He is retired.", + "Jenny is 72. She is a wife and a CEO.", + "Holly is 22. She is an explorer.", +] + + +class UserDetail(BaseModel): + age: int + name: str + occupation: Optional[str] = Field( + default=None, description="The person's occupation, if mentioned" + ) + + +MaybeUser = Maybe(UserDetail) + +for text in data: + user = client.chat.completions.create( + response_model=MaybeUser, + messages=[ + {"role": "user", "content": f"Extract the user details: {text}"}, + ], + ) + print(user) diff --git a/instructor/__init__.py b/instructor/__init__.py index b23ee50b0..0ac18b48d 100644 --- a/instructor/__init__.py +++ b/instructor/__init__.py @@ -57,6 +57,7 @@ from_together as from_together, ) from .v2.providers.openrouter.client import from_openrouter as from_openrouter + from .v2.providers.requesty.client import from_requesty as from_requesty from .v2.providers.perplexity.client import from_perplexity as from_perplexity from .v2.providers.vertexai.client import from_vertexai as from_vertexai from .v2.providers.writer.client import from_writer as from_writer @@ -78,6 +79,7 @@ "from_databricks", "from_deepseek", "from_openrouter", + "from_requesty", "from_litellm", "from_vertexai", "from_provider", @@ -117,6 +119,7 @@ "from_databricks": (".v2.providers.openai.client", "from_databricks"), "from_deepseek": (".v2.providers.openai.client", "from_deepseek"), "from_openrouter": (".v2.providers.openrouter.client", "from_openrouter"), + "from_requesty": (".v2.providers.requesty.client", "from_requesty"), "from_litellm": (".v2.providers.litellm.client", "from_litellm"), "Mode": (".mode", "Mode"), "patch": (".core.patch", "patch"), diff --git a/instructor/v2/auto_client.py b/instructor/v2/auto_client.py index 54335e960..214272d12 100644 --- a/instructor/v2/auto_client.py +++ b/instructor/v2/auto_client.py @@ -1536,6 +1536,70 @@ def _build_openrouter( raise +def _build_requesty( + *, + provider: str, + model_name: str, + async_client: bool, + mode: Mode | None, + api_key: str | None, + kwargs: dict[str, Any], + provider_info: dict[str, str], +) -> InstructorType: + try: + import openai + from instructor.v2.providers.requesty.client import from_requesty + import os + + # Get API key from kwargs or environment + api_key = api_key or os.environ.get("REQUESTY_API_KEY") + + if not api_key: + from instructor.v2.core.errors import ConfigurationError + + raise ConfigurationError( + "REQUESTY_API_KEY is not set. " + "Set it with `export REQUESTY_API_KEY=` or pass it as kwarg api_key=" + ) + + # Requesty uses an OpenAI-compatible API + base_url = kwargs.pop("base_url", "https://router.requesty.ai/v1") + + client = ( + openai.AsyncOpenAI(api_key=api_key, base_url=base_url) + if async_client + else openai.OpenAI(api_key=api_key, base_url=base_url) + ) + + result = from_requesty( + client, + model=model_name, + mode=mode if mode else Mode.TOOLS, + **kwargs, + ) + logger.info( + "Client initialized", + extra={**provider_info, "status": "success"}, + ) + return result + except ImportError: + from instructor.v2.core.errors import ConfigurationError + + raise ConfigurationError( + "The openai package is required to use the Requesty provider. " + "Install it with `pip install openai`." + ) from None + except Exception as e: + logger.error( + "Error initializing %s client: %s", + provider, + e, + exc_info=True, + extra={**provider_info, "status": "error"}, + ) + raise + + def _build_litellm( *, provider: str, @@ -1604,5 +1668,6 @@ def _build_litellm( "deepseek": _build_deepseek, "xai": _build_xai, "openrouter": _build_openrouter, + "requesty": _build_requesty, "litellm": _build_litellm, } diff --git a/instructor/v2/core/provider_specs.py b/instructor/v2/core/provider_specs.py index 3bc375fe6..7e31c364e 100644 --- a/instructor/v2/core/provider_specs.py +++ b/instructor/v2/core/provider_specs.py @@ -166,6 +166,26 @@ def _openai_compat_spec( client_module="instructor.v2.providers.openrouter.client", sdk_module="openai", ), + Provider.REQUESTY: _spec( + Provider.REQUESTY, + aliases=("requesty",), + handler_module="instructor.v2.providers.requesty.handlers", + supported_modes=( + Mode.TOOLS, + Mode.JSON_SCHEMA, + Mode.MD_JSON, + Mode.PARALLEL_TOOLS, + ), + unsupported_modes=(Mode.RESPONSES_TOOLS,), + legacy_modes={ + Mode.FUNCTIONS: Mode.TOOLS, + Mode.TOOLS_STRICT: Mode.TOOLS, + Mode.JSON_O1: Mode.JSON_SCHEMA, + }, + from_function="from_requesty", + client_module="instructor.v2.providers.requesty.client", + sdk_module="openai", + ), Provider.ANTHROPIC: _spec( Provider.ANTHROPIC, aliases=("anthropic",), diff --git a/instructor/v2/core/providers.py b/instructor/v2/core/providers.py index 131e3a7e5..755fede5b 100644 --- a/instructor/v2/core/providers.py +++ b/instructor/v2/core/providers.py @@ -36,6 +36,7 @@ class Provider(Enum): BEDROCK = "bedrock" PERPLEXITY = "perplexity" OPENROUTER = "openrouter" + REQUESTY = "requesty" def provider_from_mode(mode: Mode, default: Provider = Provider.OPENAI) -> Provider: @@ -117,6 +118,7 @@ def get_provider(base_url: str) -> Provider: ("localhost:11434", Provider.OLLAMA), ("litellm", Provider.LITELLM), ("openrouter", Provider.OPENROUTER), + ("requesty", Provider.REQUESTY), ("x.ai", Provider.XAI), ("xai", Provider.XAI), ) diff --git a/instructor/v2/providers/openai/handlers.py b/instructor/v2/providers/openai/handlers.py index 364b90948..a91132ff8 100644 --- a/instructor/v2/providers/openai/handlers.py +++ b/instructor/v2/providers/openai/handlers.py @@ -60,6 +60,7 @@ Provider.DATABRICKS, Provider.DEEPSEEK, Provider.OPENROUTER, + Provider.REQUESTY, Provider.GROQ, Provider.FIREWORKS, Provider.CEREBRAS, @@ -72,6 +73,7 @@ Provider.DATABRICKS, Provider.DEEPSEEK, Provider.OPENROUTER, + Provider.REQUESTY, Provider.CEREBRAS, ] diff --git a/instructor/v2/providers/requesty/__init__.py b/instructor/v2/providers/requesty/__init__.py new file mode 100644 index 000000000..4134e6d3f --- /dev/null +++ b/instructor/v2/providers/requesty/__init__.py @@ -0,0 +1,6 @@ +"""Requesty v2 provider handlers and client.""" + +from .client import from_requesty +from .handlers import RequestyJSONSchemaHandler + +__all__ = ["RequestyJSONSchemaHandler", "from_requesty"] diff --git a/instructor/v2/providers/requesty/client.py b/instructor/v2/providers/requesty/client.py new file mode 100644 index 000000000..89659fb0e --- /dev/null +++ b/instructor/v2/providers/requesty/client.py @@ -0,0 +1,51 @@ +"""v2 Requesty client factory.""" + +from __future__ import annotations + +from typing import Any, overload + +import openai + +from instructor.v2.core.client import AsyncInstructor, Instructor +from instructor.v2.core.mode import Mode +from instructor.v2.core.providers import Provider +from instructor.v2.providers.openai.client import _from_openai_compat + +# Ensure Requesty handlers are registered (overrides JSON_SCHEMA). +from instructor.v2.providers.requesty import handlers # noqa: F401 + + +@overload +def from_requesty( + client: openai.OpenAI, + mode: Mode = Mode.TOOLS, + model: str | None = None, + **kwargs: Any, +) -> Instructor: ... + + +@overload +def from_requesty( + client: openai.AsyncOpenAI, + mode: Mode = Mode.TOOLS, + model: str | None = None, + **kwargs: Any, +) -> AsyncInstructor: ... + + +def from_requesty( + client: openai.OpenAI | openai.AsyncOpenAI, + mode: Mode = Mode.TOOLS, + model: str | None = None, + **kwargs: Any, +) -> Instructor | AsyncInstructor: + return _from_openai_compat( + client=client, + provider=Provider.REQUESTY, + mode=mode, + model=model, + **kwargs, + ) + + +__all__ = ["from_requesty"] diff --git a/instructor/v2/providers/requesty/handlers.py b/instructor/v2/providers/requesty/handlers.py new file mode 100644 index 000000000..ae63396cd --- /dev/null +++ b/instructor/v2/providers/requesty/handlers.py @@ -0,0 +1,48 @@ +"""Requesty v2 mode handlers.""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel + +from instructor.v2.core.mode import Mode +from instructor.v2.core.providers import Provider +from instructor.v2.core.decorators import register_mode_handler + +# Register OpenAI-compatible handlers (TOOLS, MD_JSON, PARALLEL_TOOLS) for Requesty. +from instructor.v2.providers.openai import handlers as _openai_handlers # noqa: F401 +from instructor.v2.providers.openai.handlers import ( + OpenAIJSONSchemaHandler, + handle_openrouter_structured_outputs, + reask_default, +) + + +@register_mode_handler(Provider.REQUESTY, Mode.JSON_SCHEMA) +class RequestyJSONSchemaHandler(OpenAIJSONSchemaHandler): + """Handler for Requesty structured outputs.""" + + mode = Mode.JSON_SCHEMA + + def prepare_request( + self, + response_model: type[BaseModel] | None, + kwargs: dict[str, Any], + ) -> tuple[type[BaseModel] | None, dict[str, Any]]: + if response_model is None: + return None, kwargs + new_kwargs = kwargs.copy() + new_kwargs["messages"] = list(kwargs.get("messages", [])) + return handle_openrouter_structured_outputs(response_model, new_kwargs) + + def handle_reask( + self, + kwargs: dict[str, Any], + response: Any, + exception: Exception, + ) -> dict[str, Any]: + return reask_default(kwargs, response, exception) + + +__all__ = ["RequestyJSONSchemaHandler"] diff --git a/mkdocs.yml b/mkdocs.yml index 8487fdac3..62a6e012e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -194,6 +194,7 @@ nav: - Perplexity: 'integrations/perplexity.md' - Writer: 'integrations/writer.md' - OpenRouter: 'integrations/openrouter.md' + - Requesty: 'integrations/requesty.md' - SambaNova: 'integrations/sambanova.md' - TrueFoundry: 'integrations/truefoundry.md' - Cookbook: