Files
567-labs--instructor/NEW_PROVIDER_AGENT_INSTRUCTIONS.md
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Waiting to run
Test / Core Tests (push) Waiting to run
Test / Offline Coverage Tests (Python 3.10) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.11) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.12) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.13) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.9) (push) Waiting to run
Test / Full Coverage (Python 3.11) (push) Waiting to run
Test / Core Provider Tests (OpenAI) (push) Blocked by required conditions
Test / Core Provider Tests (Anthropic) (push) Blocked by required conditions
Test / Core Provider Tests (Google) (push) Blocked by required conditions
Test / Core Provider Tests (Other) (push) Blocked by required conditions
Test / Anthropic Tests (push) Blocked by required conditions
Test / Gemini Tests (push) Blocked by required conditions
Test / Google GenAI Tests (push) Blocked by required conditions
Test / Vertex AI Tests (push) Blocked by required conditions
Test / OpenAI Tests (push) Blocked by required conditions
Test / Writer Tests (push) Blocked by required conditions
Test / Auto Client Tests (push) Blocked by required conditions
ty / type-check (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

888 lines
25 KiB
Markdown

# AI Agent Instructions: Creating a New Instructor Provider
**Instructions for AI coding agents to create a new provider for the instructor library.**
Copy these instructions to your AI coding agent when you want to add a new LLM provider to instructor. The agent will have everything needed to implement a complete, working provider.
**For human contributors:** See the quick reference template in [`instructor/providers/README.md`](instructor/providers/README.md#adding-a-new-provider)
---
## Mission
Create a complete, production-ready provider package for the instructor library that:
- Follows the BaseProvider protocol exactly
- Includes comprehensive tests using transcript fixtures
- Has proper error handling and validation
- Provides excellent documentation
- Integrates seamlessly with the instructor plugin system
## Prerequisites
Before starting, ensure you have:
- Provider name (e.g., "groq", "perplexity", "fireworks")
- Provider's Python SDK package name and version
- API documentation URL
- Sample API key format (for documentation)
- Knowledge of provider's chat completion API structure
## Step-by-Step Implementation
### Step 1: Project Structure Setup
**Note: This creates a new provider integration that follows instructor's existing patterns, not a separate package.**
Create the following structure in the instructor repository:
```
instructor/providers/{provider}/
├── __init__.py # Empty or basic exports
├── client.py # from_{provider} function implementation
└── utils.py # Provider-specific utilities
tests/llm/test_{provider}/
├── __init__.py # Empty
├── conftest.py # Test configuration & API key handling
├── util.py # Models and modes configuration
├── test_simple.py # Basic functionality tests
├── test_stream.py # Streaming tests (if supported)
├── test_format.py # Format/structure tests
└── test_retries.py # Error handling tests
docs/integrations/
└── {provider}.md # Provider documentation following existing pattern
```
**Important: You're adding to the existing instructor codebase, not creating a separate package.**
### Step 2: Provider Client Implementation
#### File: `instructor/providers/{provider}/client.py`
Follow the exact pattern used by other providers in instructor. This creates a `from_{provider}` function:
```python
from __future__ import annotations
from typing import Any, overload
import instructor
from ...core.client import AsyncInstructor, Instructor
# Import the provider's SDK
from {provider_sdk} import {SyncClient}, {AsyncClient} # Replace with actual imports
@overload
def from_{provider}(
client: {SyncClient},
mode: instructor.Mode = instructor.Mode.{PROVIDER}_TOOLS, # Default mode
**kwargs: Any,
) -> Instructor: ...
@overload
def from_{provider}(
client: {AsyncClient},
mode: instructor.Mode = instructor.Mode.{PROVIDER}_TOOLS, # Default mode
**kwargs: Any,
) -> AsyncInstructor: ...
def from_{provider}(
client: {SyncClient} | {AsyncClient},
mode: instructor.Mode = instructor.Mode.{PROVIDER}_TOOLS, # Default mode
**kwargs: Any,
) -> Instructor | AsyncInstructor:
"""
Create an instructor client from a {Provider} client
Args:
client: {Provider} sync or async client instance
mode: Mode to use for structured outputs
**kwargs: Additional arguments passed to instructor client
Returns:
Instructor or AsyncInstructor instance
"""
# Define valid modes for this provider
valid_modes = {
instructor.Mode.{PROVIDER}_TOOLS,
instructor.Mode.{PROVIDER}_JSON,
# Add other modes your provider supports
}
# Validate mode
if mode not in valid_modes:
from ...core.exceptions import ModeError
raise ModeError(
mode=str(mode),
provider="{Provider}",
valid_modes=[str(m) for m in valid_modes],
)
# Validate client type
if not isinstance(client, ({AsyncClient}, {SyncClient})):
from ...core.exceptions import ClientError
raise ClientError(
f"Client must be an instance of {SyncClient} or {AsyncClient}. "
f"Got: {type(client).__name__}"
)
# Handle async client
if isinstance(client, {AsyncClient}):
async def async_wrapper(*args: Any, **kwargs: Any):
"""Wrapper for async client calls"""
if "stream" in kwargs and kwargs["stream"] is True:
# Handle streaming if supported
return client.chat.completions.acreate(*args, **kwargs)
return await client.chat.completions.acreate(*args, **kwargs)
return AsyncInstructor(
client=client,
create=instructor.patch(create=async_wrapper, mode=mode),
provider=instructor.Provider.{PROVIDER}, # Must be defined in Provider enum
mode=mode,
**kwargs,
)
# Handle sync client
if isinstance(client, {SyncClient}):
return Instructor(
client=client,
create=instructor.patch(create=client.chat.completions.create, mode=mode),
provider=instructor.Provider.{PROVIDER}, # Must be defined in Provider enum
mode=mode,
**kwargs,
)
```
### Step 3: Mode Handlers Implementation
#### File: `instructor_{provider}/handlers.py`
```python
"""
Mode handlers for {Provider} provider
Each handler knows how to:
1. Format requests for the specific mode (TOOLS, JSON, etc.)
2. Parse responses back into Pydantic models
3. Handle provider-specific response formats
"""
from typing import Dict, Any, Type, Union
from pydantic import BaseModel
from instructor.mode import Mode
from instructor.function_calls import openai_schema
import json
class BaseModeHandler:
"""Base class for mode handlers"""
def __init__(self, provider):
self.provider = provider
def prepare_request(
self,
response_model: Type[BaseModel],
messages: list,
model: str,
**kwargs
) -> Dict[str, Any]:
"""Prepare request for this mode"""
raise NotImplementedError
def parse_response(self, response: Any, response_model: Type[BaseModel]) -> BaseModel:
"""Parse provider response into Pydantic model"""
raise NotImplementedError
class ToolsHandler(BaseModeHandler):
"""Handler for function/tool calling mode"""
def prepare_request(self, response_model, messages, model, **kwargs):
# Convert Pydantic model to function schema
schema = openai_schema(response_model)
return {
"model": model,
"messages": messages,
"tools": [{
"type": "function",
"function": schema
}],
"tool_choice": "auto", # or provider-specific equivalent
**kwargs
}
def parse_response(self, response, response_model):
# Extract function call from response
# This is provider-specific - adapt to your provider's response format
if hasattr(response, 'choices') and response.choices:
choice = response.choices[0]
if hasattr(choice.message, 'tool_calls') and choice.message.tool_calls:
tool_call = choice.message.tool_calls[0]
function_args = json.loads(tool_call.function.arguments)
return response_model(**function_args)
raise ValueError("No valid tool call found in response")
class JSONHandler(BaseModeHandler):
"""Handler for JSON mode responses"""
def prepare_request(self, response_model, messages, model, **kwargs):
# Add JSON schema to system message
schema_prompt = f"""
You must respond with valid JSON that matches this schema:
{response_model.model_json_schema()}
Respond with only the JSON, no additional text.
"""
# Add schema to messages
enhanced_messages = [
{"role": "system", "content": schema_prompt}
] + messages
return {
"model": model,
"messages": enhanced_messages,
"response_format": {"type": "json_object"}, # if provider supports
**kwargs
}
def parse_response(self, response, response_model):
# Extract JSON from response content
if hasattr(response, 'choices') and response.choices:
content = response.choices[0].message.content
try:
data = json.loads(content)
return response_model(**data)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in response: {e}")
raise ValueError("No valid response content found")
# Handler registry
_HANDLERS = {
Mode.TOOLS: ToolsHandler,
Mode.JSON: JSONHandler,
# Add other modes as supported by provider
}
def get_handler(mode: Mode, provider) -> BaseModeHandler:
"""Get handler instance for the specified mode"""
if mode not in _HANDLERS:
supported = ", ".join(h.name for h in _HANDLERS.keys())
raise ValueError(f"Mode {mode} not supported. Supported modes: {supported}")
handler_class = _HANDLERS[mode]
return handler_class(provider)
```
### Step 4: Package Configuration
#### File: `pyproject.toml`
```toml
[project]
name = "instructor-{provider}"
version = "0.1.0"
description = "Instructor provider for {Provider Name}"
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
license = {text = "MIT"}
requires-python = ">=3.9"
dependencies = [
"instructor-core>=2.0.0,<3.0.0",
"{provider_sdk}>=X.X.X,<Y.0.0", # Replace with actual version constraints
"pydantic>=2.8.0,<3.0.0",
]
readme = "README.md"
keywords = ["instructor", "llm", "structured-output", "{provider}"]
[project.urls]
Homepage = "https://github.com/instructor-ai/instructor"
Documentation = "https://python.useinstructor.com"
Repository = "https://github.com/instructor-ai/instructor"
[project.optional-dependencies]
dev = [
"pytest>=8.3.3,<9.0.0",
"pytest-asyncio>=0.24.0,<1.0.0",
"pytest-mock>=3.12.0",
"responses>=0.24.0", # For HTTP mocking
"python-dotenv>=1.0.1",
]
# Register the provider with instructor's plugin system
[project.entry-points."instructor.providers"]
{provider} = "instructor_{provider}:{Provider}Provider"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"unit: Unit tests (fast, no external dependencies)",
"integration: Integration tests (may require API keys)",
"live: Live API tests (requires valid API key)"
]
[tool.ruff]
target-version = "py39"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "B", "A", "C4", "T20"]
ignore = ["E501"] # Line too long (handled by formatter)
```
### Step 3: Testing Implementation
#### File: `tests/llm/test_{provider}/conftest.py`
Follow the exact pattern used by all other providers:
```python
import os
import pytest
# Skip entire test suite if API key is missing
if not os.getenv("{PROVIDER}_API_KEY"):
pytest.skip(
"{PROVIDER}_API_KEY environment variable not set",
allow_module_level=True,
)
# Skip if provider package is not installed
try:
from {provider_sdk} import {SyncClient}, {AsyncClient} # Replace with actual imports
except ImportError:
pytest.skip("{provider_sdk} package is not installed", allow_module_level=True)
@pytest.fixture(scope="function")
def client():
"""Sync client fixture"""
yield {SyncClient}()
@pytest.fixture(scope="function")
def aclient():
"""Async client fixture"""
yield {AsyncClient}()
```
#### File: `tests/llm/test_{provider}/util.py`
Define supported models and modes:
```python
import instructor
# Replace with actual model names your provider supports
models = ["provider-model-name-1", "provider-model-name-2"]
# Replace with actual modes your provider supports
modes = [
instructor.Mode.{PROVIDER}_TOOLS,
instructor.Mode.{PROVIDER}_JSON,
]
```
#### File: `tests/llm/test_{provider}/test_simple.py`
Follow the standard pattern for basic functionality tests:
```python
import instructor
from {provider_sdk} import {SyncClient}, {AsyncClient} # Replace with actual imports
from pydantic import BaseModel, field_validator
import pytest
from itertools import product
from .util import models, modes
class User(BaseModel):
"""Standard test model"""
name: str
age: int
@pytest.mark.parametrize("model, mode", product(models, modes))
def test_{provider}_sync(model: str, mode: instructor.Mode, client):
"""Test basic sync functionality"""
client = instructor.from_{provider}(client, mode=mode)
resp = client.chat.completions.create(
model=model,
messages=[
{
"role": "user",
"content": "Extract a user from this sentence: Ivan is 27 and lives in Singapore",
},
],
response_model=User,
)
assert resp.name.lower() == "ivan"
assert resp.age == 27
@pytest.mark.parametrize("model, mode", product(models, modes))
def test_{provider}_sync_validated(model: str, mode: instructor.Mode, client):
"""Test sync with validation retries"""
class ValidatedUser(BaseModel):
name: str
age: int
@field_validator("name")
def name_validator(cls, v: str) -> str:
if not v.isupper():
raise ValueError(
f"All letters in the name must be uppercase (Eg. JOHN, SMITH) - {v} is not a valid example."
)
return v
client = instructor.from_{provider}(client, mode=mode)
resp = client.chat.completions.create(
model=model,
messages=[
{
"role": "user",
"content": "Extract a user from this sentence: Ivan is 27 and lives in Singapore",
},
],
max_retries=5,
response_model=ValidatedUser,
)
assert resp.name == "IVAN"
assert resp.age == 27
@pytest.mark.parametrize("model, mode", product(models, modes))
@pytest.mark.asyncio(scope="session")
async def test_{provider}_async(model: str, mode: instructor.Mode, aclient):
"""Test async functionality"""
client = instructor.from_{provider}(aclient, mode=mode)
resp = await client.chat.completions.create(
model=model,
messages=[
{
"role": "user",
"content": "Extract a user from this sentence: Ivan is 27 and lives in Singapore",
},
],
response_model=User,
)
assert resp.name.lower() == "ivan"
assert resp.age == 27
@pytest.mark.parametrize("model, mode", product(models, modes))
@pytest.mark.asyncio(scope="session")
async def test_{provider}_async_validated(model: str, mode: instructor.Mode, aclient):
"""Test async with validation retries"""
class ValidatedUser(BaseModel):
name: str
age: int
@field_validator("name")
def name_validator(cls, v: str) -> str:
if not v.isupper():
raise ValueError(
f"Make sure to uppercase all letters in the name field. Examples include: JOHN, SMITH, etc. {v} is not a valid example."
)
return v
client = instructor.from_{provider}(aclient, mode=mode)
resp = await client.chat.completions.create(
model=model,
messages=[
{
"role": "user",
"content": "Extract a user from this sentence: Ivan is 27 and lives in Singapore",
},
],
response_model=ValidatedUser,
max_retries=5,
)
assert resp.name == "IVAN"
assert resp.age == 27
```
### Step 4: Required Infrastructure Updates
#### A. Add Mode Constants
Add your provider's modes to `instructor/mode.py`:
```python
# Add to the Mode enum class
{PROVIDER}_TOOLS = "{provider}_tools"
{PROVIDER}_JSON = "{provider}_json"
# Add other modes as needed
```
#### B. Add Provider to Enum
Add your provider to `instructor/utils/providers.py`:
```python
# Add to the Provider enum
{PROVIDER} = "{provider}"
```
#### C. Update Main __init__.py
Add conditional import to `instructor/__init__.py`:
```python
# Add this block with the other provider imports
if importlib.util.find_spec("{provider_sdk}") is not None:
from .providers.{provider}.client import from_{provider}
__all__ += ["from_{provider}"]
```
#### D. Add to pyproject.toml
Add your provider to the optional dependencies:
```toml
# In [project.optional-dependencies]
{provider} = ["{provider_sdk}>=X.X.X,<Y.0.0"] # Replace with actual version
# In [dependency-groups]
{provider} = ["{provider_sdk}>=X.X.X,<Y.0.0"]
```
### Step 5: Documentation
#### File: `docs/integrations/{provider}.md`
Follow the exact pattern of existing provider docs:
```markdown
---
title: "Structured outputs with {Provider}, a complete guide w/ instructor"
description: "Complete guide to using Instructor with {Provider} models. Learn how to generate structured, type-safe outputs with {provider description}."
---
# Structured outputs with {Provider}, a complete guide w/ instructor
{Provider description and benefits}. This guide shows you how to use Instructor with {Provider}'s models for type-safe, validated responses.
## Quick Start
Install Instructor with {Provider} support:
```bash
pip install "instructor[{provider}]"
```
## Simple User Example (Sync)
```python
from {provider_sdk} import {SyncClient}
import instructor
from pydantic import BaseModel
# Initialize the client
client = {SyncClient}()
# Enable instructor patches
client = instructor.from_{provider}(client)
class User(BaseModel):
name: str
age: int
# Extract structured data
user = client.chat.completions.create(
model="your-model-name",
messages=[{"role": "user", "content": "Extract: Jason is 25 years old"}],
response_model=User
)
print(user.name) # Jason
print(user.age) # 25
```
## Simple User Example (Async)
```python
from {provider_sdk} import {AsyncClient}
import instructor
from pydantic import BaseModel
import asyncio
# Initialize async client
client = {AsyncClient}()
# Enable instructor patches
client = instructor.from_{provider}(client)
class User(BaseModel):
name: str
age: int
async def extract_user():
user = await client.chat.completions.create(
model="your-model-name",
messages=[{"role": "user", "content": "Extract: Jason is 25 years old"}],
response_model=User
)
return user
# Run async function
user = asyncio.run(extract_user())
print(user.name) # Jason
print(user.age) # 25
```
## Supported Models
- `model-1` - Description and capabilities
- `model-2` - Description and capabilities
Check [{Provider} documentation](provider-docs-url) for the complete list of available models.
## Modes
The {Provider} provider supports these modes:
- `instructor.Mode.{PROVIDER}_TOOLS` - Uses {provider} function calling (recommended)
- `instructor.Mode.{PROVIDER}_JSON` - Uses JSON mode responses
```python
client = instructor.from_{provider}(client, mode=instructor.Mode.{PROVIDER}_TOOLS)
```
## Advanced Usage
### Validation and Retries
```python
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
age: int
@field_validator('age')
def validate_age(cls, v):
if v < 0:
raise ValueError('Age must be positive')
return v
# Automatic retries on validation errors
user = client.chat.completions.create(
model="your-model-name",
messages=[{"role": "user", "content": "Extract: Jason is -5 years old"}],
response_model=User,
max_retries=3
)
```
### Complex Nested Models
```python
from typing import List
class Address(BaseModel):
street: str
city: str
country: str
class User(BaseModel):
name: str
age: int
addresses: List[Address]
users = client.chat.completions.create(
model="your-model-name",
messages=[{"role": "user", "content": "Extract user info with multiple addresses..."}],
response_model=User
)
```
## Migration from Other Providers
If you're migrating from another provider:
```python
# Old way (other provider)
# client = instructor.from_openai(openai_client)
# New way ({Provider})
client = instructor.from_{provider}({provider_sdk}.{SyncClient}())
```
## API Reference
For detailed API documentation, see the [Instructor API reference](../api/index.md).
```
## Example Provider: Groq
Here's a concrete example implementing a Groq provider:
#### File: `instructor/providers/groq/client.py`
```python
from __future__ import annotations
from typing import Any, overload
import instructor
from ...core.client import AsyncInstructor, Instructor
from groq import Groq, AsyncGroq
@overload
def from_groq(
client: Groq,
mode: instructor.Mode = instructor.Mode.GROQ_TOOLS,
**kwargs: Any,
) -> Instructor: ...
@overload
def from_groq(
client: AsyncGroq,
mode: instructor.Mode = instructor.Mode.GROQ_TOOLS,
**kwargs: Any,
) -> AsyncInstructor: ...
def from_groq(
client: Groq | AsyncGroq,
mode: instructor.Mode = instructor.Mode.GROQ_TOOLS,
**kwargs: Any,
) -> Instructor | AsyncInstructor:
valid_modes = {
instructor.Mode.GROQ_TOOLS,
instructor.Mode.GROQ_JSON,
}
if mode not in valid_modes:
from ...core.exceptions import ModeError
raise ModeError(
mode=str(mode),
provider="Groq",
valid_modes=[str(m) for m in valid_modes],
)
if not isinstance(client, (AsyncGroq, Groq)):
from ...core.exceptions import ClientError
raise ClientError(
f"Client must be an instance of Groq or AsyncGroq. "
f"Got: {type(client).__name__}"
)
if isinstance(client, AsyncGroq):
async def async_wrapper(*args: Any, **kwargs: Any):
return await client.chat.completions.acreate(*args, **kwargs)
return AsyncInstructor(
client=client,
create=instructor.patch(create=async_wrapper, mode=mode),
provider=instructor.Provider.GROQ,
mode=mode,
**kwargs,
)
return Instructor(
client=client,
create=instructor.patch(create=client.chat.completions.create, mode=mode),
provider=instructor.Provider.GROQ,
mode=mode,
**kwargs,
)
```
## Quality Checklist
Before submitting your provider implementation, verify:
### Core Implementation
- [ ] `from_{provider}` function implemented following the exact pattern
- [ ] Both sync and async clients supported with proper overloads
- [ ] Valid modes defined and enforced with proper error messages
- [ ] Client type validation with helpful error messages
- [ ] Proper use of `instructor.patch()` for both sync and async
### Testing
- [ ] `conftest.py` skips tests if API key missing or package not installed
- [ ] `util.py` defines supported models and modes
- [ ] `test_simple.py` covers basic sync/async functionality with validation
- [ ] Tests use parametrized approach with `product(models, modes)`
- [ ] All tests pass with real API key: `pytest tests/llm/test_{provider}/`
### Infrastructure Updates
- [ ] Modes added to `instructor/mode.py`
- [ ] Provider added to `instructor/utils/providers.py` Provider enum
- [ ] Conditional import added to `instructor/__init__.py`
- [ ] Dependencies added to `pyproject.toml` optional-dependencies
- [ ] Dependencies added to `pyproject.toml` dependency-groups
### Documentation
- [ ] Provider documentation created in `docs/integrations/{provider}.md`
- [ ] Follows exact pattern with frontmatter, examples, and sections
- [ ] All code examples are tested and work
- [ ] Covers sync/async usage, validation, nested models
- [ ] Links to provider documentation and API reference
### Integration
- [ ] Works with existing instructor patterns and conventions
- [ ] Error messages are helpful and actionable
- [ ] Follows the same API as other providers
- [ ] No performance regressions
## Submission Process
1. **Test Locally**: Ensure all tests pass and examples work
2. **Create PR**: Submit to instructor repository
3. **Package Registry**: Publish to PyPI as `instructor-{provider}`
4. **Documentation**: Add to instructor docs site
5. **Announcement**: Share with community
## Common Issues & Solutions
### "Provider not found" error
- Check entry point configuration in pyproject.toml
- Verify provider name matches exactly
- Ensure package is installed in same environment
### Validation errors not retrying
- Verify error handling in chat() method catches ValidationError
- Check that validation messages are added to conversation
- Ensure max_retries parameter is respected
### Mode not supported
- Implement handler in handlers.py for the mode
- Add to _HANDLERS registry
- Test with provider's actual API capabilities
### Streaming issues
- Check if provider supports streaming at all
- Implement incremental parsing for partial responses
- Handle stream interruption and reconnection
### Type checking failures
- Ensure all method signatures match BaseProvider protocol exactly
- Add proper type hints for all parameters and returns
- Use Union/Optional types where appropriate
---
**This completes the full provider implementation guide. Follow these instructions systematically and you'll have a production-ready instructor provider that integrates seamlessly with the existing ecosystem.**