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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

---

## [1.15.5] - 2026-06-28
## [Unreleased]

### Added
- **Sarvam**: First-class `from_sarvam()` and `from_provider("sarvam/...")` support for Sarvam's OpenAI-compatible Indic chat API (`sarvam-30b`, `sarvam-105b`).

### Changed
- **Sarvam**: Default structured-output mode is `Mode.MD_JSON`.
---

### Fixed
- **v2 imports**: Defer OpenAI SDK imports from core v2 modules until an OpenAI-specific path actually needs them, reducing import side effects for non-OpenAI usage. ([#2390](https://github.com/567-labs/instructor/pull/2390))
Expand Down
1 change: 1 addition & 0 deletions docs/concepts/from_provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ user = client.create(
- Cohere: `"cohere/command-r-plus"`
- Perplexity: `"perplexity/llama-3.1-sonar"`
- DeepSeek: `"deepseek/deepseek-chat"`
- Sarvam: `"sarvam/sarvam-30b"` (Indic languages — Hindi, Tamil, Bengali, and more)
- xAI: `"xai/grok-beta"`
- OpenRouter: `"openrouter/meta-llama/llama-3.1-70b"`
- Ollama: `"ollama/llama3.2"` (local models)
Expand Down
1 change: 1 addition & 0 deletions docs/integrations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Learn how to integrate Instructor with various AI model providers. These compreh
[:octicons-arrow-right-16: Cohere](./cohere.md) ·
[:octicons-arrow-right-16: Mistral](./mistral.md) ·
[:octicons-arrow-right-16: DeepSeek](./deepseek.md) ·
[:octicons-arrow-right-16: Sarvam](./sarvam.md) ·
[:octicons-arrow-right-16: Together AI](./together.md) ·
[:octicons-arrow-right-16: Groq](./groq.md) ·
[:octicons-arrow-right-16: Fireworks](./fireworks.md) ·
Expand Down
118 changes: 118 additions & 0 deletions docs/integrations/sarvam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: "Structured outputs with Sarvam AI"
description: "Use Instructor with Sarvam Indic LLMs for type-safe structured extraction across Indian languages."
---

# Structured outputs with Sarvam AI

[Sarvam AI](https://www.sarvam.ai/models) provides Indic-focused LLMs such as **Sarvam 30B** and **Sarvam 105B** through an [OpenAI-compatible chat API](https://docs.sarvam.ai/). Instructor now supports Sarvam as a first-class provider via `from_provider("sarvam/...")` and `from_sarvam()`.

## Install

```bash
pip install "instructor[openai]"
```

Set your API key:

```bash
export SARVAM_API_KEY='your-api-key-here'
```

## Quick start

```python
import instructor
from pydantic import BaseModel


class User(BaseModel):
name: str
age: int


client = instructor.from_provider("sarvam/sarvam-30b")

user = client.chat.completions.create(
model="sarvam-30b",
response_model=User,
messages=[{"role": "user", "content": "राहुल की उम्र 28 साल है।"}],
)

print(user)
# > User(name='Rahul', age=28)
```

## Supported models

Chat LLMs exposed via the Sarvam API (see [Sarvam models](https://www.sarvam.ai/models)):

| Model ID | Description |
|----------|-------------|
| `sarvam-30b` | Efficient MoE chat model for high-throughput Indic workloads |
| `sarvam-105b` | Flagship chat model for highest-quality Indic understanding |

Per [Sarvam's model guide](https://docs.sarvam.ai/api-reference-docs/getting-started/models), chat models support **11 languages** (10 Indian + English). `sarvam-m` is deprecated — use `sarvam-30b` or `sarvam-105b`.

Speech (Saaras), TTS (Bulbul), translation (Mayura, Sarvam Translate), and document intelligence (Sarvam Vision) use separate APIs and are not covered by this integration yet. See [Extending Sarvam support](#extending-sarvam-support).

## Modes

Sarvam works with Instructor's OpenAI-compatible modes:

| Mode | When to use |
|------|-------------|
| `Mode.MD_JSON` (default) | Most reliable across Indic scripts in our Jul 2026 benchmark |
| `Mode.TOOLS` | Fastest latency; best on `sarvam-105b`, but unreliable on `sarvam-30b` for extraction |
| `Mode.JSON_SCHEMA` | Native `response_format` JSON schema |

!!! warning "`Mode.TOOLS` on `sarvam-30b`"
In the full 432-trial eval, `sarvam-30b + Mode.TOOLS` scored **29–46%** success on person extraction (vs **58–75%** for `Mode.MD_JSON`). Prefer `Mode.MD_JSON` for cost-sensitive extraction workloads.

```python
import instructor

client = instructor.from_provider(
"sarvam/sarvam-105b",
mode=instructor.Mode.JSON_SCHEMA,
)
```

Sarvam-specific API parameters such as `reasoning_effort` and `wiki_grounding` can be passed through `create()` kwargs.

## Async usage

```python
import asyncio
import instructor
from pydantic import BaseModel


class CityInfo(BaseModel):
city: str
state: str


async def main() -> None:
client = instructor.from_provider("sarvam/sarvam-30b", async_client=True)
result = await client.chat.completions.create(
model="sarvam-30b",
response_model=CityInfo,
messages=[{"role": "user", "content": "Extract city info: Bengaluru is in Karnataka."}],
)
print(result)


asyncio.run(main())
```

## Extending Sarvam support

Future non-chat Sarvam products (Vision, Saaras STT, Bulbul TTS, Translate) would need dedicated provider handlers under `instructor/v2/providers/sarvam/` with separate API endpoints. The current integration is intentionally scoped to chat completions.

## Related resources

- [Sarvam models](https://www.sarvam.ai/models)
- [Sarvam API documentation](https://docs.sarvam.ai/)
- [Instructor modes comparison](../concepts/modes-comparison.md)
- [from_provider guide](../concepts/from_provider.md)
3 changes: 3 additions & 0 deletions instructor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from_anyscale as from_anyscale,
from_databricks as from_databricks,
from_deepseek as from_deepseek,
from_sarvam as from_sarvam,
from_openai as from_openai,
from_together as from_together,
)
Expand All @@ -77,6 +78,7 @@
"from_together",
"from_databricks",
"from_deepseek",
"from_sarvam",
"from_openrouter",
"from_litellm",
"from_vertexai",
Expand Down Expand Up @@ -116,6 +118,7 @@
"from_together": (".v2.providers.openai.client", "from_together"),
"from_databricks": (".v2.providers.openai.client", "from_databricks"),
"from_deepseek": (".v2.providers.openai.client", "from_deepseek"),
"from_sarvam": (".v2.providers.openai.client", "from_sarvam"),
"from_openrouter": (".v2.providers.openrouter.client", "from_openrouter"),
"from_litellm": (".v2.providers.litellm.client", "from_litellm"),
"Mode": (".mode", "Mode"),
Expand Down
3 changes: 3 additions & 0 deletions instructor/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,8 @@
# DeepSeek Models
"deepseek/deepseek-chat",
"deepseek/deepseek-reasoner",
# Sarvam Models - https://docs.sarvam.ai/api-reference-docs/getting-started/models
"sarvam/sarvam-30b",
"sarvam/sarvam-105b",
],
)
2 changes: 1 addition & 1 deletion instructor/v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1142,7 +1142,7 @@ This checklist tracks which providers have been migrated to v2:
- Tests: `tests/v2/test_provider_modes.py`, `tests/v2/test_handlers_parametrized.py`
- Status: ✅ Complete

- [x] **OpenAI-Compatible** (`Provider.ANYSCALE`, `Provider.TOGETHER`, `Provider.DATABRICKS`, `Provider.DEEPSEEK`)
- [x] **OpenAI-Compatible** (`Provider.ANYSCALE`, `Provider.TOGETHER`, `Provider.DATABRICKS`, `Provider.DEEPSEEK`, `Provider.SARVAM`)
- Location: `instructor/v2/providers/openai/`
- Modes: `TOOLS`, `JSON`, `JSON_SCHEMA`, `MD_JSON`, `PARALLEL_TOOLS`
- Tests: `tests/v2/test_handlers_parametrized.py`, `tests/v2/test_client_unified.py`
Expand Down
63 changes: 63 additions & 0 deletions instructor/v2/auto_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,68 @@ def _build_deepseek(
raise


def _build_sarvam(
*,
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.openai.client import from_sarvam
import os

api_key = api_key or os.environ.get("SARVAM_API_KEY")

if not api_key:
from instructor.v2.core.errors import ConfigurationError

raise ConfigurationError(
"SARVAM_API_KEY is not set. "
"Set it with `export SARVAM_API_KEY=<your-api-key>` or pass it as kwarg api_key=<your-api-key>"
)

base_url = kwargs.pop("base_url", "https://api.sarvam.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_sarvam(
client,
model=model_name,
mode=mode if mode else Mode.MD_JSON,
**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 Sarvam 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_xai(
*,
provider: str,
Expand Down Expand Up @@ -1602,6 +1664,7 @@ def _build_litellm(
"generative-ai": _build_generative_ai,
"ollama": _build_ollama,
"deepseek": _build_deepseek,
"sarvam": _build_sarvam,
"xai": _build_xai,
"openrouter": _build_openrouter,
"litellm": _build_litellm,
Expand Down
9 changes: 9 additions & 0 deletions instructor/v2/core/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ class Mode(enum.Enum):
PERPLEXITY_JSON = "perplexity_json"
OPENROUTER_STRUCTURED_OUTPUTS = "openrouter_structured_outputs"

# Sarvam modes
SARVAM_TOOLS = "sarvam_tools"
SARVAM_JSON = "sarvam_json"

# Classification helpers
@classmethod
def tool_modes(cls) -> set["Mode"]:
Expand Down Expand Up @@ -102,6 +106,7 @@ def tool_modes(cls) -> set["Mode"]:
cls.GENAI_TOOLS,
cls.RESPONSES_TOOLS,
cls.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
cls.SARVAM_TOOLS,
}

@classmethod
Expand All @@ -124,6 +129,7 @@ def json_modes(cls) -> set["Mode"]:
cls.OPENROUTER_STRUCTURED_OUTPUTS,
cls.MISTRAL_STRUCTURED_OUTPUTS,
cls.XAI_JSON,
cls.SARVAM_JSON,
}

@classmethod
Expand Down Expand Up @@ -247,6 +253,9 @@ def warn_deprecated_mode(cls, mode: "Mode") -> None:
Mode.GEMINI_JSON: Mode.MD_JSON,
# OpenRouter legacy modes
Mode.OPENROUTER_STRUCTURED_OUTPUTS: Mode.JSON_SCHEMA,
# Sarvam legacy modes
Mode.SARVAM_TOOLS: Mode.TOOLS,
Mode.SARVAM_JSON: Mode.MD_JSON,
}


Expand Down
17 changes: 17 additions & 0 deletions instructor/v2/core/provider_specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,23 @@ def _openai_compat_spec(
alias="deepseek",
from_function="from_deepseek",
),
Provider.SARVAM: _spec(
Provider.SARVAM,
aliases=("sarvam",),
handler_module="instructor.v2.providers.openai.handlers",
supported_modes=(Mode.TOOLS, Mode.JSON, Mode.JSON_SCHEMA, Mode.MD_JSON),
unsupported_modes=(Mode.PARALLEL_TOOLS, Mode.RESPONSES_TOOLS),
legacy_modes={
Mode.SARVAM_TOOLS: Mode.TOOLS,
Mode.SARVAM_JSON: Mode.MD_JSON,
},
from_function="from_sarvam",
client_module="instructor.v2.providers.openai.client",
sdk_module="openai",
provider_string="sarvam/sarvam-30b",
basic_modes=(Mode.TOOLS, Mode.JSON_SCHEMA, Mode.MD_JSON),
async_modes=(Mode.TOOLS, Mode.JSON_SCHEMA, Mode.MD_JSON),
),
Provider.OPENROUTER: _spec(
Provider.OPENROUTER,
aliases=("openrouter",),
Expand Down
4 changes: 4 additions & 0 deletions instructor/v2/core/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Provider(Enum):
BEDROCK = "bedrock"
PERPLEXITY = "perplexity"
OPENROUTER = "openrouter"
SARVAM = "sarvam"


def provider_from_mode(mode: Mode, default: Provider = Provider.OPENAI) -> Provider:
Expand Down Expand Up @@ -69,6 +70,8 @@ def provider_from_mode(mode: Mode, default: Provider = Provider.OPENAI) -> Provi
Mode.BEDROCK_JSON: Provider.BEDROCK,
Mode.PERPLEXITY_JSON: Provider.PERPLEXITY,
Mode.OPENROUTER_STRUCTURED_OUTPUTS: Provider.OPENROUTER,
Mode.SARVAM_TOOLS: Provider.SARVAM,
Mode.SARVAM_JSON: Provider.SARVAM,
}
return mapping.get(mode, default)

Expand Down Expand Up @@ -119,6 +122,7 @@ def get_provider(base_url: str) -> Provider:
("openrouter", Provider.OPENROUTER),
("x.ai", Provider.XAI),
("xai", Provider.XAI),
("sarvam", Provider.SARVAM),
)
for token, provider in providers:
if token in normalized:
Expand Down
Loading