Files
567-labs--instructor/tests/coverage/test_auto_client_coverage.py
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

604 lines
21 KiB
Python

from __future__ import annotations
import builtins
import importlib
import logging
from collections.abc import Iterator
from functools import partial
from typing import Any, cast
import pytest
from instructor import AsyncInstructor, Instructor, Mode
from instructor.v2 import auto_client
from instructor.v2.core.errors import ConfigurationError
from tests.coverage.client_cleanup import (
clear_proxy_environment,
close_idle_event_loop,
close_provider_client,
)
@pytest.fixture(autouse=True)
def isolated_provider_environment(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
for name in (
"ANYSCALE_API_KEY",
"TOGETHER_API_KEY",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_ENDPOINT",
"DATABRICKS_TOKEN",
"DATABRICKS_API_KEY",
"DATABRICKS_HOST",
"DATABRICKS_BASE_URL",
"DATABRICKS_WORKSPACE_URL",
"GOOGLE_API_KEY",
"MISTRAL_API_KEY",
"PERPLEXITY_API_KEY",
):
monkeypatch.delenv(name, raising=False)
clear_proxy_environment(monkeypatch)
monkeypatch.setenv("AWS_EC2_METADATA_DISABLED", "true")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "test-access-key")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "test-secret-key")
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
yield
close_idle_event_loop()
REAL_PROVIDER_CASES = [
("openai/gpt-4", {"api_key": "test-key"}, "openai"),
(
"azure_openai/gpt-4",
{"api_key": "test-key", "azure_endpoint": "https://azure.invalid"},
"openai.lib.azure",
),
("anyscale/llama", {"api_key": "test-key"}, "openai"),
("together/llama", {"api_key": "test-key"}, "openai"),
("anthropic/claude", {"api_key": "test-key"}, "anthropic"),
("google/gemini", {"api_key": "test-key"}, "google.genai"),
("mistral/mistral-small", {"api_key": "test-key"}, "mistralai"),
("cohere/command", {"api_key": "test-key"}, "cohere"),
("perplexity/sonar", {"api_key": "test-key"}, "openai"),
("groq/llama", {"api_key": "test-key"}, "groq"),
("writer/palmyra", {"api_key": "test-key"}, "writerai"),
("bedrock/anthropic.claude", {}, "botocore"),
]
@pytest.mark.parametrize("model,kwargs,inner_module", REAL_PROVIDER_CASES)
@pytest.mark.parametrize("async_client", [False, True])
def test_real_provider_clients_build_without_network(
request: pytest.FixtureRequest,
model: str,
kwargs: dict[str, Any],
inner_module: str,
async_client: bool,
) -> None:
client = auto_client.from_provider(model, async_client=async_client, **dict(kwargs))
request.addfinalizer(
partial(close_provider_client, client.client, async_client=async_client)
)
assert type(client) is (AsyncInstructor if async_client else Instructor)
assert type(client.client).__module__.startswith(inner_module)
assert callable(client.create_fn)
def test_openai_forwards_all_constructor_settings_and_custom_mode(
request: pytest.FixtureRequest,
) -> None:
client = auto_client.from_provider(
"openai/gpt-4",
api_key="test-key",
base_url="https://example.invalid/v1",
organization="org-test",
timeout=12,
max_retries="4",
default_headers={"X-Test": "yes"},
default_query={"trace": "on"},
_strict_response_validation=True,
mode=Mode.JSON,
)
request.addfinalizer(partial(close_provider_client, client.client))
assert type(client) is Instructor
assert client.mode is Mode.JSON
assert client.client is not None
assert str(client.client.base_url) == "https://example.invalid/v1/"
assert client.client.organization == "org-test"
assert client.client.max_retries == 4
assert client.client.default_headers["X-Test"] == "yes"
assert client.client.default_query == {"trace": "on"}
def test_azure_reads_environment_and_reports_missing_settings(
monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest
) -> None:
with pytest.raises(ConfigurationError, match="AZURE_OPENAI_API_KEY is not set"):
auto_client.from_provider("azure_openai/gpt-4")
monkeypatch.setenv("AZURE_OPENAI_API_KEY", "test-key")
with pytest.raises(ConfigurationError, match="AZURE_OPENAI_ENDPOINT is not set"):
auto_client.from_provider("azure_openai/gpt-4")
monkeypatch.setenv("AZURE_OPENAI_ENDPOINT", "https://azure.invalid")
client = auto_client.from_provider("azure_openai/gpt-4", api_version="2025-01-01")
request.addfinalizer(partial(close_provider_client, client.client))
assert type(client) is Instructor
assert client.client is not None
assert client.client.api_key == "test-key"
assert client.client._api_version == "2025-01-01"
@pytest.mark.parametrize(
"model,environment_key,default_url",
[
(
"anyscale/llama",
"ANYSCALE_API_KEY",
"https://api.endpoints.anyscale.com/v1/",
),
("together/llama", "TOGETHER_API_KEY", "https://api.together.xyz/v1/"),
],
)
def test_openai_compatible_providers_use_environment_and_custom_url(
monkeypatch: pytest.MonkeyPatch,
request: pytest.FixtureRequest,
model: str,
environment_key: str,
default_url: str,
) -> None:
monkeypatch.setenv(environment_key, "test-key")
default_client = auto_client.from_provider(model)
request.addfinalizer(partial(close_provider_client, default_client.client))
custom_client = auto_client.from_provider(
model, base_url="https://compatible.invalid/v1", async_client=True
)
request.addfinalizer(
partial(close_provider_client, custom_client.client, async_client=True)
)
assert default_client.client is not None
assert custom_client.client is not None
assert str(default_client.client.base_url) == default_url
assert str(custom_client.client.base_url) == "https://compatible.invalid/v1/"
assert custom_client.mode is Mode.TOOLS
def test_anthropic_sets_user_agent_and_default_token_limit(
request: pytest.FixtureRequest,
) -> None:
default_client = auto_client.from_provider("anthropic/claude", api_key="test-key")
request.addfinalizer(partial(close_provider_client, default_client.client))
limited_client = auto_client.from_provider(
"anthropic/claude", api_key="test-key", max_tokens=32, mode=Mode.JSON
)
request.addfinalizer(partial(close_provider_client, limited_client.client))
assert default_client.client is not None
assert default_client.client.default_headers["User-Agent"].startswith("instructor/")
assert default_client.kwargs["max_tokens"] == 4096
assert limited_client.kwargs["max_tokens"] == 32
assert limited_client.mode is Mode.JSON
def test_anthropic_reports_unavailable_factory(monkeypatch: pytest.MonkeyPatch) -> None:
import instructor.v2.providers.anthropic.client as anthropic_client
monkeypatch.setattr(anthropic_client, "from_anthropic", None)
with pytest.raises(ConfigurationError, match="Failed to import Anthropic provider"):
auto_client.from_provider("anthropic/claude", api_key="test-key")
def test_google_extracts_client_options_and_model_override(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import instructor
seen: dict[str, Any] = {}
def capture(client: object, **kwargs: Any) -> dict[str, Any]:
seen["client"] = client
seen["kwargs"] = kwargs
return kwargs
monkeypatch.setattr(instructor, "from_genai", capture)
result = auto_client._build_google(
provider="google",
model_name="gemini",
async_client=False,
mode=Mode.JSON,
api_key="test-key",
kwargs={
"http_options": {"api_version": "v1beta"},
"model": "gemini-override",
"max_tokens": 23,
},
provider_info={"provider": "google", "operation": "initialize"},
)
assert type(seen["client"]).__module__ == "google.genai.client"
assert result == {
"mode": Mode.JSON,
"use_async": False,
"model": "gemini-override",
"max_tokens": 23,
}
class LegacyGemini:
configured: list[str] = []
@classmethod
def configure(cls, *, api_key: str) -> None:
cls.configured.append(api_key)
class GenerativeModel:
def __init__(self, model_name: str) -> None:
self.model_name = model_name
@pytest.mark.parametrize("async_client", [False, True])
def test_legacy_gemini_configures_key_and_forwards_mode(
monkeypatch: pytest.MonkeyPatch, async_client: bool
) -> None:
import instructor.v2.providers.gemini.client as gemini_client
calls: list[tuple[object, dict[str, Any]]] = []
def capture(client: object, **kwargs: Any) -> dict[str, Any]:
calls.append((client, kwargs))
return kwargs
LegacyGemini.configured = []
monkeypatch.setattr(importlib, "import_module", lambda _name: LegacyGemini)
monkeypatch.setattr(gemini_client, "from_gemini", capture)
result = auto_client.from_provider(
"gemini/gemini-pro", api_key="legacy-key", async_client=async_client
)
assert LegacyGemini.configured == ["legacy-key"]
assert isinstance(calls[0][0], LegacyGemini.GenerativeModel)
assert calls[0][0].model_name == "gemini-pro"
assert result == {"mode": Mode.MD_JSON, "use_async": async_client}
def test_legacy_gemini_does_not_configure_an_empty_key(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
import instructor.v2.providers.gemini.client as gemini_client
calls: list[tuple[object, dict[str, Any]]] = []
def capture(client: object, **kwargs: Any) -> dict[str, Any]:
calls.append((client, kwargs))
return kwargs
LegacyGemini.configured = []
monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
monkeypatch.setattr(importlib, "import_module", lambda _name: LegacyGemini)
monkeypatch.setattr(gemini_client, "from_gemini", capture)
with caplog.at_level(logging.DEBUG, logger="instructor.auto_client"):
result = auto_client.from_provider("gemini/gemini-pro", api_key="")
assert LegacyGemini.configured == []
assert isinstance(calls[0][0], LegacyGemini.GenerativeModel)
assert calls[0][0].model_name == "gemini-pro"
assert result == {"mode": Mode.MD_JSON, "use_async": False}
assert "API key provided for gemini provider" not in caplog.text
def test_mistral_requires_key_and_accepts_environment_key(
monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest
) -> None:
with pytest.raises(ValueError, match="MISTRAL_API_KEY is not set"):
auto_client.from_provider("mistral/mistral-small")
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
client = auto_client.from_provider("mistral/mistral-small", async_client=True)
request.addfinalizer(
partial(close_provider_client, client.client, async_client=True)
)
assert type(client) is AsyncInstructor
assert client.client is not None
assert callable(client.create_fn)
@pytest.mark.parametrize(
"model,environment_key",
[
("perplexity/sonar", "PERPLEXITY_API_KEY"),
],
)
def test_compatible_providers_require_keys_and_accept_environment_keys(
monkeypatch: pytest.MonkeyPatch,
request: pytest.FixtureRequest,
model: str,
environment_key: str,
) -> None:
with pytest.raises(ConfigurationError, match=f"{environment_key} is not set"):
auto_client.from_provider(model)
monkeypatch.setenv(environment_key, "test-key")
client = auto_client.from_provider(model, async_client=True)
request.addfinalizer(
partial(close_provider_client, client.client, async_client=True)
)
assert type(client) is AsyncInstructor
assert client.client is not None
assert client.client.api_key == "test-key"
def test_bedrock_forwards_explicit_region_credentials_and_mode(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import boto3
import instructor.v2.providers.bedrock.client as bedrock_client
seen: dict[str, Any] = {}
def create_client(service_name: str, **kwargs: Any) -> object:
seen["service"] = service_name
seen["client_kwargs"] = kwargs
return object()
def create_instructor(client: object, **kwargs: Any) -> dict[str, Any]:
seen["client"] = client
seen["factory_kwargs"] = kwargs
return kwargs
monkeypatch.setattr(boto3, "client", create_client)
monkeypatch.setattr(bedrock_client, "from_bedrock", create_instructor)
result = auto_client.from_provider(
"bedrock/amazon.titan",
region="eu-west-1",
aws_access_key_id="explicit-access",
aws_secret_access_key="explicit-secret",
aws_session_token="explicit-token",
mode=Mode.JSON,
async_client=True,
max_tokens=19,
)
assert seen["service"] == "bedrock-runtime"
assert seen["client_kwargs"] == {
"aws_access_key_id": "explicit-access",
"aws_secret_access_key": "explicit-secret",
"aws_session_token": "explicit-token",
"region_name": "eu-west-1",
}
assert result == {"mode": Mode.JSON, "async_client": True, "max_tokens": 19}
def test_bedrock_reads_all_environment_credentials(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import boto3
import instructor.v2.providers.bedrock.client as bedrock_client
seen: dict[str, Any] = {}
def create_client(_service_name: str, **kwargs: Any) -> object:
seen.update(kwargs)
return object()
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "environment-access")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "environment-secret")
monkeypatch.setenv("AWS_SESSION_TOKEN", "environment-token")
monkeypatch.setenv("AWS_DEFAULT_REGION", "ap-south-1")
monkeypatch.setattr(boto3, "client", create_client)
monkeypatch.setattr(
bedrock_client, "from_bedrock", lambda _client, **kwargs: kwargs
)
result = cast(dict[str, Any], auto_client.from_provider("bedrock/amazon.titan"))
assert seen == {
"aws_access_key_id": "environment-access",
"aws_secret_access_key": "environment-secret",
"aws_session_token": "environment-token",
"region_name": "ap-south-1",
}
assert result["mode"] is Mode.MD_JSON
MISSING_IMPORT_CASES = [
("openai/gpt-4", "openai", "openai package is required"),
("azure_openai/gpt-4", "openai", "Azure OpenAI provider"),
("anyscale/llama", "openai", "anyscale provider"),
("together/llama", "openai", "together provider"),
("databricks/model", "openai", "Databricks provider"),
("anthropic/claude", "anthropic", "Anthropic provider"),
("google/gemini", "google.genai", "Google provider"),
("mistral/model", "mistralai", "Mistral provider"),
("cohere/command", "cohere", "Cohere provider"),
("perplexity/sonar", "openai", "Perplexity provider"),
("groq/llama", "groq", "Groq provider"),
("writer/palmyra", "writerai", "Writer provider"),
("bedrock/model", "boto3", "AWS Bedrock provider"),
]
@pytest.mark.parametrize("model,blocked_import,message", MISSING_IMPORT_CASES)
def test_missing_provider_dependency_has_actionable_error(
monkeypatch: pytest.MonkeyPatch,
model: str,
blocked_import: str,
message: str,
) -> None:
real_import = builtins.__import__
def guarded_import(
name: str,
globals_: dict[str, Any] | None = None,
locals_: dict[str, Any] | None = None,
fromlist: tuple[str, ...] = (),
level: int = 0,
) -> Any:
if name == blocked_import or name.startswith(f"{blocked_import}."):
raise ModuleNotFoundError(
f"No module named '{blocked_import}'", name=blocked_import
)
return real_import(name, globals_, locals_, fromlist, level)
monkeypatch.setattr(builtins, "__import__", guarded_import)
with pytest.raises(ConfigurationError, match=message):
auto_client.from_provider(model, api_key="test-key")
def test_legacy_gemini_missing_dependency_has_actionable_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def missing(_name: str) -> None:
raise ModuleNotFoundError(
"No module named 'google.generativeai'", name="google.generativeai"
)
monkeypatch.setattr(importlib, "import_module", missing)
with pytest.raises(ConfigurationError, match="Gemini provider"):
auto_client.from_provider("gemini/gemini-pro")
def test_openai_does_not_hide_unrelated_import_failure(
monkeypatch: pytest.MonkeyPatch,
) -> None:
real_import = builtins.__import__
def guarded_import(
name: str,
globals_: dict[str, Any] | None = None,
locals_: dict[str, Any] | None = None,
fromlist: tuple[str, ...] = (),
level: int = 0,
) -> Any:
if name == "openai":
raise ModuleNotFoundError("No module named 'unrelated'", name="unrelated")
return real_import(name, globals_, locals_, fromlist, level)
monkeypatch.setattr(builtins, "__import__", guarded_import)
with pytest.raises(ModuleNotFoundError, match="unrelated"):
auto_client.from_provider("openai/gpt-4", api_key="test-key")
FACTORY_FAILURE_CASES = [
("openai/gpt-4", "instructor", "from_openai", {"api_key": "test-key"}),
(
"azure_openai/gpt-4",
"instructor.v2.providers.openai.client",
"from_openai",
{"api_key": "test-key", "azure_endpoint": "https://azure.invalid"},
),
(
"anyscale/llama",
"instructor.v2.providers.openai.client",
"from_anyscale",
{"api_key": "test-key"},
),
(
"databricks/model",
"instructor",
"from_openai",
{"api_key": "test-key", "base_url": "https://workspace.invalid"},
),
(
"anthropic/claude",
"instructor.v2.providers.anthropic.client",
"from_anthropic",
{"api_key": "test-key"},
),
("google/gemini", "instructor", "from_genai", {"api_key": "test-key"}),
(
"mistral/model",
"instructor.v2.providers.mistral.client",
"from_mistral",
{"api_key": "test-key"},
),
(
"cohere/command",
"instructor.v2.providers.cohere.client",
"from_cohere",
{"api_key": "test-key"},
),
(
"perplexity/sonar",
"instructor.v2.providers.perplexity.client",
"from_perplexity",
{"api_key": "test-key"},
),
(
"groq/llama",
"instructor.v2.providers.groq.client",
"from_groq",
{"api_key": "test-key"},
),
(
"writer/palmyra",
"instructor.v2.providers.writer.client",
"from_writer",
{"api_key": "test-key"},
),
(
"bedrock/model",
"instructor.v2.providers.bedrock.client",
"from_bedrock",
{},
),
]
@pytest.mark.parametrize("model,module_name,factory_name,kwargs", FACTORY_FAILURE_CASES)
def test_provider_factory_failures_are_logged_and_propagated(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
model: str,
module_name: str,
factory_name: str,
kwargs: dict[str, Any],
) -> None:
module = importlib.import_module(module_name)
def fail(client: object, **_kwargs: Any) -> None:
close_provider_client(client)
raise RuntimeError("provider factory failed")
monkeypatch.setattr(module, factory_name, fail)
with caplog.at_level(logging.ERROR, logger="instructor.auto_client"):
with pytest.raises(RuntimeError, match="provider factory failed"):
auto_client.from_provider(model, **dict(kwargs))
assert f"Error initializing {model.split('/', 1)[0]} client" in caplog.text
def test_legacy_gemini_factory_failure_is_logged_and_propagated(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
import instructor.v2.providers.gemini.client as gemini_client
def fail(_client: object, **_kwargs: Any) -> None:
raise RuntimeError("legacy Gemini factory failed")
monkeypatch.setattr(importlib, "import_module", lambda _name: LegacyGemini)
monkeypatch.setattr(gemini_client, "from_gemini", fail)
with caplog.at_level(logging.ERROR, logger="instructor.auto_client"):
with pytest.raises(RuntimeError, match="legacy Gemini factory failed"):
auto_client.from_provider("gemini/gemini-pro", api_key="test-key")
assert "Error initializing gemini client" in caplog.text
def test_openai_constructor_failure_is_logged_and_propagated(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.ERROR, logger="instructor.auto_client"):
with pytest.raises(ValueError, match="invalid literal"):
auto_client.from_provider(
"openai/gpt-4", api_key="test-key", max_retries="invalid"
)
assert "Error initializing openai client" in caplog.text