diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cd6396..fa696ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented here. Format follows [Kee ## [Unreleased] +### Added +- Native `anthropic` SDK support. Wraps `Anthropic.messages.create` (including `stream=True`) and `Anthropic.messages.stream(...)` context manager. Same coverage on `AsyncAnthropic` (sync + async variants). +- `extract_anthropic_native` adapter with the full Anthropic field map: `input_tokens`, `output_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens`, `cache_creation.ephemeral_5m_input_tokens`, `cache_creation.ephemeral_1h_input_tokens`, `content[].type == "tool_use"`. +- `anthropic` optional dependency group: `pip install 'lago-agent-sdk[anthropic]'`. +- 19 new unit tests (adapter + wrapper) and 3 live integration tests (gated on `ANTHROPIC_API_KEY`). Total: 256 unit tests, ≥80% coverage maintained. +- 9 captured response fixtures from the real Anthropic API (plain, tool use, 5m + 1h prompt caching, extended thinking, streaming, multi-turn). + + ## [0.1.0] — initial release ### Added diff --git a/README.md b/README.md index 89ad3f9..b8855c7 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ pip install lago-agent-sdk For Bedrock support: `pip install 'lago-agent-sdk[bedrock]'` (adds `boto3`). For Mistral support: `pip install 'lago-agent-sdk[mistral]'` (adds `mistralai`). +For Anthropic native support: `pip install 'lago-agent-sdk[anthropic]'` (adds `anthropic`). ## Quickstart — Bedrock @@ -52,6 +53,25 @@ sdk.flush() The wrapped client behaves identically to the original — same arguments, same return shape, same exceptions. The SDK adds an in-memory queue that batches events to Lago in the background. +## Quickstart — Anthropic + +```python +from anthropic import Anthropic +from lago_agent_sdk import LagoSDK + +sdk = LagoSDK(api_key="...", default_subscription_id="sub_acme") +client = sdk.wrap(Anthropic(api_key="...")) + +resp = client.messages.create( + model="claude-sonnet-4-6", + max_tokens=200, + messages=[{"role": "user", "content": "Hello"}], +) +sdk.flush() +``` + +Works with `Anthropic` and `AsyncAnthropic`. Both `messages.create(..., stream=True)` and the `messages.stream(...)` context manager are instrumented — usage is captured from the final `message_delta` event in either case. + ## Quickstart — Mistral ```python @@ -92,9 +112,9 @@ Backed by `contextvars` for safe propagation across `asyncio` tasks. |---|---|---| | AWS Bedrock | `Converse` (sync + stream) | ✓ | | AWS Bedrock | `InvokeModel` (sync + stream), 7 model families | ✓ | +| Anthropic | native SDK (`messages.create` + `messages.stream`, sync + async) | ✓ | | Mistral | native SDK (`chat.complete` + `chat.stream`) | ✓ | | OpenAI | native SDK | Phase 2 | -| Anthropic | native SDK | Phase 2 | | Google Gemini | native SDK | Phase 2 | | LiteLLM | callback bridge | Phase 4 | @@ -102,16 +122,16 @@ Backed by `contextvars` for safe propagation across `asyncio` tasks. `CanonicalUsage` carries 10 numeric fields. Which ones populate depends on the provider: -| Field | Lago metric code | Bedrock | Mistral native | -|---|---|---|---| -| input | `llm_input_tokens` | ✓ | ✓ | -| output | `llm_output_tokens` | ✓ | ✓ | -| cache_read | `llm_cached_input_tokens` | ✓ (Anthropic) | ✓ (when cache hits) | -| cache_write | `llm_cache_creation_tokens` | ✓ (Anthropic) | ✗ | -| cache_write_5m / 1h | `llm_cache_write_5m/1h_tokens` | ✓ (Anthropic InvokeModel) | ✗ | -| reasoning | `llm_reasoning_tokens` | ✗ (folded into output) | ✗ (folded into output) | -| tool_calls | `llm_tool_calls` | ✓ | ✓ | -| image_input / audio_input | `llm_image/audio_input_tokens` | ✗ | ✗ | +| Field | Lago metric code | Bedrock | Anthropic native | Mistral native | +|---|---|---|---|---| +| input | `llm_input_tokens` | ✓ | ✓ | ✓ | +| output | `llm_output_tokens` | ✓ | ✓ | ✓ | +| cache_read | `llm_cached_input_tokens` | ✓ (Anthropic) | ✓ | ✓ (when cache hits) | +| cache_write | `llm_cache_creation_tokens` | ✓ (Anthropic) | ✓ | ✗ | +| cache_write_5m / 1h | `llm_cache_write_5m/1h_tokens` | ✓ (Anthropic InvokeModel) | ✓ | ✗ | +| reasoning | `llm_reasoning_tokens` | ✗ (folded into output) | ✗ (folded into output, even with extended thinking) | ✗ (folded into output) | +| tool_calls | `llm_tool_calls` | ✓ | ✓ | ✓ | +| image_input / audio_input | `llm_image/audio_input_tokens` | ✗ | ✗ | ✗ | Reasoning, image, and audio fields will populate when Phase 2 native OpenAI ships. diff --git a/pyproject.toml b/pyproject.toml index 8c23f42..4044de0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,9 @@ dev = [ "mypy>=1.10", "types-requests>=2.31", ] +anthropic = [ + "anthropic>=0.30", +] [project.urls] Homepage = "https://www.getlago.com" @@ -81,3 +84,8 @@ files = ["src/lago_agent_sdk"] [[tool.mypy.overrides]] module = ["boto3.*", "botocore.*", "mistralai.*"] ignore_missing_imports = true + +[dependency-groups] +dev = [ + "anthropic>=0.30", +] diff --git a/src/lago_agent_sdk/adapters/__init__.py b/src/lago_agent_sdk/adapters/__init__.py index 31f94e8..217ed3d 100644 --- a/src/lago_agent_sdk/adapters/__init__.py +++ b/src/lago_agent_sdk/adapters/__init__.py @@ -1,8 +1,10 @@ +from .anthropic_native import extract_anthropic_native from .bedrock_converse import extract_bedrock_converse from .bedrock_invoke import extract_bedrock_invoke, pick_invoke_adapter from .mistral_native import extract_mistral_native __all__ = [ + "extract_anthropic_native", "extract_bedrock_converse", "extract_bedrock_invoke", "pick_invoke_adapter", diff --git a/src/lago_agent_sdk/adapters/anthropic_native.py b/src/lago_agent_sdk/adapters/anthropic_native.py new file mode 100644 index 0000000..5943676 --- /dev/null +++ b/src/lago_agent_sdk/adapters/anthropic_native.py @@ -0,0 +1,91 @@ +"""Anthropic native adapter — verified against real fixtures. + +Field mapping: + usage.input_tokens → input + usage.output_tokens → output + usage.cache_read_input_tokens → cache_read + usage.cache_creation_input_tokens → cache_write + usage.cache_creation.ephemeral_5m_input_tokens → cache_write_5m + usage.cache_creation.ephemeral_1h_input_tokens → cache_write_1h + count of content[].type == "tool_use" → tool_calls + +Not exposed by Anthropic (folded into output_tokens): + reasoning_tokens — even with extended thinking enabled + +Unknown usage fields (service_tier, inference_geo, server_tool_use, …) land in extras. +""" + +from __future__ import annotations + +from typing import Any, cast + +from ..canonical import CanonicalUsage + +_KNOWN_USAGE_FIELDS = { + "input_tokens", + "output_tokens", + "cache_read_input_tokens", + "cache_creation_input_tokens", + "cache_creation", +} + + +def _safe_dict(v: Any) -> dict[str, Any]: + return v if isinstance(v, dict) else {} + + +def _safe_int(v: Any) -> int: + try: + return max(0, int(v or 0)) + except (TypeError, ValueError): + return 0 + + +def _to_dict(obj: Any) -> dict[str, Any]: + """Best-effort pydantic-or-dict to dict (Anthropic SDK returns pydantic Message objects).""" + if isinstance(obj, dict): + return obj + if hasattr(obj, "model_dump"): + try: + return cast(dict[str, Any], obj.model_dump()) + except Exception: # noqa: BLE001 + pass + return {} + + +def extract_anthropic_native(response: Any, model_id: str = "") -> CanonicalUsage: + """Translate an Anthropic native response (Message or dict) → CanonicalUsage. + + Accepts the SDK's pydantic Message object, a dict (e.g. captured fixture), + or a synthetic `{"usage": {...}}` blob produced by the streaming wrapper. + """ + resp = _to_dict(response) if not isinstance(response, dict) else response + + usage = _safe_dict(resp.get("usage")) + cache_creation = _safe_dict(usage.get("cache_creation")) + + content = resp.get("content") + tool_calls = ( + sum(1 for b in content if isinstance(b, dict) and b.get("type") == "tool_use") + if isinstance(content, list) + else 0 + ) + + extras: dict[str, Any] = {} + for k, v in usage.items(): + if k not in _KNOWN_USAGE_FIELDS: + extras[k] = v + + return CanonicalUsage( + input=_safe_int(usage.get("input_tokens")), + output=_safe_int(usage.get("output_tokens")), + cache_read=_safe_int(usage.get("cache_read_input_tokens")), + cache_write=_safe_int(usage.get("cache_creation_input_tokens")), + cache_write_5m=_safe_int(cache_creation.get("ephemeral_5m_input_tokens")), + cache_write_1h=_safe_int(cache_creation.get("ephemeral_1h_input_tokens")), + tool_calls=tool_calls, + model=model_id or (resp.get("model") if isinstance(resp.get("model"), str) else "") or "", + provider="anthropic", + api="native", + extras=extras, + ) diff --git a/src/lago_agent_sdk/sdk.py b/src/lago_agent_sdk/sdk.py index c401df5..c303e03 100644 --- a/src/lago_agent_sdk/sdk.py +++ b/src/lago_agent_sdk/sdk.py @@ -83,13 +83,17 @@ def wrap( from .wrappers.mistral import wrap_mistral_client return wrap_mistral_client(self, client, dimensions=dimensions, subscription=subscription) + if kind == "anthropic": + from .wrappers.anthropic import wrap_anthropic_client + + return wrap_anthropic_client(self, client, dimensions=dimensions, subscription=subscription) if kind == "unknown": raise UnknownClientError( f"Unknown client passed to wrap(): {type(client).__module__}.{type(client).__name__}. " - "Supported: boto3 bedrock-runtime, mistralai.client.Mistral." + "Supported: boto3 bedrock-runtime, mistralai.client.Mistral, anthropic.Anthropic / AsyncAnthropic." ) raise UnknownClientError( - f"Client kind '{kind}' is not yet supported. Implemented: 'bedrock', 'mistral'." + f"Client kind '{kind}' is not yet supported. Implemented: 'bedrock', 'mistral', 'anthropic'." ) # ------------------------------------------------------------------ diff --git a/src/lago_agent_sdk/wrappers/anthropic.py b/src/lago_agent_sdk/wrappers/anthropic.py new file mode 100644 index 0000000..50da482 --- /dev/null +++ b/src/lago_agent_sdk/wrappers/anthropic.py @@ -0,0 +1,231 @@ +"""anthropic SDK wrapper. + +Wraps the public methods of `Anthropic.messages` (and `AsyncAnthropic.messages`) +in place — instrumentation never breaks the customer's call. + +Methods wrapped: + - .create(...) — non-streaming and stream=True both supported + - .stream(...) — sync context-manager helper + - AsyncMessages.create(...) — async non-streaming and stream=True + - AsyncMessages.stream(...) — async context-manager helper + +Per-call override: pop `extra_lago={"subscription": ..., "dimensions": ...}` from kwargs +before forwarding so Anthropic's strict validation doesn't reject it. +""" + +from __future__ import annotations + +import logging +from collections.abc import AsyncIterator, Iterator +from typing import Any + +from ..adapters import extract_anthropic_native + +logger = logging.getLogger("lago_agent_sdk.wrappers.anthropic") + +_INSTRUMENTED_ATTR = "_lago_instrumented" +_LAGO_KWARG = "extra_lago" + + +def _pop_lago_kwarg(kwargs: dict[str, Any]) -> dict[str, Any]: + return kwargs.pop(_LAGO_KWARG, {}) or {} + + +def _is_message_like(obj: Any) -> bool: + """Anthropic Message objects expose `.usage` and `.content`; streams don't. + + Safe against properties that raise — falls through to False so the customer's + call is never broken by attribute-access surprises in their custom objects. + """ + try: + if isinstance(obj, dict): + return "usage" in obj + # hasattr propagates non-AttributeError exceptions on Py3; guard explicitly. + return hasattr(obj, "usage") + except Exception: # noqa: BLE001 + return False + + +def wrap_anthropic_client( + sdk: Any, + client: Any, + dimensions: dict[str, Any] | None = None, + subscription: str | None = None, +) -> Any: + """In-place wrap of an `anthropic.Anthropic` or `anthropic.AsyncAnthropic` client. Idempotent.""" + if getattr(client, _INSTRUMENTED_ATTR, False): + logger.info("lago: anthropic client already wrapped — skipping") + return client + + base_dims = dict(dimensions or {}) + base_sub = subscription + + messages = getattr(client, "messages", None) + if messages is None: + logger.warning("lago: anthropic client has no .messages — skipping wrap") + return client + + original_create = getattr(messages, "create", None) + original_stream = getattr(messages, "stream", None) + is_async = type(client).__name__.startswith("Async") + + def _resolve_opts(lago_opts: dict[str, Any]) -> tuple[str | None, dict[str, Any]]: + sub = lago_opts.get("subscription") or base_sub + dims = {**base_dims, **(lago_opts.get("dimensions") or {})} + return sub, dims + + def _emit_from(payload: Any, model_id: str, sub: str | None, dims: dict[str, Any]) -> None: + try: + usage = extract_anthropic_native(payload, model_id=model_id) + sdk.emit(usage, subscription=sub, dimensions=dims) + except Exception as exc: # noqa: BLE001 + logger.warning("lago: anthropic emit failed: %s", exc) + + # ------------------------------------------------------------------ + # Sync messages.create — auto-detects streaming via response shape + # ------------------------------------------------------------------ + def _create(*args: Any, **kwargs: Any) -> Any: + assert original_create is not None + lago_opts = _pop_lago_kwarg(kwargs) + model_id = kwargs.get("model", "") + sub, dims = _resolve_opts(lago_opts) + response = original_create(*args, **kwargs) + + if _is_message_like(response): + _emit_from(response, model_id, sub, dims) + return response + + # Streaming — wrap the iterator to capture the final usage on close. + def _wrap_stream(src: Iterator[Any]) -> Iterator[Any]: + last_usage: dict[str, Any] | None = None + try: + for event in src: + payload = event.model_dump() if hasattr(event, "model_dump") else event + if isinstance(payload, dict): + usage = payload.get("usage") + if isinstance(usage, dict): + last_usage = {"usage": usage} + yield event + finally: + if last_usage is not None: + _emit_from(last_usage, model_id, sub, dims) + + return _wrap_stream(response) + + # ------------------------------------------------------------------ + # Async messages.create — same as sync, awaited + # ------------------------------------------------------------------ + async def _create_async(*args: Any, **kwargs: Any) -> Any: + assert original_create is not None + lago_opts = _pop_lago_kwarg(kwargs) + model_id = kwargs.get("model", "") + sub, dims = _resolve_opts(lago_opts) + response = await original_create(*args, **kwargs) + + if _is_message_like(response): + _emit_from(response, model_id, sub, dims) + return response + + async def _wrap_async_stream(src: AsyncIterator[Any]) -> AsyncIterator[Any]: + last_usage: dict[str, Any] | None = None + try: + async for event in src: + payload = event.model_dump() if hasattr(event, "model_dump") else event + if isinstance(payload, dict): + usage = payload.get("usage") + if isinstance(usage, dict): + last_usage = {"usage": usage} + yield event + finally: + if last_usage is not None: + _emit_from(last_usage, model_id, sub, dims) + + return _wrap_async_stream(response) + + # ------------------------------------------------------------------ + # messages.stream context manager (sync + async) + # + # Anthropic returns a MessageStreamManager (sync) / AsyncMessageStreamManager + # (async). Both have .__enter__/.__exit__ and the inner stream object + # exposes .get_final_message() after the with-block closes. + # ------------------------------------------------------------------ + def _wrap_stream_manager(*args: Any, **kwargs: Any) -> Any: + assert original_stream is not None + lago_opts = _pop_lago_kwarg(kwargs) + model_id = kwargs.get("model", "") + sub, dims = _resolve_opts(lago_opts) + inner = original_stream(*args, **kwargs) + return _LagoStreamManager(inner, sdk, model_id, sub, dims, is_async=is_async) + + if original_create is not None: + messages.create = _create_async if is_async else _create + if original_stream is not None: + messages.stream = _wrap_stream_manager + + setattr(client, _INSTRUMENTED_ATTR, True) + return client + + +class _LagoStreamManager: + """Proxies Anthropic's MessageStreamManager and emits on close. + + Works for both sync (`with`) and async (`async with`) variants by detecting + which __exit__ kind is being called. + """ + + def __init__( + self, + inner: Any, + sdk: Any, + model_id: str, + sub: str | None, + dims: dict[str, Any], + *, + is_async: bool, + ) -> None: + self._inner = inner + self._sdk = sdk + self._model_id = model_id + self._sub = sub + self._dims = dims + self._stream: Any = None + self._is_async = is_async + + # ----- sync ----- + def __enter__(self) -> Any: + self._stream = self._inner.__enter__() + return self._stream + + def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Any: + try: + result = self._inner.__exit__(exc_type, exc, tb) + finally: + self._emit_final() + return result + + # ----- async ----- + async def __aenter__(self) -> Any: + self._stream = await self._inner.__aenter__() + return self._stream + + async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> Any: + try: + result = await self._inner.__aexit__(exc_type, exc, tb) + finally: + self._emit_final() + return result + + def _emit_final(self) -> None: + try: + final = ( + self._stream.get_final_message() + if self._stream and hasattr(self._stream, "get_final_message") + else None + ) + if final is not None: + from ..adapters import extract_anthropic_native + + usage = extract_anthropic_native(final, model_id=self._model_id) + self._sdk.emit(usage, subscription=self._sub, dimensions=self._dims) + except Exception as exc: # noqa: BLE001 + logger.warning("lago: anthropic stream-manager emit failed: %s", exc) diff --git a/tests/integration/test_live_anthropic.py b/tests/integration/test_live_anthropic.py new file mode 100644 index 0000000..1b60e67 --- /dev/null +++ b/tests/integration/test_live_anthropic.py @@ -0,0 +1,114 @@ +"""End-to-end Anthropic integration test — live API + mocked Lago. + +Skipped unless ANTHROPIC_API_KEY is set. +""" + +from __future__ import annotations + +import json +import os +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer + +import pytest + +from lago_agent_sdk import LagoSDK + +pytestmark = pytest.mark.skipif( + not os.environ.get("ANTHROPIC_API_KEY"), + reason="ANTHROPIC_API_KEY not set", +) + + +class _MockLago(BaseHTTPRequestHandler): + def do_POST(self): # noqa: N802 + n = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(n) + self.server.received.append(json.loads(body)) # type: ignore[attr-defined] + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(b'{"ok": true}') + + def log_message(self, *_args, **_kwargs): + return + + +def _spawn_lago(): + s = HTTPServer(("127.0.0.1", 0), _MockLago) + s.received = [] # type: ignore[attr-defined] + threading.Thread(target=s.serve_forever, daemon=True).start() + return s, f"http://127.0.0.1:{s.server_port}" + + +def test_live_anthropic_messages_create_emits_to_lago() -> None: + from anthropic import Anthropic + + server, url = _spawn_lago() + try: + sdk = LagoSDK(api_key="x", api_url=url, default_subscription_id="sub_int") + client = sdk.wrap(Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])) + client.messages.create( + model="claude-haiku-4-5-20251001", + max_tokens=20, + messages=[{"role": "user", "content": "Say hi"}], + ) + assert sdk.flush(timeout=10.0) + sdk.shutdown(timeout=2.0) + events = [e for p in server.received for e in p["events"]] # type: ignore[attr-defined] + codes = {e["code"] for e in events} + assert "llm_input_tokens" in codes + assert "llm_output_tokens" in codes + for e in events: + assert e["properties"]["api"] == "native" + assert e["properties"]["provider"] == "anthropic" + finally: + server.shutdown() + + +def test_live_anthropic_streaming_emits_from_final_delta() -> None: + from anthropic import Anthropic + + server, url = _spawn_lago() + try: + sdk = LagoSDK(api_key="x", api_url=url, default_subscription_id="sub_int") + client = sdk.wrap(Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])) + for _ in client.messages.create( + model="claude-haiku-4-5-20251001", + max_tokens=20, + messages=[{"role": "user", "content": "Say hi"}], + stream=True, + ): + pass + assert sdk.flush(timeout=10.0) + sdk.shutdown(timeout=2.0) + events = [e for p in server.received for e in p["events"]] # type: ignore[attr-defined] + codes = {e["code"] for e in events} + assert "llm_input_tokens" in codes + assert "llm_output_tokens" in codes + finally: + server.shutdown() + + +def test_live_anthropic_messages_stream_context_manager() -> None: + from anthropic import Anthropic + + server, url = _spawn_lago() + try: + sdk = LagoSDK(api_key="x", api_url=url, default_subscription_id="sub_int") + client = sdk.wrap(Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])) + with client.messages.stream( + model="claude-haiku-4-5-20251001", + max_tokens=20, + messages=[{"role": "user", "content": "Say hi"}], + ) as stream: + for _ in stream.text_stream: + pass + assert sdk.flush(timeout=10.0) + sdk.shutdown(timeout=2.0) + events = [e for p in server.received for e in p["events"]] # type: ignore[attr-defined] + codes = {e["code"] for e in events} + assert "llm_input_tokens" in codes + assert "llm_output_tokens" in codes + finally: + server.shutdown() diff --git a/tests/unit/adapters/fixtures/anthropic_native/01_plain_haiku.json b/tests/unit/adapters/fixtures/anthropic_native/01_plain_haiku.json new file mode 100644 index 0000000..ebe80c3 --- /dev/null +++ b/tests/unit/adapters/fixtures/anthropic_native/01_plain_haiku.json @@ -0,0 +1,33 @@ +{ + "_model_id": "claude-haiku-4-5-20251001", + "_response": { + "id": "msg_014oRrBt8p4HqV5k5eS1RyKN", + "container": null, + "content": [ + { + "citations": null, + "text": "Dolphins are highly intelligent marine mammals known for their playful behavior, complex social structures, and remarkable ability to communicate with each other through clicks and whistles.", + "type": "text" + } + ], + "model": "claude-haiku-4-5-20251001", + "role": "assistant", + "stop_details": null, + "stop_reason": "end_turn", + "stop_sequence": null, + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "inference_geo": "not_available", + "input_tokens": 13, + "output_tokens": 35, + "server_tool_use": null, + "service_tier": "standard" + } + } +} \ No newline at end of file diff --git a/tests/unit/adapters/fixtures/anthropic_native/02_plain_sonnet.json b/tests/unit/adapters/fixtures/anthropic_native/02_plain_sonnet.json new file mode 100644 index 0000000..36071ff --- /dev/null +++ b/tests/unit/adapters/fixtures/anthropic_native/02_plain_sonnet.json @@ -0,0 +1,33 @@ +{ + "_model_id": "claude-sonnet-4-6", + "_response": { + "id": "msg_01Y3fakcdpcj6tk6FJrxV5GJ", + "container": null, + "content": [ + { + "citations": null, + "text": "Dolphins are highly intelligent marine mammals known for their playful behavior, complex social bonds, and remarkable ability to communicate using a variety of clicks, whistles, and other sounds.", + "type": "text" + } + ], + "model": "claude-sonnet-4-6", + "role": "assistant", + "stop_details": null, + "stop_reason": "end_turn", + "stop_sequence": null, + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "inference_geo": "global", + "input_tokens": 13, + "output_tokens": 39, + "server_tool_use": null, + "service_tier": "standard" + } + } +} \ No newline at end of file diff --git a/tests/unit/adapters/fixtures/anthropic_native/03_tool_use.json b/tests/unit/adapters/fixtures/anthropic_native/03_tool_use.json new file mode 100644 index 0000000..5dc4e83 --- /dev/null +++ b/tests/unit/adapters/fixtures/anthropic_native/03_tool_use.json @@ -0,0 +1,39 @@ +{ + "_model_id": "claude-sonnet-4-6", + "_response": { + "id": "msg_01FBZLSB8UduV9akqUkEtyEW", + "container": null, + "content": [ + { + "id": "toolu_01AMkoyrfvRgYBCA21zpGXNi", + "caller": { + "type": "direct" + }, + "input": { + "city": "Tokyo" + }, + "name": "get_weather", + "type": "tool_use" + } + ], + "model": "claude-sonnet-4-6", + "role": "assistant", + "stop_details": null, + "stop_reason": "tool_use", + "stop_sequence": null, + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "inference_geo": "global", + "input_tokens": 658, + "output_tokens": 38, + "server_tool_use": null, + "service_tier": "standard" + } + } +} \ No newline at end of file diff --git a/tests/unit/adapters/fixtures/anthropic_native/04_cache_create_5m.json b/tests/unit/adapters/fixtures/anthropic_native/04_cache_create_5m.json new file mode 100644 index 0000000..2fdaf1f --- /dev/null +++ b/tests/unit/adapters/fixtures/anthropic_native/04_cache_create_5m.json @@ -0,0 +1,33 @@ +{ + "_model_id": "claude-sonnet-4-6", + "_response": { + "id": "msg_01VtgdQSaox4WMdGvHipchpn", + "container": null, + "content": [ + { + "citations": null, + "text": "**4**\n\nSteps:\n1. Start with 2\n2. Add 2\n3. Result = **4**", + "type": "text" + } + ], + "model": "claude-sonnet-4-6", + "role": "assistant", + "stop_details": null, + "stop_reason": "end_turn", + "stop_sequence": null, + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 2803 + }, + "cache_creation_input_tokens": 2803, + "cache_read_input_tokens": 0, + "inference_geo": "global", + "input_tokens": 13, + "output_tokens": 30, + "server_tool_use": null, + "service_tier": "standard" + } + } +} \ No newline at end of file diff --git a/tests/unit/adapters/fixtures/anthropic_native/05_cache_read.json b/tests/unit/adapters/fixtures/anthropic_native/05_cache_read.json new file mode 100644 index 0000000..c7fc4bb --- /dev/null +++ b/tests/unit/adapters/fixtures/anthropic_native/05_cache_read.json @@ -0,0 +1,33 @@ +{ + "_model_id": "claude-sonnet-4-6", + "_response": { + "id": "msg_011bpj3E8EKkiNzynaPfBuJu", + "container": null, + "content": [ + { + "citations": null, + "text": "**3 + 3 = 6**\n\n**Step 1:** Start with 3.\n**Step 2:** Add ", + "type": "text" + } + ], + "model": "claude-sonnet-4-6", + "role": "assistant", + "stop_details": null, + "stop_reason": "max_tokens", + "stop_sequence": null, + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 2803, + "inference_geo": "global", + "input_tokens": 13, + "output_tokens": 30, + "server_tool_use": null, + "service_tier": "standard" + } + } +} \ No newline at end of file diff --git a/tests/unit/adapters/fixtures/anthropic_native/06_cache_create_1h.json b/tests/unit/adapters/fixtures/anthropic_native/06_cache_create_1h.json new file mode 100644 index 0000000..8624bc4 --- /dev/null +++ b/tests/unit/adapters/fixtures/anthropic_native/06_cache_create_1h.json @@ -0,0 +1,33 @@ +{ + "_model_id": "claude-sonnet-4-6", + "_response": { + "id": "msg_01CnMDjYpfBiy7JWKyMeNWsL", + "container": null, + "content": [ + { + "citations": null, + "text": "Hi! How can I help you today?", + "type": "text" + } + ], + "model": "claude-sonnet-4-6", + "role": "assistant", + "stop_details": null, + "stop_reason": "end_turn", + "stop_sequence": null, + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 2808, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 2808, + "cache_read_input_tokens": 0, + "inference_geo": "global", + "input_tokens": 7, + "output_tokens": 12, + "server_tool_use": null, + "service_tier": "standard" + } + } +} \ No newline at end of file diff --git a/tests/unit/adapters/fixtures/anthropic_native/07_extended_thinking.json b/tests/unit/adapters/fixtures/anthropic_native/07_extended_thinking.json new file mode 100644 index 0000000..4d0002d --- /dev/null +++ b/tests/unit/adapters/fixtures/anthropic_native/07_extended_thinking.json @@ -0,0 +1,38 @@ +{ + "_model_id": "claude-sonnet-4-6", + "_response": { + "id": "msg_01EAk8qJYaKDY7YNx6hiKBta", + "container": null, + "content": [ + { + "signature": "ErQCCmUIDRgCKkCFY8OE3VsRVnV6nXU/77sHcbai8ab7vVMPJKrFLAG73brAKyvMuOUHY3F3hARXd9ZCb++Z1sJ6emEjEdif2OwRMhFjbGF1ZGUtc29ubmV0LTQtNjgAQgh0aGlua2luZxIM6cZezoae3YMBKeUHGgw/iy0PoRrY0tY3z48iML7CqI0ZluxPJPXTAI3QxkdVG3dzXfxNpDhRWvOHvrURM5KpKm8gasgo5LLIJfQa0ip9EvggIrnDJKCXnOk5BFOsoNUe+2QdTzF7tGepNuhmB9R10uJzdEft/L/pw847GzejnrRYrahkVoNAv8hx/tiQABnoEbPqLixGimf2FhSMEUzzaHHoYiNlIdaR9cCZOXQ3hgyKrfkjg004wt6SaCLbZxm5SLZ8Yi5gsgVC0kAYAQ==", + "thinking": "We need to prove that 1\u00b3 + 2\u00b3 + 3\u00b3 + ... + n\u00b3 = (1 + 2 + 3 + ... + n)\u00b2\n\nI'll use mathematical induction.", + "type": "thinking" + }, + { + "citations": null, + "text": "# Proof: Sum of First n Cubes\n\n## Statement to Prove\n\n$$\\sum_{k=1}^{n} k^3 = \\left(\\sum_{k=1}^{n} k\\right)^2$$\n\nUsing the known closed form for the linear sum, this is equivalent to:\n\n$$1^3 + 2^3 + 3^3 + \\cdots + n^3 = \\left(\\frac{n(n+1)}{2}\\right)^2$$\n\n## Proof by Mathematical Induction\n\n### Base Case (n = 1)\n\n**Left side:** $1^3 = 1$\n\n**Right side:** $\\left(\\dfrac{1 \\cdot 2}{2}\\right)^2 = (1)^2 = 1$ \u2713\n\n### Inductive Step\n\n**Inductive Hypothesis:** Assume the statement holds for some $n = m \\geq 1$:\n\n$$1^3 + 2^3 + \\cdots + m^3 = \\left(\\frac{m(m+1)}{2}\\right)^2$$\n\n**Goal:** Prove the statement holds for $n = m + 1$:\n\n$$1^3 + 2^3 + \\cdots + m^3 + (m+1)^3 = \\left(\\frac{(m+1)(m+2)}{2}\\right)^2$$\n\n**Derivation:**\n\nStarting with the left side of the goal:\n\n$$\\underbrace{1^3 + 2^3 + \\cdots + m^3}_{\\text{apply hypothesis}} + (m+1)^3$$\n\n$$= \\left(\\frac{m(m+1)}{2}\\right)^2 + (m+1)^3$$\n\n$$= \\frac{m^2(m+1)^2}{4} + (m+1)^3$$\n\nFactor out $(m+1)^2$:\n\n$$= (m+1)^2\\left(\\frac{m^2}{4} + (m+1)\\right)$$\n\nCombine terms inside the parentheses over a common denominator of 4:\n\n$$= (m+1)^2\\left(\\frac{m^2 + 4(m+1)}{4}\\right)$$\n\n$$= (m+1)^2\\left(\\frac{m^2 + 4m + 4}{4}\\right)$$\n\nFactor the numerator as a perfect square:\n\n$$= (m+1)^2\\left(\\frac{(m+2)^2}{4}\\right)$$\n\nRearrange:\n\n$$= \\frac{(m+1)^2(m+2)^2}{4}$$\n\n$$= \\left(\\frac{(m+1)(m+2)}{2}\\right)^2$$\n\nThis is exactly the right side of the goal. $\\blacksquare$\n\n## Conclusion\n\nBy the principle of mathematical induction, for all positive integers $n$:\n\n$$\\boxed{1^3 + 2^3 + 3^3 + \\cdots + n^3 = \\left(\\frac{n(n+1)}{2}\\right)^2 = \\left(1 + 2 + 3 + \\cdots + n\\right)^2}$$", + "type": "text" + } + ], + "model": "claude-sonnet-4-6", + "role": "assistant", + "stop_details": null, + "stop_reason": "end_turn", + "stop_sequence": null, + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "inference_geo": "global", + "input_tokens": 66, + "output_tokens": 862, + "server_tool_use": null, + "service_tier": "standard" + } + } +} \ No newline at end of file diff --git a/tests/unit/adapters/fixtures/anthropic_native/08_stream.json b/tests/unit/adapters/fixtures/anthropic_native/08_stream.json new file mode 100644 index 0000000..6b55d43 --- /dev/null +++ b/tests/unit/adapters/fixtures/anthropic_native/08_stream.json @@ -0,0 +1,142 @@ +{ + "_model_id": "claude-haiku-4-5-20251001", + "_response": { + "events": [ + { + "message": { + "id": "msg_01A9ASsVixqCzpb4qwbcCkd2", + "container": null, + "content": [], + "model": "claude-haiku-4-5-20251001", + "role": "assistant", + "stop_details": null, + "stop_reason": null, + "stop_sequence": null, + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "inference_geo": "not_available", + "input_tokens": 13, + "output_tokens": 8, + "server_tool_use": null, + "service_tier": "standard" + } + }, + "type": "message_start" + }, + { + "content_block": { + "citations": null, + "text": "", + "type": "text" + }, + "index": 0, + "type": "content_block_start" + }, + { + "delta": { + "text": "Dolphins are highly intelligent marine mammals known", + "type": "text_delta" + }, + "index": 0, + "type": "content_block_delta" + }, + { + "type": "text", + "text": "Dolphins are highly intelligent marine mammals known", + "snapshot": "Dolphins are highly intelligent marine mammals known" + }, + { + "delta": { + "text": " for their playful behavior, complex social structures, and remarkable ability to communicate with each other through clicks", + "type": "text_delta" + }, + "index": 0, + "type": "content_block_delta" + }, + { + "type": "text", + "text": " for their playful behavior, complex social structures, and remarkable ability to communicate with each other through clicks", + "snapshot": "Dolphins are highly intelligent marine mammals known for their playful behavior, complex social structures, and remarkable ability to communicate with each other through clicks" + }, + { + "delta": { + "text": " and whistles.", + "type": "text_delta" + }, + "index": 0, + "type": "content_block_delta" + }, + { + "type": "text", + "text": " and whistles.", + "snapshot": "Dolphins are highly intelligent marine mammals known for their playful behavior, complex social structures, and remarkable ability to communicate with each other through clicks and whistles." + }, + { + "index": 0, + "type": "content_block_stop", + "content_block": { + "citations": null, + "text": "Dolphins are highly intelligent marine mammals known for their playful behavior, complex social structures, and remarkable ability to communicate with each other through clicks and whistles.", + "type": "text", + "parsed_output": null + } + }, + { + "delta": { + "container": null, + "stop_details": null, + "stop_reason": "end_turn", + "stop_sequence": null + }, + "type": "message_delta", + "usage": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input_tokens": 13, + "output_tokens": 35, + "server_tool_use": null + } + }, + { + "type": "message_stop", + "message": { + "id": "msg_01A9ASsVixqCzpb4qwbcCkd2", + "container": null, + "content": [ + { + "citations": null, + "text": "Dolphins are highly intelligent marine mammals known for their playful behavior, complex social structures, and remarkable ability to communicate with each other through clicks and whistles.", + "type": "text", + "parsed_output": null + } + ], + "model": "claude-haiku-4-5-20251001", + "role": "assistant", + "stop_details": null, + "stop_reason": "end_turn", + "stop_sequence": null, + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "inference_geo": "not_available", + "input_tokens": 13, + "output_tokens": 35, + "server_tool_use": null, + "service_tier": "standard" + } + } + } + ] + } +} \ No newline at end of file diff --git a/tests/unit/adapters/fixtures/anthropic_native/09_multi_turn.json b/tests/unit/adapters/fixtures/anthropic_native/09_multi_turn.json new file mode 100644 index 0000000..eb3ff9c --- /dev/null +++ b/tests/unit/adapters/fixtures/anthropic_native/09_multi_turn.json @@ -0,0 +1,33 @@ +{ + "_model_id": "claude-haiku-4-5-20251001", + "_response": { + "id": "msg_01Tg1sNzgCXenQnUBLwQ8Ycr", + "container": null, + "content": [ + { + "citations": null, + "text": "4 times 3 equals 12.", + "type": "text" + } + ], + "model": "claude-haiku-4-5-20251001", + "role": "assistant", + "stop_details": null, + "stop_reason": "end_turn", + "stop_sequence": null, + "type": "message", + "usage": { + "cache_creation": { + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0 + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "inference_geo": "not_available", + "input_tokens": 34, + "output_tokens": 14, + "server_tool_use": null, + "service_tier": "standard" + } + } +} \ No newline at end of file diff --git a/tests/unit/adapters/fixtures/capture_anthropic.py b/tests/unit/adapters/fixtures/capture_anthropic.py new file mode 100644 index 0000000..73893a2 --- /dev/null +++ b/tests/unit/adapters/fixtures/capture_anthropic.py @@ -0,0 +1,179 @@ +"""Capture real Anthropic native API responses for adapter design. + +Saves raw responses to tests/unit/adapters/fixtures/anthropic_native/.json +so we can verify mappings against reality before writing the adapter. + +Reads ANTHROPIC_API_KEY from env. +""" + +from __future__ import annotations + +import json +import os +import pathlib +import sys + +from anthropic import Anthropic + +OUT = pathlib.Path(__file__).parent / "anthropic_native" +OUT.mkdir(parents=True, exist_ok=True) + + +def to_dict(response) -> dict: + """Anthropic SDK returns pydantic models — convert to plain dict for JSON.""" + if hasattr(response, "model_dump"): + return response.model_dump() + if hasattr(response, "dict"): + return response.dict() + return json.loads(response.json()) if hasattr(response, "json") else dict(response) + + +def save(name: str, model: str, payload: dict) -> None: + path = OUT / f"{name}.json" + path.write_text(json.dumps({"_model_id": model, "_response": payload}, indent=2, default=str)) + print(f" ✓ saved {path.name}") + + +def main() -> int: + key = os.environ.get("ANTHROPIC_API_KEY") + if not key: + print("error: set ANTHROPIC_API_KEY", file=sys.stderr) + return 2 + + client = Anthropic(api_key=key) + PROMPT = "Write one sentence about dolphins." + + # Rename badge: the script header reads "Sonnet 4.5" but the API only exposes 4-6+ now. + # ----- 1. Plain call (small model) ----- + print("\n[1] plain — claude-haiku-4-5-20251001") + r = client.messages.create( + model="claude-haiku-4-5-20251001", + max_tokens=80, + messages=[{"role": "user", "content": PROMPT}], + ) + save("01_plain_haiku", "claude-haiku-4-5-20251001", to_dict(r)) + + # ----- 2. Plain call (Sonnet, larger) ----- + print("\n[2] plain — claude-sonnet-4-6") + r = client.messages.create( + model="claude-sonnet-4-6", + max_tokens=80, + messages=[{"role": "user", "content": PROMPT}], + ) + save("02_plain_sonnet", "claude-sonnet-4-6", to_dict(r)) + + # ----- 3. Tool use ----- + print("\n[3] tool use — claude-sonnet-4-6 with weather tool") + tools = [ + { + "name": "get_weather", + "description": "Get the current weather for a city.", + "input_schema": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, + } + ] + r = client.messages.create( + model="claude-sonnet-4-6", + max_tokens=200, + tools=tools, + tool_choice={"type": "any"}, + messages=[{"role": "user", "content": "What's the weather in Tokyo?"}], + ) + save("03_tool_use", "claude-sonnet-4-6", to_dict(r)) + + # ----- 4. Cache create (5m default TTL) — long system prompt ----- + print("\n[4] cache create — long system + cache_control 5m default") + LONG_TEXT = ("You are a helpful assistant. Answer concisely. " * 200) + ( + "Always cite step by step. " * 100 + ) + cached_body = { + "model": "claude-sonnet-4-6", + "max_tokens": 30, + "system": [{"type": "text", "text": LONG_TEXT, "cache_control": {"type": "ephemeral"}}], + "messages": [{"role": "user", "content": "What's 2+2?"}], + } + r = client.messages.create(**cached_body) + save("04_cache_create_5m", "claude-sonnet-4-6", to_dict(r)) + + # ----- 5. Cache read (same long system, different user question) ----- + print("\n[5] cache read — same cached_control content, second call") + cached_body["messages"] = [{"role": "user", "content": "What's 3+3?"}] + r = client.messages.create(**cached_body) + save("05_cache_read", "claude-sonnet-4-6", to_dict(r)) + + # ----- 6. Cache 1h TTL ----- + print("\n[6] cache 1h — explicit ttl") + cached_1h = { + "model": "claude-sonnet-4-6", + "max_tokens": 30, + "system": [ + { + "type": "text", + "text": LONG_TEXT + " (1h variant)", + "cache_control": {"type": "ephemeral", "ttl": "1h"}, + } + ], + "messages": [{"role": "user", "content": "Hi"}], + } + try: + r = client.messages.create(**cached_1h) + save("06_cache_create_1h", "claude-sonnet-4-6", to_dict(r)) + except Exception as exc: # noqa: BLE001 + print(f" 1h TTL not available on this account/region: {str(exc)[:160]}") + + # ----- 7. Extended thinking (reasoning) ----- + print("\n[7] extended thinking — claude-sonnet-4-6") + try: + r = client.messages.create( + model="claude-sonnet-4-6", + max_tokens=2048, + thinking={"type": "enabled", "budget_tokens": 1024}, + messages=[ + { + "role": "user", + "content": ( + "Prove that the sum of the first n cubes equals the square of the sum of " + "the first n positive integers. Show each algebraic step." + ), + } + ], + ) + save("07_extended_thinking", "claude-sonnet-4-6", to_dict(r)) + except Exception as exc: # noqa: BLE001 + print(f" extended thinking error: {str(exc)[:160]}") + + # ----- 8. Streaming ----- + print("\n[8] streaming — claude-haiku-4-5-20251001") + events: list[dict] = [] + with client.messages.stream( + model="claude-haiku-4-5-20251001", + max_tokens=60, + messages=[{"role": "user", "content": PROMPT}], + ) as stream: + for event in stream: + events.append(to_dict(event)) + save("08_stream", "claude-haiku-4-5-20251001", {"events": events}) + + # ----- 9. Multi-turn ----- + print("\n[9] multi-turn — claude-haiku-4-5-20251001") + convo = [ + {"role": "user", "content": "What is 2+2?"}, + {"role": "assistant", "content": "2+2 equals 4."}, + {"role": "user", "content": "And times 3?"}, + ] + r = client.messages.create( + model="claude-haiku-4-5-20251001", + max_tokens=40, + messages=convo, + ) + save("09_multi_turn", "claude-haiku-4-5-20251001", to_dict(r)) + + print("\nDone. Inspect tests/unit/adapters/fixtures/anthropic_native/*.json") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/adapters/test_anthropic_native.py b/tests/unit/adapters/test_anthropic_native.py new file mode 100644 index 0000000..13bff69 --- /dev/null +++ b/tests/unit/adapters/test_anthropic_native.py @@ -0,0 +1,152 @@ +"""Anthropic native adapter — verified against real fixtures.""" + +from __future__ import annotations + +import json +import pathlib + +from lago_agent_sdk.adapters import extract_anthropic_native + +FIX = pathlib.Path(__file__).parent / "fixtures" / "anthropic_native" + + +def _load(name: str) -> tuple[str, dict]: + data = json.loads((FIX / name).read_text()) + return data["_model_id"], data["_response"] + + +# -------------------------------------------------------------------------- +# Real fixtures +# -------------------------------------------------------------------------- +def test_plain_haiku() -> None: + model_id, resp = _load("01_plain_haiku.json") + u = extract_anthropic_native(resp, model_id=model_id) + assert u.input == 13 + assert u.output == 35 + assert u.cache_read == 0 + assert u.cache_write == 0 + assert u.tool_calls == 0 + assert u.api == "native" + assert u.provider == "anthropic" + assert u.model == "claude-haiku-4-5-20251001" + + +def test_plain_sonnet() -> None: + model_id, resp = _load("02_plain_sonnet.json") + u = extract_anthropic_native(resp, model_id=model_id) + assert u.input == 13 + assert u.output == 39 + + +def test_tool_use_counts_tool_calls() -> None: + model_id, resp = _load("03_tool_use.json") + u = extract_anthropic_native(resp, model_id=model_id) + assert u.input == 658 + assert u.output == 38 + assert u.tool_calls == 1 + + +def test_cache_create_5m() -> None: + model_id, resp = _load("04_cache_create_5m.json") + u = extract_anthropic_native(resp, model_id=model_id) + assert u.cache_write == 2803 + assert u.cache_write_5m == 2803 + assert u.cache_write_1h == 0 + assert u.cache_read == 0 + + +def test_cache_read_after_create() -> None: + model_id, resp = _load("05_cache_read.json") + u = extract_anthropic_native(resp, model_id=model_id) + assert u.cache_read == 2803 + assert u.cache_write == 0 + assert u.cache_write_5m == 0 + + +def test_cache_create_1h() -> None: + model_id, resp = _load("06_cache_create_1h.json") + u = extract_anthropic_native(resp, model_id=model_id) + assert u.cache_write == 2808 + assert u.cache_write_1h == 2808 + assert u.cache_write_5m == 0 + + +def test_extended_thinking_bundles_into_output_tokens() -> None: + """Anthropic's extended thinking does NOT expose reasoning_tokens — they're folded into output_tokens.""" + model_id, resp = _load("07_extended_thinking.json") + u = extract_anthropic_native(resp, model_id=model_id) + assert u.input == 66 + assert u.output == 862 # all 862 includes thinking + final answer + assert u.reasoning == 0 # confirmed: Anthropic doesn't separate it + # content has both 'thinking' and 'text' blocks — neither counts as a tool call + assert u.tool_calls == 0 + + +def test_multi_turn() -> None: + model_id, resp = _load("09_multi_turn.json") + u = extract_anthropic_native(resp, model_id=model_id) + assert u.input == 34 + assert u.output == 14 + + +def test_unknown_top_usage_field_lands_in_extras() -> None: + """service_tier, inference_geo, server_tool_use are new fields → drift detection.""" + model_id, resp = _load("01_plain_haiku.json") + u = extract_anthropic_native(resp, model_id=model_id) + assert "service_tier" in u.extras + assert "inference_geo" in u.extras + assert "server_tool_use" in u.extras + + +# -------------------------------------------------------------------------- +# Synthetic +# -------------------------------------------------------------------------- +def test_handles_pydantic_via_model_dump() -> None: + class FakePydantic: + def model_dump(self) -> dict: + return { + "model": "claude-sonnet-4-6", + "content": [{"type": "text", "text": "hi"}], + "usage": { + "input_tokens": 5, + "output_tokens": 7, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0, + }, + }, + } + + u = extract_anthropic_native(FakePydantic(), model_id="claude-sonnet-4-6") + assert u.input == 5 + assert u.output == 7 + assert u.api == "native" + + +def test_multiple_tool_use_blocks_counted() -> None: + resp = { + "usage": {"input_tokens": 10, "output_tokens": 20}, + "content": [ + {"type": "text", "text": "..."}, + {"type": "tool_use", "id": "t1"}, + {"type": "tool_use", "id": "t2"}, + {"type": "tool_use", "id": "t3"}, + ], + } + u = extract_anthropic_native(resp, model_id="claude-sonnet-4-6") + assert u.tool_calls == 3 + + +def test_no_usage_returns_zeros() -> None: + u = extract_anthropic_native({}, model_id="claude-sonnet-4-6") + assert u.input == 0 + assert u.output == 0 + assert not u.nonzero_numeric() + + +def test_survives_non_dict_usage() -> None: + assert extract_anthropic_native({"usage": True}, model_id="x").input == 0 + assert extract_anthropic_native({"usage": "bogus"}, model_id="x").output == 0 + assert extract_anthropic_native(None, model_id="x").input == 0 diff --git a/tests/unit/test_buffer_overflow.py b/tests/unit/test_buffer_overflow.py index 19d2e0f..d1c9907 100644 --- a/tests/unit/test_buffer_overflow.py +++ b/tests/unit/test_buffer_overflow.py @@ -45,7 +45,11 @@ def test_repeated_overflow_keeps_window_sliding(): def slow_sender(batch): paused.wait(timeout=30.0) - q = EventQueue(sender=slow_sender, flush_interval=10.0, max_batch_size=100, max_buffer_size=100) + # max_batch_size > max_buffer_size keeps the background worker from ever + # being woken by push (buffer can't exceed max_batch_size). Combined with + # a long flush_interval, the test is deterministic — the worker only runs + # once shutdown() releases `paused` in the finally block. + q = EventQueue(sender=slow_sender, flush_interval=60.0, max_batch_size=10_000, max_buffer_size=100) try: for i in range(250): # 150 events overflow q.push({"i": i}) diff --git a/tests/unit/test_wrapper_anthropic.py b/tests/unit/test_wrapper_anthropic.py new file mode 100644 index 0000000..08c7a70 --- /dev/null +++ b/tests/unit/test_wrapper_anthropic.py @@ -0,0 +1,223 @@ +"""Anthropic wrapper tests — fake client, no live API.""" + +from __future__ import annotations + +from typing import Any + +from lago_agent_sdk import LagoSDK + + +class FakeMessage: + """Mimics Anthropic's Message pydantic object.""" + + def __init__(self, payload: dict[str, Any]) -> None: + self._payload = payload + # expose .usage and .content as attribute access for _is_message_like check + self.usage = payload.get("usage") + self.content = payload.get("content", []) + + def model_dump(self) -> dict[str, Any]: + return self._payload + + +class FakeStreamEvent: + """Mimics one of Anthropic's MessageStreamEvent objects (MessageDelta/Start/etc.).""" + + def __init__(self, payload: dict[str, Any]) -> None: + self._payload = payload + + def model_dump(self) -> dict[str, Any]: + return self._payload + + +class FakeMessages: + def __init__(self) -> None: + self.create_calls = 0 + self.stream_calls = 0 + + def create(self, **kwargs: Any) -> Any: + self.create_calls += 1 + assert "extra_lago" not in kwargs + if kwargs.get("stream") is True: + events = [ + FakeStreamEvent({"type": "message_start", "message": {"usage": {"input_tokens": 12}}}), + FakeStreamEvent( + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"input_tokens": 12, "output_tokens": 22}, + } + ), + FakeStreamEvent({"type": "message_stop"}), + ] + return iter(events) + return FakeMessage( + { + "model": kwargs.get("model", "claude-sonnet-4-6"), + "content": [{"type": "text", "text": "hi"}], + "usage": { + "input_tokens": 8, + "output_tokens": 16, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0, + }, + }, + } + ) + + def stream(self, **kwargs: Any) -> Any: + self.stream_calls += 1 + assert "extra_lago" not in kwargs + outer = self + + class _FakeStreamManager: + def __enter__(self_inner) -> Any: + outer._final = FakeMessage( + { + "model": kwargs.get("model", "claude-sonnet-4-6"), + "content": [{"type": "text", "text": "hi"}], + "usage": { + "input_tokens": 5, + "output_tokens": 11, + }, + } + ) + return _FakeStreamHandle(outer._final) + + def __exit__(self_inner, exc_type, exc, tb) -> Any: # noqa: D401 + return False + + return _FakeStreamManager() + + +class _FakeStreamHandle: + def __init__(self, final: FakeMessage) -> None: + self._final = final + self.text_stream = iter(["hi"]) + + def get_final_message(self) -> FakeMessage: + return self._final + + +class FakeAnthropic: + """Mimics `from anthropic import Anthropic; Anthropic(api_key=...)`.""" + + def __init__(self) -> None: + self.messages = FakeMessages() + + +# Module path needs to contain 'anthropic' so detector.py routes to anthropic wrapper. +FakeAnthropic.__module__ = "anthropic.fake" + + +def _new_sdk(default_sub: str = "sub_test") -> tuple[LagoSDK, list[dict]]: + received: list[dict] = [] + + def sender(batch: list[dict]) -> None: + received.extend(batch) + + sdk = LagoSDK(api_key="dummy", default_subscription_id=default_sub) + sdk._queue._sender = sender # type: ignore[attr-defined] + return sdk, received + + +def test_wrap_messages_create_emits_input_and_output() -> None: + sdk, received = _new_sdk() + fake = FakeAnthropic() + client = sdk.wrap(fake) + resp = client.messages.create(model="claude-sonnet-4-6", messages=[]) + assert resp.usage["input_tokens"] == 8 + assert sdk.flush(timeout=2.0) + sdk.shutdown(timeout=1.0) + by_code = {e["code"]: int(float(e["properties"]["value"])) for e in received} + assert by_code["llm_input_tokens"] == 8 + assert by_code["llm_output_tokens"] == 16 + + +def test_wrap_strips_extra_lago_and_uses_per_call_sub() -> None: + sdk, received = _new_sdk("sub_default") + fake = FakeAnthropic() + client = sdk.wrap(fake) + client.messages.create( + model="claude-sonnet-4-6", + messages=[], + extra_lago={"subscription": "sub_per_call", "dimensions": {"feature": "X"}}, + ) + assert sdk.flush(timeout=2.0) + sdk.shutdown(timeout=1.0) + assert all(e["external_subscription_id"] == "sub_per_call" for e in received) + assert received[0]["properties"]["feature"] == "X" + + +def test_wrap_double_wrap_is_idempotent() -> None: + sdk, received = _new_sdk() + fake = FakeAnthropic() + sdk.wrap(fake) + sdk.wrap(fake) + sdk.wrap(fake) + fake.messages.create(model="claude-sonnet-4-6", messages=[]) + assert sdk.flush(timeout=2.0) + sdk.shutdown(timeout=1.0) + assert len(received) == 2 # input + output, not 6 + assert fake.messages.create_calls == 1 + + +def test_wrap_create_with_stream_captures_usage_from_message_delta() -> None: + sdk, received = _new_sdk() + fake = FakeAnthropic() + client = sdk.wrap(fake) + events = list(client.messages.create(model="claude-sonnet-4-6", messages=[], stream=True)) + assert len(events) == 3 + assert sdk.flush(timeout=2.0) + sdk.shutdown(timeout=1.0) + by_code = {e["code"]: int(float(e["properties"]["value"])) for e in received} + assert by_code["llm_input_tokens"] == 12 + assert by_code["llm_output_tokens"] == 22 + + +def test_wrap_messages_stream_context_manager_emits_on_close() -> None: + sdk, received = _new_sdk() + fake = FakeAnthropic() + client = sdk.wrap(fake) + with client.messages.stream(model="claude-sonnet-4-6", messages=[]) as stream: + list(stream.text_stream) + assert sdk.flush(timeout=2.0) + sdk.shutdown(timeout=1.0) + by_code = {e["code"]: int(float(e["properties"]["value"])) for e in received} + assert by_code["llm_input_tokens"] == 5 + assert by_code["llm_output_tokens"] == 11 + + +def test_instrumentation_failure_does_not_break_call() -> None: + sdk, _ = _new_sdk() + + class BadMessage: + @property + def usage(self): + raise RuntimeError("boom") + + @property + def content(self): + return [] + + def model_dump(self): + raise RuntimeError("boom") + + class BadMessages: + def create(self, **_kw): + return BadMessage() + + class BadAnthropic: + def __init__(self): + self.messages = BadMessages() + + BadAnthropic.__module__ = "anthropic.fake" + + client = sdk.wrap(BadAnthropic()) + # Adapter will crash inside, but wrap must still return resp. + resp = client.messages.create(model="x", messages=[]) + assert resp is not None + sdk.shutdown(timeout=1.0) diff --git a/uv.lock b/uv.lock index f1fc5c8..a40fdc7 100644 --- a/uv.lock +++ b/uv.lock @@ -15,6 +15,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anthropic" +version = "0.103.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/57/0b758b08cf4606c94d63a997d67a0063f7438efbaf81cfedd0d7c0c69d67/anthropic-0.103.1.tar.gz", hash = "sha256:21c12f4fc0fdd87a2e80d58479cd0af640062b3cfb82bbfa01c7977acd4defeb", size = 848877, upload-time = "2026-05-19T15:43:27.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/ec/cf357cf571377a39552c1530390a9b79bbdb6ea463f48fbe4e3624141e3b/anthropic-0.103.1-py3-none-any.whl", hash = "sha256:b9a523fac34e64caf6ee55fdbda213950e6a744b906fce100d34909aad2cd8f4", size = 832551, upload-time = "2026-05-19T15:43:29.663Z" }, +] + [[package]] name = "anyio" version = "4.13.0" @@ -345,6 +364,24 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + [[package]] name = "eval-type-backport" version = "0.3.1" @@ -446,6 +483,109 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jiter" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/da/76a2c7e510ba15fe323d9509c223ab272da79ea59f54488f4a78da6426db/jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4", size = 310849, upload-time = "2026-05-19T10:06:51.944Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8e/827be942883a4dc0862c48626ff41af3320b1902d136a0bf4b9041f2c567/jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f", size = 314991, upload-time = "2026-05-19T10:06:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/6d/38/be2832be361ba1b9517c76f46d30b64e985be1dd43c974f4c3a4b1844436/jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18", size = 340843, upload-time = "2026-05-19T10:06:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/90f01fb83c0c7ba509303ec93e32a308fbfa167d264860b01c0fd0dbbd06/jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f", size = 365116, upload-time = "2026-05-19T10:06:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/91/38/94593d34f8c67a0b6f6cbc027f016ffa9780b3a858a7a86f6fd7a15bcc1e/jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4", size = 457970, upload-time = "2026-05-19T10:06:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/df/04/d79962dd49d00c97e2a9b4cacea1947904d02135936960351f9a96d4c1a6/jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6", size = 375744, upload-time = "2026-05-19T10:07:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2e/5d37abe2be0e819c21e2338bebd410e481763ce526a9138c8c3652fa0123/jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c", size = 349609, upload-time = "2026-05-19T10:07:01.829Z" }, + { url = "https://files.pythonhosted.org/packages/7a/90/98768ad2ed90c1fda15d64157de2dfbf73c1c074d4b1bfaca915480bc7cf/jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512", size = 354366, upload-time = "2026-05-19T10:07:03.587Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c4/fbfb806209f1fe4b7dccdfb07bc62bb044300734a945b06fd64db446ef6a/jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a", size = 393519, upload-time = "2026-05-19T10:07:05.08Z" }, + { url = "https://files.pythonhosted.org/packages/37/1c/b9c257cd70cb453b6d10f3ebf0402cdb11669ab455389096f09839670290/jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887", size = 519952, upload-time = "2026-05-19T10:07:06.589Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1a/aa85027db7ab15829c12feebbc33b404f53fc399bd559d85fd0d6365ff0d/jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823", size = 550770, upload-time = "2026-05-19T10:07:08.228Z" }, + { url = "https://files.pythonhosted.org/packages/d4/54/8c3f65c8a5687925e84708f19d63f7f37d28e2b86a48d951702ad94424d8/jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53", size = 209303, upload-time = "2026-05-19T10:07:10.006Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/0528a1eb9f42dd2d8228a0711458628f35924d131f623eaebc35fd23d3d4/jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1", size = 200404, upload-time = "2026-05-19T10:07:11.426Z" }, + { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, + { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, + { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, + { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, +] + [[package]] name = "jmespath" version = "1.1.0" @@ -473,6 +613,9 @@ dependencies = [ ] [package.optional-dependencies] +anthropic = [ + { name = "anthropic" }, +] bedrock = [ { name = "boto3" }, ] @@ -491,8 +634,14 @@ mistral = [ { name = "mistralai" }, ] +[package.dev-dependencies] +dev = [ + { name = "anthropic" }, +] + [package.metadata] requires-dist = [ + { name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=0.30" }, { name = "boto3", marker = "extra == 'bedrock'", specifier = ">=1.34" }, { name = "boto3", marker = "extra == 'dev'", specifier = ">=1.34" }, { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6" }, @@ -506,7 +655,10 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, { name = "types-requests", marker = "extra == 'dev'", specifier = ">=2.31" }, ] -provides-extras = ["bedrock", "mistral", "dev"] +provides-extras = ["bedrock", "mistral", "dev", "anthropic"] + +[package.metadata.requires-dev] +dev = [{ name = "anthropic", specifier = ">=0.30" }] [[package]] name = "librt" @@ -992,6 +1144,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "sortedcontainers" version = "2.4.0"