Files
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

224 lines
6.5 KiB
Python

from __future__ import annotations
import json
from types import SimpleNamespace
from typing import Any
import instructor
import pytest
from instructor.cache import (
AutoCache,
BaseCache,
DiskCache,
load_cached_response,
make_cache_key,
store_cached_response,
)
from pydantic import BaseModel
class User(BaseModel):
name: str
def test_cache_key_without_response_model_is_stable_and_schema_independent() -> None:
messages = [{"role": "user", "content": "hello"}]
without_schema = make_cache_key(
messages=messages, model="local", response_model=None, mode="json"
)
repeated = make_cache_key(
messages=messages, model="local", response_model=None, mode="json"
)
with_schema = make_cache_key(
messages=messages, model="local", response_model=User, mode="json"
)
assert without_schema == repeated
assert without_schema != with_schema
class MemoryCache(BaseCache):
def __init__(self) -> None:
self.values: dict[str, Any] = {}
self.writes: list[tuple[str, Any, int | None]] = []
def get(self, key: str) -> Any | None:
return self.values.get(key)
def set(self, key: str, value: Any, ttl: int | None = None) -> None:
self.values[key] = value
self.writes.append((key, value, ttl))
def completion(name: str) -> SimpleNamespace:
return SimpleNamespace(
choices=[
SimpleNamespace(
message=SimpleNamespace(content=User(name=name).model_dump_json()),
finish_reason="stop",
)
],
usage={},
)
def test_auto_cache_rejects_invalid_size_and_evicts_least_recently_used() -> None:
with pytest.raises(ValueError, match="maxsize must be > 0"):
AutoCache(maxsize=0)
cache = AutoCache(maxsize=2)
assert cache.get("missing") is None
cache.set("first", "old")
cache.set("second", "value")
cache.set("first", "updated")
cache.set("third", "value")
assert cache.get("first") == "updated"
assert cache.get("second") is None
assert cache.get("third") == "value"
def test_disk_cache_round_trip_supports_plain_and_ttl_writes(tmp_path: Any) -> None:
cache = DiskCache(directory=str(tmp_path / "responses"))
try:
assert cache.get("missing") is None
cache.set("plain", {"name": "plain"})
cache.set("timed", {"name": "timed"}, ttl=60)
assert cache.get("plain") == {"name": "plain"}
assert cache.get("timed") == {"name": "timed"}
finally:
cache._cache.close()
def test_sync_client_stores_on_miss_and_uses_cache_on_hit() -> None:
calls: list[str] = []
def complete(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
calls.append("called")
return completion("sync")
cache = MemoryCache()
client = instructor.from_litellm(complete, mode=instructor.Mode.JSON)
first = client.create(
response_model=User,
cache=cache,
cache_ttl=30,
messages=[{"role": "user", "content": "hello"}],
model="local",
)
second = client.create(
response_model=User,
cache=cache,
cache_ttl=30,
messages=[{"role": "user", "content": "hello"}],
model="local",
)
assert isinstance(first, User)
assert isinstance(second, User)
assert first.name == second.name == "sync"
assert calls == ["called"]
assert len(cache.writes) == 1
assert cache.writes[0][2] == 30
@pytest.mark.asyncio
async def test_async_client_stores_on_miss_and_uses_cache_on_hit() -> None:
calls: list[str] = []
async def complete(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
calls.append("called")
return completion("async")
cache = MemoryCache()
client = instructor.from_litellm(
complete, mode=instructor.Mode.JSON, async_client=True
)
first = await client.create(
response_model=User,
cache=cache,
cache_ttl=45,
messages=[{"role": "user", "content": "hello"}],
model="local",
)
second = await client.create(
response_model=User,
cache=cache,
cache_ttl=45,
messages=[{"role": "user", "content": "hello"}],
model="local",
)
assert isinstance(first, User)
assert isinstance(second, User)
assert first.name == second.name == "async"
assert calls == ["called"]
assert len(cache.writes) == 1
assert cache.writes[0][2] == 45
def test_load_cached_response_accepts_legacy_model_json() -> None:
cache = AutoCache()
cache.set("legacy", User(name="legacy").model_dump_json())
restored = load_cached_response(cache, "legacy", User)
assert restored == User(name="legacy")
assert not hasattr(restored, "_raw_response")
def test_store_cached_response_without_raw_response_round_trips() -> None:
cache = MemoryCache()
store_cached_response(cache, "model-only", User(name="model-only"), ttl=15)
payload = json.loads(cache.values["model-only"])
restored = load_cached_response(cache, "model-only", User)
assert payload["raw"] is None
assert restored == User(name="model-only")
assert cache.writes[0][2] == 15
def test_pydantic_raw_response_is_restored_as_completion_like_object() -> None:
class RawCompletion(BaseModel):
id: str
choices: list[dict[str, Any]]
cache = AutoCache()
user = User(name="pydantic")
object.__setattr__(
user,
"_raw_response",
RawCompletion(id="chatcmpl-1", choices=[{"message": {"content": "ok"}}]),
)
store_cached_response(cache, "pydantic", user, ttl=10)
restored = load_cached_response(cache, "pydantic", User)
assert isinstance(restored, User)
assert restored.name == "pydantic"
assert restored._raw_response.id == "chatcmpl-1"
assert restored._raw_response.choices[0].message.content == "ok"
def test_unserializable_raw_response_uses_string_fallback(caplog: Any) -> None:
cache = MemoryCache()
raw_response: list[Any] = []
raw_response.append(raw_response)
user = User(name="fallback")
object.__setattr__(user, "_raw_response", raw_response)
store_cached_response(cache, "fallback", user)
payload = json.loads(cache.values["fallback"])
restored = load_cached_response(cache, "fallback", User)
assert payload["raw"] == "[[...]]"
assert isinstance(restored, User)
assert restored.name == "fallback"
assert restored._raw_response == "[[...]]"
assert "Raw response could not be serialized as JSON" in caplog.text