Files
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

341 lines
11 KiB
Python

"""
Comprehensive parametrized tests for all provider modes.
Tests all registered modes for each provider with actual API calls to ensure complete coverage.
"""
from __future__ import annotations
import os
from collections.abc import Iterable
from typing import Literal, Union
import importlib.util
from pathlib import Path
import pytest
from pydantic import BaseModel
import instructor
from instructor.core.exceptions import InstructorRetryException
from instructor import Mode
from instructor.v2 import Provider, mode_registry
from tests.v2.provider_matrix import legacy_config_dicts
# Ensure handlers are loaded by dynamically importing them
_PROJECT_ROOT = Path(__file__).resolve().parents[2]
_HANDLER_MODULE_PATHS: dict[Provider, Path] = {
Provider.OPENAI: _PROJECT_ROOT / "instructor/v2/providers/openai/handlers.py",
Provider.ANTHROPIC: _PROJECT_ROOT / "instructor/v2/providers/anthropic/handlers.py",
Provider.GENAI: _PROJECT_ROOT / "instructor/v2/providers/genai/handlers.py",
Provider.COHERE: _PROJECT_ROOT / "instructor/v2/providers/cohere/handlers.py",
Provider.XAI: _PROJECT_ROOT / "instructor/v2/providers/xai/handlers.py",
Provider.GROQ: _PROJECT_ROOT / "instructor/v2/providers/openai/handlers.py",
Provider.MISTRAL: _PROJECT_ROOT / "instructor/v2/providers/mistral/handlers.py",
Provider.FIREWORKS: _PROJECT_ROOT / "instructor/v2/providers/openai/handlers.py",
Provider.BEDROCK: _PROJECT_ROOT / "instructor/v2/providers/bedrock/handlers.py",
Provider.CEREBRAS: _PROJECT_ROOT / "instructor/v2/providers/openai/handlers.py",
Provider.WRITER: _PROJECT_ROOT / "instructor/v2/providers/writer/handlers.py",
}
_HANDLERS_LOADED: set[Provider] = set()
def _clear_proxy_env(monkeypatch: pytest.MonkeyPatch) -> None:
for key in (
"ALL_PROXY",
"all_proxy",
"HTTPS_PROXY",
"https_proxy",
"HTTP_PROXY",
"http_proxy",
):
monkeypatch.delenv(key, raising=False)
def _is_expected_missing_dependency(provider: Provider, exc: ImportError) -> bool:
"""Return True when handler loading failed only because an optional SDK is absent."""
sdk_module = PROVIDER_CONFIGS.get(provider, {}).get("sdk_module")
if not sdk_module:
return False
expected_root = str(sdk_module).split(".")[0]
missing_name = getattr(exc, "name", None)
if missing_name:
return missing_name.split(".")[0] == expected_root
return f"No module named '{expected_root}'" in str(exc)
def _ensure_handlers_loaded(provider: Provider) -> None:
"""Dynamically load handler module to ensure handlers are registered."""
if provider in _HANDLERS_LOADED:
return
handler_path = _HANDLER_MODULE_PATHS.get(provider)
if handler_path is None:
return
if not handler_path.exists():
return
try:
spec = importlib.util.spec_from_file_location(
f"tests.v2.handlers_{provider.value}",
handler_path,
)
if spec is None or spec.loader is None:
return
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
_HANDLERS_LOADED.add(provider)
except (ImportError, ModuleNotFoundError) as exc:
if _is_expected_missing_dependency(provider, exc):
pytest.skip(
f"{provider.value} handlers require optional dependency " # ty: ignore[too-many-positional-arguments]
f"{PROVIDER_CONFIGS[provider]['sdk_module']}"
)
raise
class Answer(BaseModel):
"""Simple answer model."""
answer: float
class Weather(BaseModel):
"""Weather tool."""
location: str
units: Literal["imperial", "metric"]
class GoogleSearch(BaseModel):
"""Search tool."""
query: str
_PROVIDER_CLIENT_CONFIGS = legacy_config_dicts()
PROVIDER_CONFIGS = {
provider: {
"provider_string": config["provider_string"],
"sdk_module": config["sdk_module"],
"modes": config["supported_modes"],
"basic_modes": config["basic_modes"],
"async_modes": config["async_modes"],
}
for provider, config in _PROVIDER_CLIENT_CONFIGS.items()
if config["provider_string"] is not None and config["basic_modes"]
}
def _get_all_mode_params():
"""Generate (provider, mode) tuples for all registered modes."""
params = []
for provider, config in PROVIDER_CONFIGS.items():
for mode in config["modes"]:
params.append((provider, mode))
return params
@pytest.mark.parametrize("provider,mode", _get_all_mode_params())
def test_mode_is_registered(provider: Provider, mode: Mode):
"""Verify each mode is registered in the v2 registry."""
_ensure_handlers_loaded(provider)
# Skip if handler module doesn't exist or isn't registered
if not mode_registry.is_registered(provider, mode):
# fmt: off
pytest.skip(f"Mode {mode.value} not registered for {provider.value}") # ty: ignore[too-many-positional-arguments]
# fmt: on
handlers = mode_registry.get_handlers(provider, mode)
assert handlers.request_handler is not None
assert handlers.reask_handler is not None
assert handlers.response_parser is not None
def _get_basic_mode_params():
"""Generate (provider, mode) tuples for basic extraction tests."""
params = []
for provider, config in PROVIDER_CONFIGS.items():
for mode in config["basic_modes"]:
params.append((provider, mode))
return params
def _skip_on_provider_quota(provider: Provider, exc: Exception) -> None:
"""Skip tests when provider quota limits prevent execution."""
if (
provider == Provider.GENAI
and isinstance(exc, InstructorRetryException)
and "RESOURCE_EXHAUSTED" in str(exc)
):
# fmt: off
pytest.skip("GenAI quota exhausted for this environment") # ty: ignore[too-many-positional-arguments]
# fmt: on
if (
provider == Provider.OPENAI
and isinstance(exc, InstructorRetryException)
and "Connection error" in str(exc)
):
if os.environ.get("CI") or os.environ.get("INSTRUCTOR_STRICT_PROVIDER_TESTS"):
return
# fmt: off
pytest.skip("OpenAI connectivity is unavailable in this environment") # ty: ignore[too-many-positional-arguments]
# fmt: on
@pytest.mark.parametrize("provider,mode", _get_basic_mode_params())
@pytest.mark.requires_api_key
def test_mode_basic_extraction(
provider: Provider, mode: Mode, monkeypatch: pytest.MonkeyPatch
):
"""Test basic extraction with each mode."""
config = PROVIDER_CONFIGS[provider]
_clear_proxy_env(monkeypatch)
# All providers now use from_provider()
client = instructor.from_provider(
config["provider_string"],
mode=mode,
)
try:
response = client.chat.completions.create(
response_model=Answer,
messages=[
{
"role": "user",
"content": "What is 2 + 2? Reply with a number.",
},
],
max_tokens=1000,
)
except InstructorRetryException as exc:
_skip_on_provider_quota(provider, exc)
raise
assert isinstance(response, Answer)
assert response.answer == 4.0
def _get_async_mode_params():
"""Generate (provider, mode) tuples for async extraction tests."""
params = []
for provider, config in PROVIDER_CONFIGS.items():
for mode in config["async_modes"]:
params.append((provider, mode))
return params
@pytest.mark.parametrize("provider,mode", _get_async_mode_params())
@pytest.mark.asyncio
@pytest.mark.requires_api_key
async def test_mode_async_extraction(
provider: Provider, mode: Mode, monkeypatch: pytest.MonkeyPatch
):
"""Test async extraction with each mode."""
config = PROVIDER_CONFIGS[provider]
_clear_proxy_env(monkeypatch)
# All providers now use from_provider()
client = instructor.from_provider(
config["provider_string"],
mode=mode,
async_client=True,
)
try:
response = await client.chat.completions.create(
response_model=Answer,
messages=[
{
"role": "user",
"content": "What is 4 + 4? Reply with a number.",
},
],
max_tokens=1000,
)
except InstructorRetryException as exc:
_skip_on_provider_quota(provider, exc)
raise
assert isinstance(response, Answer)
assert response.answer == 8.0
@pytest.mark.provider(Provider.ANTHROPIC)
@pytest.mark.requires_api_key
def test_anthropic_parallel_tools_extraction():
"""Test PARALLEL_TOOLS mode extraction (Anthropic-specific)."""
client = instructor.from_provider(
"anthropic/claude-sonnet-4-6",
mode=Mode.PARALLEL_TOOLS,
)
response = client.chat.completions.create(
response_model=Iterable[Union[Weather, GoogleSearch]],
messages=[
{
"role": "system",
"content": "You must always use tools. Use them simultaneously when appropriate.",
},
{
"role": "user",
"content": "Get weather for San Francisco and search for Python tutorials.",
},
],
max_tokens=1000,
)
result = list(response)
assert len(result) >= 1
assert all(isinstance(r, (Weather, GoogleSearch)) for r in result)
@pytest.mark.parametrize("mode", [Mode.TOOLS])
@pytest.mark.provider(Provider.ANTHROPIC)
@pytest.mark.requires_api_key
def test_anthropic_tools_with_thinking(mode: Mode):
"""Test tools modes with thinking parameter (Anthropic-specific)."""
# Note: Thinking requires Claude 3.7 Sonnet or later
client = instructor.from_provider(
"anthropic/claude-3-7-sonnet-20250219",
mode=mode,
)
# Note: max_tokens must be greater than thinking.budget_tokens
response = client.chat.completions.create(
response_model=Answer,
messages=[
{
"role": "user",
"content": "What is 5 + 5? Reply with a number.",
},
],
max_tokens=2048, # Must be > budget_tokens
thinking={"type": "enabled", "budget_tokens": 1024},
)
assert isinstance(response, Answer)
assert response.answer == 10.0
def test_anthropic_reasoning_tools_normalizes_in_v2():
"""Legacy reasoning mode remains accepted through Anthropic tools mode."""
assert mode_registry.is_registered(
Provider.ANTHROPIC,
Mode.ANTHROPIC_REASONING_TOOLS,
)
@pytest.mark.parametrize("provider", PROVIDER_CONFIGS.keys())
@pytest.mark.requires_api_key
def test_all_modes_covered(provider: Provider):
"""Verify we're testing all registered modes for each provider."""
config = PROVIDER_CONFIGS[provider]
tested_modes = set(config["modes"])
registered_modes = set(mode_registry.get_modes_for_provider(provider))
# All registered modes should be tested
assert tested_modes.issubset(registered_modes), (
f"Tested modes {tested_modes} should be subset of registered modes {registered_modes}"
)