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
604 lines
21 KiB
Python
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
|