From 78d8b9e929788cfc3911e29dc82fde9347d77da0 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Mon, 21 Sep 2026 11:14:35 -0800 Subject: [PATCH 1/5] feat(AIC-3410): add LiteLLM integrations Add in-process messages and Agents SDK handlers so AI Configs can route across LiteLLM-supported providers with tools, streaming, and graph support. --- .env.example | 3 + .release-please-manifest.json | 4 +- AGENTS.md | 10 +- README.md | 34 +- examples/litellm_agents_example.py | 37 + examples/litellm_example.py | 41 + examples/litellm_messages_example.py | 38 + main.py | 4 + packages/litellm-agents/README.md | 35 + packages/litellm-agents/pyproject.toml | 34 + .../__init__.py | 18 + .../launchdarkly_ai_litellm_agents/graph.py | 27 + .../launchdarkly_ai_litellm_agents/handler.py | 683 +++++++++++++ .../native_graph.py | 163 +++ packages/litellm-agents/tests/test_graph.py | 361 +++++++ packages/litellm-agents/tests/test_handler.py | 506 ++++++++++ packages/litellm-messages/README.md | 35 + packages/litellm-messages/pyproject.toml | 34 + .../__init__.py | 11 + .../handler.py | 631 ++++++++++++ .../litellm-messages/tests/test_handler.py | 597 +++++++++++ pyproject.toml | 2 + release-please-config.json | 22 + uv.lock | 935 ++++++++++++++++++ 24 files changed, 4257 insertions(+), 8 deletions(-) create mode 100644 examples/litellm_agents_example.py create mode 100644 examples/litellm_example.py create mode 100644 examples/litellm_messages_example.py create mode 100644 packages/litellm-agents/README.md create mode 100644 packages/litellm-agents/pyproject.toml create mode 100644 packages/litellm-agents/src/launchdarkly_ai_litellm_agents/__init__.py create mode 100644 packages/litellm-agents/src/launchdarkly_ai_litellm_agents/graph.py create mode 100644 packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py create mode 100644 packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py create mode 100644 packages/litellm-agents/tests/test_graph.py create mode 100644 packages/litellm-agents/tests/test_handler.py create mode 100644 packages/litellm-messages/README.md create mode 100644 packages/litellm-messages/pyproject.toml create mode 100644 packages/litellm-messages/src/launchdarkly_ai_litellm_messages/__init__.py create mode 100644 packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py create mode 100644 packages/litellm-messages/tests/test_handler.py diff --git a/.env.example b/.env.example index 5bb04800..db75b59a 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,9 @@ LD_SDK_KEY=your-sdk-key-here OPENAI_API_KEY=your-openai-key-here ANTHROPIC_API_KEY=your-anthropic-key-here +# Python LiteLLM runs in-process and uses the provider keys above; no +# LITELLM_BASE_URL or proxy API key is required. + # LaunchDarkly endpoints (leave blank to use production defaults) LD_BASE_URI= LD_STREAM_URI= diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 26e16e54..e6bf93b5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -6,5 +6,7 @@ "packages/openai-agents": "0.2.2", "packages/openai-messages": "0.2.2", "packages/langchain-agents": "0.2.2", - "packages/langchain-messages": "0.2.2" + "packages/langchain-messages": "0.2.2", + "packages/litellm-agents": "0.1.0", + "packages/litellm-messages": "0.1.0" } diff --git a/AGENTS.md b/AGENTS.md index 305613a9..23d26e2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,6 +87,7 @@ graph TD claude["launchdarkly-ai-claude-agents"] openai["launchdarkly-ai-openai-agents"] langchain["launchdarkly-ai-langchain-agents"] + litellm["launchdarkly-ai-litellm-agents"] newHandler["launchdarkly-ai-new-provider\n(future)"] end subgraph tier0 ["Tier 0 — Core"] @@ -110,7 +111,7 @@ graph TD - **Tier 0 — Core** (`launchdarkly-ai-server`): The foundation. Owns all LaunchDarkly integration, telemetry orchestration, shared data types, and the primary entry points (`config()`, `graph()`, `resolve_graph()`). Has no dependency on any other `launchdarkly-ai-*` package. - **Tier 0 — Convenience barrel** (`launchdarkly-ai-python`): A pure re-export package that makes all of `launchdarkly-ai-server` available under a shorter install name. No new logic — intended as the default install for most Python applications. -- **Tier 1 — Handler packages** (`launchdarkly-ai-claude-agents`, `launchdarkly-ai-claude-messages`, `launchdarkly-ai-openai-agents`, `launchdarkly-ai-openai-messages`, `launchdarkly-ai-langchain-agents`, `launchdarkly-ai-langchain-messages`, …): Each wraps a specific AI provider SDK. Depends on `launchdarkly-ai-server` for shared types and utilities. Must not depend on other Tier 1 packages. +- **Tier 1 — Handler packages** (`launchdarkly-ai-claude-agents`, `launchdarkly-ai-claude-messages`, `launchdarkly-ai-openai-agents`, `launchdarkly-ai-openai-messages`, `launchdarkly-ai-langchain-agents`, `launchdarkly-ai-langchain-messages`, `launchdarkly-ai-litellm-agents`, `launchdarkly-ai-litellm-messages`, …): Each wraps a specific AI provider SDK. Depends on `launchdarkly-ai-server` for shared types and utilities. Must not depend on other Tier 1 packages. - **Tier 2 — Consumer applications** (e.g. `main.py`, downstream projects): Imports from one or more handler packages and either `launchdarkly-ai-python` or `launchdarkly-ai-server`. Owns tool implementations and orchestration logic. No `launchdarkly-ai-*` package should ever depend on Tier 2 code. ### Rules @@ -375,7 +376,7 @@ Requires `handlers` (either in `options` or via `options.registry`) to be set. Resolves an agent graph's topology and node configs without executing it. The returned `GraphDefinition` carries `enabled`; callers should branch on it before traversing. -This is the entry point that framework-native runners (`to_claude_agents`, `to_openai_agents`, `to_lang_graph`) use to build their own execution structure. +This is the entry point that framework-native runners (`to_claude_agents`, `to_openai_agents`, `to_lang_graph`, `to_litellm_agents`) use to build their own execution structure. ### `Registry` / `global_registry` / `compose` @@ -657,7 +658,7 @@ def my_provider( For example, `claude_agents(config_key, user_input, context)` is equivalent to `config(key=config_key, handler=create_claude_agents_handler()).invoke(user_input, context)`. -The naming convention matches the package suffix: `claude_agents`, `claude_messages`, `openai_agents`, `openai_messages`, `langchain_agents`, `langchain_messages`. +The naming convention matches the package suffix: `claude_agents`, `claude_messages`, `openai_agents`, `openai_messages`, `langchain_agents`, `langchain_messages`, `litellm_agents`, `litellm_messages`. ### Graph Export (optional) @@ -668,7 +669,7 @@ def claude_graph(key: str, **options) -> GraphInstance: return graph(key, handlers=[create_claude_agents_handler()], **options) ``` -Naming convention: `claude_graph`, `openai_graph`, `langchain_graph`. +Naming convention: `claude_graph`, `openai_graph`, `langchain_graph`, `litellm_graph`. ### Native Graph Adapter (optional) @@ -678,6 +679,7 @@ Current adapters: - `to_claude_agents(def_coro, opts)` — exported from `launchdarkly_ai_claude_agents` - `to_openai_agents(def_coro, opts)` — exported from `launchdarkly_ai_openai_agents` - `to_lang_graph(def_coro, opts)` — exported from `launchdarkly_ai_langchain_agents` +- `to_litellm_agents(def_coro, opts)` — exported from `launchdarkly_ai_litellm_agents` --- diff --git a/README.md b/README.md index 0e693665..1f043ec3 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ That call is the whole integration. Everything it does is configured in LaunchDa - Run agents and multi-step graphs, where each step can use a different provider - Score output quality with judges, including scoring that stays off the request path - See cost, latency, token usage, errors, and full conversations with no instrumentation code -- Keep the providers and frameworks you already run: OpenAI, Anthropic, LangChain, or your own handler +- Keep the providers and frameworks you already run: OpenAI, Anthropic, LangChain, LiteLLM, or your own handler - [What you get](#what-you-get) - [How It Works](#how-it-works) @@ -85,6 +85,8 @@ Tier 0 — Core Client (launchdarkly-ai-server) | [`launchdarkly-ai-claude-agents`](packages/claude-agents/README.md) | Anthropic | `agent` | Claude Agent SDK — agentic loop with MCP tool support | | [`launchdarkly-ai-langchain-messages`](packages/langchain-messages/README.md) | `*` (any) | `messages` | Any `BaseChatModel` via LangChain `bind_tools` loop | | [`launchdarkly-ai-langchain-agents`](packages/langchain-agents/README.md) | `*` (any) | `agent` | LangGraph `StateGraph` — managed ReAct loop | +| [`launchdarkly-ai-litellm-messages`](packages/litellm-messages/README.md) | `*` (any) | `messages` | In-process LiteLLM `acompletion` tool loop | +| [`launchdarkly-ai-litellm-agents`](packages/litellm-agents/README.md) | `*` (any) | `agent` | OpenAI Agents SDK with LiteLLM models | ## Quick Start @@ -111,6 +113,9 @@ cp .env.example .env # Fill in LD_SDK_KEY and the API key for your provider ``` +LiteLLM runs in-process in Python, so no proxy endpoint is required. Set the +provider credentials required by the model names in your AI Config. + ### 3. Call a model #### 3a. Convenience functions @@ -149,6 +154,8 @@ asyncio.run(main()) | `claude_agents` | `launchdarkly-ai-claude-agents` | `claude-agent-sdk` | Claude Agent SDK (MCP) | | `langchain_messages` | `launchdarkly-ai-langchain-messages` | `langchain-core` | LangChain `bind_tools` loop | | `langchain_agents` | `launchdarkly-ai-langchain-agents` | `langgraph` | LangGraph `StateGraph` | +| `litellm_messages` | `launchdarkly-ai-litellm-messages` | `litellm` | LiteLLM `acompletion` | +| `litellm_agents` | `launchdarkly-ai-litellm-agents` | `openai-agents[litellm]` | OpenAI Agents via LiteLLM | --- @@ -250,7 +257,7 @@ async def main(): asyncio.run(main()) ``` -Provider packages also export single-provider conveniences (`claude_graph`, `openai_graph`, `langchain_graph`) that pre-bind their handler. +Provider packages also export conveniences (`claude_graph`, `openai_graph`, `langchain_graph`, `litellm_graph`) that pre-bind their handler. --- @@ -311,6 +318,22 @@ result = await to_openai_agents( ).invoke("I was double charged") ``` +##### `to_litellm_agents` — OpenAI Agents SDK over LiteLLM + +Builds the same native Agents SDK handoff tree while creating each evaluated +node model through LiteLLM in-process. + +```python +from launchdarkly_ai_server import resolve_graph +from launchdarkly_ai_litellm_agents import to_litellm_agents + +ctx = {"kind": "user", "key": "user-123"} +result = await to_litellm_agents( + resolve_graph("support-graph", context=ctx), + {"tool_handlers": registry.tools}, +).invoke("I was double charged") +``` + ##### `to_lang_graph` — LangGraph `StateGraph` Uses a pre-order traversal (root → leaves) to build a compiled `StateGraph`. Single-child edges become direct edges; multi-child edges use `Command`-returning handoff tools so the model picks exactly one target. @@ -597,6 +620,9 @@ uv run python main.py [example] [flag-key] [user-input] | `graph-history` | `uv run python main.py graph-history` | `graph().invoke()` with multimodal `history` forwarded to the root node | | `openai-only` | `uv run python main.py openai-only` | `config()` with a custom `Registry` restricted to OpenAI handlers | | `streaming` | `uv run python main.py streaming` | `config().stream()` — token-by-token output | +| `litellm` | `uv run python main.py litellm` | `config()` routing both messages and agents through LiteLLM | +| `litellm-agents` | `uv run python main.py litellm-agents` | `litellm_agents()` with in-process LiteLLM routing | +| `litellm-messages` | `uv run python main.py litellm-messages` | `litellm_messages()` with in-process LiteLLM routing | **Examples:** @@ -636,7 +662,9 @@ python-ai-sdk/ │ ├── openai-agents/ # launchdarkly-ai-openai-agents │ ├── openai-messages/ # launchdarkly-ai-openai-messages │ ├── langchain-agents/ # launchdarkly-ai-langchain-agents -│ └── langchain-messages/ # launchdarkly-ai-langchain-messages +│ ├── langchain-messages/ # launchdarkly-ai-langchain-messages +│ ├── litellm-agents/ # launchdarkly-ai-litellm-agents +│ └── litellm-messages/ # launchdarkly-ai-litellm-messages ├── .env.example # Template — copy to .env and fill in your values └── agents.md # Architecture reference for AI agents and contributors ``` diff --git a/examples/litellm_agents_example.py b/examples/litellm_agents_example.py new file mode 100644 index 00000000..3778914f --- /dev/null +++ b/examples/litellm_agents_example.py @@ -0,0 +1,37 @@ +""" +Example: litellm_agents() — LiteLLM agents handler (wildcard provider). + +Usage (via main.py): + python main.py litellm-agents "" +""" + +from __future__ import annotations + +import json + +from examples.tools import ( + fetch_launchdarkly_documentation, + get_preferences, + search_ld_documentation, + web_search, +) +from examples.utils import new_context, write_output +from launchdarkly_ai_litellm_agents import litellm_agents + + +async def run(key: str, user_input: str) -> None: + response = await litellm_agents( + key, + user_input, + new_context(), + tool_handlers={ + "get-user-preferences": get_preferences, + "search-ld-documentation": search_ld_documentation, + "fetch-ld-documentation": fetch_launchdarkly_documentation, + "fetch-launchdarkly-documentation": fetch_launchdarkly_documentation, + "web-search": web_search, + }, + ) + + print(json.dumps(response, indent=2, default=str)) + write_output(response) diff --git a/examples/litellm_example.py b/examples/litellm_example.py new file mode 100644 index 00000000..d8ff35c4 --- /dev/null +++ b/examples/litellm_example.py @@ -0,0 +1,41 @@ +""" +Example: config() with LiteLLM handlers for messages and agents. + +Python calls LiteLLM in-process, so provider-qualified models and aliases from +the evaluated AI Config are routed using the installed LiteLLM configuration. + +Usage (via main.py): + python main.py litellm "" +""" + +from __future__ import annotations + +from examples.tools import ( + fetch_launchdarkly_documentation, + get_preferences, + search_ld_documentation, + web_search, +) +from examples.utils import new_context, write_output +from launchdarkly_ai_litellm_agents import create_litellm_agents_handler +from launchdarkly_ai_litellm_messages import create_litellm_messages_handler +from launchdarkly_ai_server import config + + +async def run(key: str, user_input: str) -> None: + response = await config( + key=key, + handler=[ + create_litellm_messages_handler(), + create_litellm_agents_handler(), + ], + tool_handlers={ + "get-user-preferences": get_preferences, + "search-ld-documentation": search_ld_documentation, + "fetch-ld-documentation": fetch_launchdarkly_documentation, + "fetch-launchdarkly-documentation": fetch_launchdarkly_documentation, + "web-search": web_search, + }, + ).invoke(user_input, new_context(), variables={"user_input": user_input}) + + write_output(response) diff --git a/examples/litellm_messages_example.py b/examples/litellm_messages_example.py new file mode 100644 index 00000000..80ee2598 --- /dev/null +++ b/examples/litellm_messages_example.py @@ -0,0 +1,38 @@ +""" +Example: litellm_messages() — LiteLLM messages handler. + +Usage (via main.py): + python main.py litellm-messages "" +""" + +from __future__ import annotations + +import json + +from examples.tools import ( + fetch_launchdarkly_documentation, + get_preferences, + search_ld_documentation, + web_search, +) +from examples.utils import new_context, write_output +from launchdarkly_ai_litellm_messages import litellm_messages + + +async def run(key: str, user_input: str) -> None: + response = await litellm_messages( + key, + user_input, + new_context(), + tool_handlers={ + "get-user-preferences": get_preferences, + "search-ld-documentation": search_ld_documentation, + "fetch-ld-documentation": fetch_launchdarkly_documentation, + "fetch-launchdarkly-documentation": fetch_launchdarkly_documentation, + "web-search": web_search, + }, + variables={"user_input": user_input}, + ) + + print(json.dumps(response, indent=2, default=str)) + write_output(response) diff --git a/main.py b/main.py index fc5c1e5b..ddc5659a 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ python main.py graph-history my-agent-graph "" python main.py openai-only my-openai-flag "Tell me about feature flags" python main.py langchain my-langchain-flag "Tell me about feature flags" + python main.py litellm my-litellm-flag "Tell me about feature flags" python main.py claude-agents launch-darkly-documentation-summarizer "What is the LaunchDarkly AI SDK?" python main.py openai-agents launch-darkly-documentation-summarizer-open-ai-only "What is the LaunchDarkly AI SDK?" python main.py langchain-agents launch-darkly-documentation-summarizer "What is the LaunchDarkly AI SDK?" @@ -58,6 +59,9 @@ "langchain-agents": "examples.langchain_agents_example", "langchain-messages": "examples.langchain_messages_example", "langchain-thinking": "examples.langchain_thinking", + "litellm": "examples.litellm_example", + "litellm-agents": "examples.litellm_agents_example", + "litellm-messages": "examples.litellm_messages_example", "native-graph": "examples.native_graph", "native-graph-langchain": "examples.native_graph_langchain", } diff --git a/packages/litellm-agents/README.md b/packages/litellm-agents/README.md new file mode 100644 index 00000000..562d11c0 --- /dev/null +++ b/packages/litellm-agents/README.md @@ -0,0 +1,35 @@ +# `launchdarkly-ai-litellm-agents` + +Provider-agnostic LaunchDarkly AI Config and graph handlers built on the OpenAI +Agents SDK's `LitellmModel`. + +## Installation + +```bash +pip install launchdarkly-ai-server launchdarkly-ai-litellm-agents +``` + +Python uses LiteLLM in-process; no proxy endpoint is required. Set the provider +credentials required by the models in your AI Config, such as +`OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. Standard LiteLLM configuration, +gateways, and model aliases are supported. + +## Usage + +```python +from launchdarkly_ai_litellm_agents import create_litellm_agents_handler +from launchdarkly_ai_server import config + +result = await config( + key="assistant", + handler=create_litellm_agents_handler(), +).invoke("Hello", {"kind": "user", "key": "user-123"}) +``` + +Each evaluated config model is authoritative and creates its own `LitellmModel`. +The package supports callable tools, structured output, streaming, the +`litellm_graph` convenience wrapper, and `to_litellm_agents` for native Agents +SDK handoffs. + +Because this is a wildcard agent handler, do not register it together with +another `["*", "agent"]` handler such as LangChain in the same registry. diff --git a/packages/litellm-agents/pyproject.toml b/packages/litellm-agents/pyproject.toml new file mode 100644 index 00000000..bc46b2f1 --- /dev/null +++ b/packages/litellm-agents/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "launchdarkly-ai-litellm-agents" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "launchdarkly-ai-server", + "openai-agents[litellm]>=0.2.11", + "opentelemetry-api>=1.25", +] +description = "LiteLLM-backed OpenAI Agents handler for LaunchDarkly AI SDK" +readme = "README.md" +license = "Apache-2.0" +authors = [{name = "LaunchDarkly", email = "team@launchdarkly.com"}] +keywords = ["launchdarkly", "ai", "litellm", "agents"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries", +] + +[project.urls] +Homepage = "https://github.com/launchdarkly/python-ai-sdk" +Repository = "https://github.com/launchdarkly/python-ai-sdk" +"Bug Tracker" = "https://github.com/launchdarkly/python-ai-sdk/issues" + +[build-system] +requires = ["hatchling>=1.32,<2"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/launchdarkly_ai_litellm_agents"] diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/__init__.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/__init__.py new file mode 100644 index 00000000..7edf1562 --- /dev/null +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/__init__.py @@ -0,0 +1,18 @@ +"""LaunchDarkly AI SDK integration for LiteLLM-backed OpenAI Agents.""" + +__version__ = "0.1.0" # x-release-please-version + +from launchdarkly_ai_server import register_ai_sdk_package + +from .graph import litellm_graph +from .handler import create_litellm_agents_handler, litellm_agents +from .native_graph import to_litellm_agents + +__all__ = [ + "create_litellm_agents_handler", + "litellm_agents", + "litellm_graph", + "to_litellm_agents", +] + +register_ai_sdk_package("launchdarkly-ai-litellm-agents", __version__) diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/graph.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/graph.py new file mode 100644 index 00000000..d8fbbc70 --- /dev/null +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/graph.py @@ -0,0 +1,27 @@ +"""Graph convenience wrapper for LiteLLM-backed Agents SDK runs.""" + +from __future__ import annotations + +from typing import Any + +from launchdarkly_ai_server import graph + +from .handler import ModelFactory, create_litellm_agents_handler + + +def litellm_graph( + key: str, + *, + model_factory: ModelFactory | None = None, + capture_content: bool = False, + **options: Any, +) -> Any: + return graph( + key, + handlers=[ + create_litellm_agents_handler( + model_factory=model_factory, capture_content=capture_content + ) + ], + **options, + ) diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py new file mode 100644 index 00000000..03052eb5 --- /dev/null +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py @@ -0,0 +1,683 @@ +from __future__ import annotations + +import asyncio +import inspect +import json +from collections.abc import AsyncGenerator, Callable +from typing import Any + +from agents import ModelSettings +from agents.agent_output import AgentOutputSchemaBase +from agents.extensions.models.litellm_model import LitellmModel +from agents.lifecycle import RunHooksBase +from opentelemetry import trace +from opentelemetry.trace import StatusCode + +from launchdarkly_ai_server import ( + AiConfigRep, + LDContext, + ProviderHandler, + SpanUsage, + config, + content_to_text, + create_handler, + end_span_once, + image_block_to_url, + parse_template, + set_input_content_attributes, + set_ld_span_attributes, + set_model_identity_attributes, + set_output_content_attributes, + set_tool_call_content_attributes, + set_usage_span_attributes, + text_message, +) + +ModelFactory = Callable[[str, dict[str, Any]], Any] +_MODEL_SETTING_FIELDS = { + "temperature", + "top_p", + "frequency_penalty", + "presence_penalty", + "tool_choice", + "parallel_tool_calls", + "max_tokens", + "reasoning", + "verbosity", + "metadata", + "store", + "include_usage", + "top_logprobs", + "extra_headers", + "extra_query", + "extra_body", +} + + +def _json_schema(output_format: dict[str, Any]) -> dict[str, Any]: + """Coerce an AI Config outputFormat into a JSON Schema object. + + LaunchDarkly stores structured-output metadata with ``type: json_schema``. Providers require + a real JSON Schema root type, so that sentinel is rewritten to ``object``. + """ + schema = dict(output_format) + if schema.get("type") in (None, "json_schema", "json"): + schema["type"] = "object" + return schema + + +class _JsonSchemaOutput(AgentOutputSchemaBase): + """Agents SDK output type that keeps the evaluated JSON Schema without looking like a dict. + + Inheriting from ``dict`` made ``isinstance(output_type, dict)`` true, so the SDK treated the + envelope as the schema itself and OpenAI rejected ``type: json_schema``. + """ + + def __init__(self, schema: dict[str, Any]) -> None: + self._schema = schema + + def __getitem__(self, key: str) -> Any: + if key != "schema": + raise KeyError(key) + return self._schema + + def is_plain_text(self) -> bool: + return False + + def name(self) -> str: + return "output" + + def json_schema(self) -> dict[str, Any]: + return self._schema + + def is_strict_json_schema(self) -> bool: + return False + + def validate_json(self, json_str: str) -> Any: + return json.loads(json_str) + + +def _model_config(config_value: AiConfigRep) -> tuple[str, dict[str, Any]]: + model = config_value.get("model") or {} + raw = model.get("parameters") + parameters = dict(raw) if isinstance(raw, dict) else {} + for field in ( + "model", + "messages", + "tools", + "stream", + "stream_options", + "response_format", + "output_format", + ): + parameters.pop(field, None) + return str(model.get("name") or ""), parameters + + +def _default_model_factory(name: str, parameters: dict[str, Any]) -> Any: + return LitellmModel( + model=name, + base_url=parameters.get("base_url"), + api_key=parameters.get("api_key"), + ) + + +def _model_settings(parameters: dict[str, Any]) -> ModelSettings: + known = { + key: value for key, value in parameters.items() if key in _MODEL_SETTING_FIELDS + } + extras = { + key: value + for key, value in parameters.items() + if key + not in _MODEL_SETTING_FIELDS | {"max_turns", "base_url", "api_key", "model"} + } + if extras: + known["extra_args"] = extras + return ModelSettings(**known) + + +def _instructions(config_value: AiConfigRep, variables: dict[str, Any]) -> str | None: + if config_value.get("instructions"): + return parse_template(config_value["instructions"], variables) + system = [ + message + for message in (config_value.get("messages") or []) + if message.get("role") == "system" + ] + if not system: + return None + return "\n".join( + parse_template(str(message.get("content", "")), variables) for message in system + ) + + +def _map_agent_content(role: str, content: Any) -> Any: + if role == "assistant": + if isinstance(content, list): + text = content_to_text(content) + else: + text = str(content or "") + return [{"type": "output_text", "text": text}] + if not isinstance(content, list): + return content + parts: list[dict[str, Any]] = [] + for block in content: + if block.get("type") == "image": + parts.append( + {"type": "input_image", "image_url": image_block_to_url(block)} + ) + elif block.get("type") == "text": + parts.append({"type": "input_text", "text": block.get("text", "")}) + return parts + + +def _prompt( + config_value: AiConfigRep, + user_input: str | None, + variables: dict[str, Any], + history: list[dict[str, Any]] | None, +) -> str | list[dict[str, Any]]: + turns: list[dict[str, Any]] = [] + if not config_value.get("instructions"): + for message in config_value.get("messages") or []: + if message.get("role") == "system": + continue + content = message.get("content", "") + if isinstance(content, str): + content = parse_template(content, variables) + turns.append({"role": message.get("role", "user"), "content": content}) + if history: + turns.extend( + { + "role": message.get("role", "user"), + "content": message.get("content", ""), + } + for message in history + if message.get("role") != "system" + ) + if turns: + if user_input: + turns.append({"role": "user", "content": user_input}) + return [ + { + "role": turn["role"], + "content": _map_agent_content(str(turn["role"]), turn["content"]), + } + for turn in turns + ] + return user_input or "" + + +async def _call_tool(handler: Any, arguments: dict[str, Any]) -> Any: + result = handler(arguments) + return await result if inspect.isawaitable(result) else result + + +def _tools(config_value: AiConfigRep, handlers: dict[str, Any]) -> list[Any]: + import importlib + + FunctionTool = importlib.import_module("agents").FunctionTool + tools: list[Any] = [] + for name, definition in (config_value.get("tools") or {}).items(): + handler = handlers.get(name) + if not callable(handler): + continue + + async def invoke(_context: Any, args: str, _handler: Any = handler) -> Any: + try: + arguments = json.loads(args or "{}") + except json.JSONDecodeError as exc: + raise ValueError("Tool arguments must be valid JSON") from exc + return await _call_tool(_handler, arguments) + + tools.append( + FunctionTool( + name=name, + description=definition.get("description", "") or "", + params_json_schema=definition.get("parameters") or {}, + on_invoke_tool=invoke, + ) + ) + return tools + + +def _usage(result: Any) -> SpanUsage: + wrapper = getattr(result, "context_wrapper", None) + raw = getattr(wrapper, "usage", None) + return SpanUsage( + input=int(getattr(raw, "input_tokens", 0) or 0), + output=int(getattr(raw, "output_tokens", 0) or 0), + ) + + +def _direct_usage(raw: Any) -> SpanUsage: + return SpanUsage( + input=int(getattr(raw, "input_tokens", 0) or 0), + output=int(getattr(raw, "output_tokens", 0) or 0), + ) + + +def _root_span(config_value: AiConfigRep, variables: dict[str, Any]) -> Any: + model_name = _model_config(config_value)[0] + provider = str( + (config_value.get("provider") or {}).get("name") or "litellm" + ).lower() + span = trace.get_tracer("@launchdarkly/ai-litellm-agents").start_span( + "invoke_agent" + ) + span.set_attribute("gen_ai.operation.name", "invoke_agent") + set_model_identity_attributes(span, provider, model_name, "litellm") + set_ld_span_attributes(span, variables) + return span + + +def _parent_context(span: Any) -> Any: + return trace.set_span_in_context(span) + + +def _model_span(config_value: AiConfigRep, parent: Any) -> Any: + model_name = _model_config(config_value)[0] + provider = str( + (config_value.get("provider") or {}).get("name") or "litellm" + ).lower() + span = trace.get_tracer("@launchdarkly/ai-litellm-agents").start_span( + f"chat {model_name}", context=parent + ) + span.set_attribute("gen_ai.operation.name", "chat") + set_model_identity_attributes(span, provider, model_name, "litellm") + return span + + +def _tool_span(name: str, call_id: str, parent: Any) -> Any: + span = trace.get_tracer("@launchdarkly/ai-litellm-agents").start_span( + f"execute_tool {name}", context=parent + ) + span.set_attribute("gen_ai.operation.name", "execute_tool") + span.set_attribute("gen_ai.tool.name", name) + span.set_attribute("gen_ai.tool.call.id", call_id) + return span + + +def _finish(span: Any, model_name: str, usage: SpanUsage, ended: set[int]) -> None: + span.set_attribute("gen_ai.response.model", model_name) + set_usage_span_attributes(span, usage) + span.set_status(StatusCode.OK) + end_span_once(span, ended) + + +def _succeed(span: Any, ended: set[int]) -> None: + span.set_status(StatusCode.OK) + end_span_once(span, ended) + + +def _fail(span: Any, error: BaseException, ended: set[int]) -> None: + span.record_exception(error) + span.set_status(StatusCode.ERROR, str(error)) + end_span_once(span, ended) + + +class _SpanningHooks(RunHooksBase[Any, Any]): + def __init__( + self, + config_value: AiConfigRep, + parent: Any, + capture_content: bool, + ended: set[int], + ) -> None: + self.config = config_value + self.parent = parent + self.capture_content = capture_content + self.ended = ended + self.open_model: Any = None + self.open_tools: dict[str, Any] = {} + self.total = SpanUsage() + self.usage_reported = False + + async def on_llm_start( + self, + context: Any, + agent: Any, + system_prompt: str | None, + input_items: Any, + ) -> None: + del context, agent + span = _model_span(self.config, self.parent) + self.open_model = span + if self.capture_content: + set_input_content_attributes( + span, + True, + system_instructions=system_prompt, + messages=[text_message("user", str(input_items))], + ) + + async def on_llm_end(self, context: Any, agent: Any, response: Any) -> None: + del context, agent + span = self.open_model + self.open_model = None + if span is None: + return + try: + usage = _direct_usage(getattr(response, "usage", None)) + self.total.input += usage.input + self.total.output += usage.output + self.usage_reported = ( + self.usage_reported or getattr(response, "usage", None) is not None + ) + if self.capture_content: + set_output_content_attributes( + span, + True, + [text_message("assistant", str(response.output or ""))], + ) + _finish( + span, + _model_config(self.config)[0], + usage, + self.ended, + ) + except Exception as exc: + _fail(span, exc, self.ended) + raise + + @staticmethod + def _tool_key(context: Any, tool: Any) -> str: + return str( + getattr(context, "tool_call_id", None) + or getattr(tool, "name", None) + or "tool" + ) + + async def on_tool_start(self, context: Any, agent: Any, tool: Any) -> None: + del agent + key = self._tool_key(context, tool) + name = str( + getattr(context, "tool_name", None) or getattr(tool, "name", None) or "tool" + ) + span = _tool_span(name, key, self.parent) + self.open_tools[key] = span + if self.capture_content: + raw = getattr(context, "tool_arguments", None) + if raw is not None: + try: + arguments = json.loads(raw) if isinstance(raw, str) else raw + except json.JSONDecodeError: + arguments = raw + set_tool_call_content_attributes(span, True, arguments=arguments) + + async def on_tool_end( + self, context: Any, agent: Any, tool: Any, result: Any + ) -> None: + del agent + span = self.open_tools.pop(self._tool_key(context, tool), None) + if span is None: + return + try: + if self.capture_content: + set_tool_call_content_attributes(span, True, result=result) + _succeed(span, self.ended) + except Exception as exc: + _fail(span, exc, self.ended) + raise + + def fail_open(self, error: BaseException) -> None: + for span in self.open_tools.values(): + _fail(span, error, self.ended) + self.open_tools.clear() + if self.open_model is not None: + _fail(self.open_model, error, self.ended) + self.open_model = None + + def close_open(self, *, abandoned: bool = False, cancelled: bool = False) -> None: + for span in self.open_tools.values(): + end_span_once( + span, + self.ended, + abandoned=abandoned, + cancelled=cancelled, + ) + self.open_tools.clear() + if self.open_model is not None: + end_span_once( + self.open_model, + self.ended, + abandoned=abandoned, + cancelled=cancelled, + ) + self.open_model = None + + +def _agent_and_prompt( + config_value: AiConfigRep, + user_input: str | None, + handlers: dict[str, Any], + variables: dict[str, Any], + history: list[dict[str, Any]] | None, + model_factory: ModelFactory, +) -> tuple[Any, Any, str | None, int]: + import importlib + + Agent = importlib.import_module("agents").Agent + name, parameters = _model_config(config_value) + model = model_factory(name, dict(parameters)) + instructions = _instructions(config_value, variables) + output_format = config_value.get("outputFormat") + output_type = ( + _JsonSchemaOutput(_json_schema(output_format)) if output_format else None + ) + agent = Agent( + name="assistant", + model=model, + model_settings=_model_settings(parameters), + **({"instructions": instructions} if instructions else {}), + **( + {"tools": _tools(config_value, handlers)} + if config_value.get("tools") + else {} + ), + **({"output_type": output_type} if output_type is not None else {}), + ) + return ( + agent, + _prompt(config_value, user_input, variables, history), + instructions, + int(parameters.get("max_turns", 10)), + ) + + +def create_litellm_agents_handler( + *, + model_factory: ModelFactory | None = None, + capture_content: bool = False, +) -> ProviderHandler: + factory = model_factory or _default_model_factory + + async def call( + config_value: AiConfigRep, + user_input: str | None = None, + tool_handlers: dict[str, Any] | None = None, + variables: dict[str, Any] | None = None, + history: list[dict[str, Any]] | None = None, + ) -> dict[str, Any]: + import importlib + + values = variables or {} + agent, prompt, instructions, max_turns = _agent_and_prompt( + config_value, + user_input, + tool_handlers or {}, + values, + history, + factory, + ) + span = _root_span(config_value, values) + parent = _parent_context(span) + model_name = _model_config(config_value)[0] + ended: set[int] = set() + hooks = _SpanningHooks(config_value, parent, capture_content, ended) + try: + if capture_content: + set_input_content_attributes( + span, + True, + system_instructions=instructions, + messages=[text_message("user", str(prompt))], + ) + result = await importlib.import_module("agents").Runner.run( + agent, prompt, max_turns=max_turns, hooks=hooks + ) + output = result.final_output + if capture_content: + set_output_content_attributes( + span, True, [text_message("assistant", str(output or ""))] + ) + usage = _usage(result) + if not (usage.input or usage.output) and hooks.usage_reported: + usage = hooks.total + _finish(span, model_name, usage, ended) + return { + "output": output, + "usage": { + "input_tokens": usage.input, + "output_tokens": usage.output, + }, + } + except Exception as exc: + hooks.fail_open(exc) + if hooks.usage_reported: + span.set_attribute("gen_ai.response.model", model_name) + set_usage_span_attributes(span, hooks.total) + _fail(span, exc, ended) + raise + finally: + hooks.close_open(cancelled=True) + end_span_once(span, ended, cancelled=True) + + def stream( + config_value: AiConfigRep, + user_input: str | None = None, + tool_handlers: dict[str, Any] | None = None, + variables: dict[str, Any] | None = None, + history: list[dict[str, Any]] | None = None, + ) -> AsyncGenerator[dict[str, Any], None]: + return _stream( + config_value, + user_input, + tool_handlers or {}, + variables or {}, + history, + factory, + capture_content=capture_content, + ) + + return create_handler( + ("*", "agent"), + call, + stream, + capture_content=capture_content, + ) + + +async def _stream( + config_value: AiConfigRep, + user_input: str | None, + handlers: dict[str, Any], + variables: dict[str, Any], + history: list[dict[str, Any]] | None, + model_factory: ModelFactory, + *, + capture_content: bool, +) -> AsyncGenerator[dict[str, Any], None]: + import importlib + + agent, prompt, instructions, max_turns = _agent_and_prompt( + config_value, user_input, handlers, variables, history, model_factory + ) + span = _root_span(config_value, variables) + parent = _parent_context(span) + streamed: Any = None + completed = False + ended: set[int] = set() + hooks = _SpanningHooks(config_value, parent, capture_content, ended) + cancelled = False + try: + if capture_content: + set_input_content_attributes( + span, + True, + system_instructions=instructions, + messages=[text_message("user", str(prompt))], + ) + streamed = importlib.import_module("agents").Runner.run_streamed( + agent, prompt, max_turns=max_turns, hooks=hooks + ) + collected = "" + async for event in streamed.stream_events(): + if getattr(event, "type", None) != "raw_response_event": + continue + data = getattr(event, "data", None) + if getattr(data, "type", None) == "response.output_text.delta": + delta = getattr(data, "delta", "") + if isinstance(delta, str) and delta: + collected += delta + yield {"type": "chunk", "text": delta} + output = streamed.final_output + if output is None: + output = collected + usage = _usage(streamed) + if not (usage.input or usage.output) and hooks.usage_reported: + usage = hooks.total + _finish(span, _model_config(config_value)[0], usage, ended) + completed = True + yield { + "type": "done", + "output": output, + "usage": { + "input_tokens": usage.input, + "output_tokens": usage.output, + }, + } + except asyncio.CancelledError: + cancelled = True + raise + except Exception as exc: + hooks.fail_open(exc) + if hooks.usage_reported: + span.set_attribute("gen_ai.response.model", _model_config(config_value)[0]) + set_usage_span_attributes(span, hooks.total) + _fail(span, exc, ended) + completed = True + raise + finally: + if streamed is not None and not completed: + try: + streamed.cancel() + except Exception: + pass + hooks.close_open(abandoned=not completed, cancelled=cancelled) + if not completed: + if hooks.usage_reported: + span.set_attribute( + "gen_ai.response.model", _model_config(config_value)[0] + ) + set_usage_span_attributes(span, hooks.total) + end_span_once(span, ended, abandoned=True, cancelled=cancelled) + + +def litellm_agents( + config_key: str, + user_input: str, + context: LDContext, + **kwargs: Any, +) -> Any: + variables = kwargs.pop("variables", None) + capture_content = bool(kwargs.pop("capture_content", False)) + model_factory = kwargs.pop("model_factory", None) + return config( + key=config_key, + handler=create_litellm_agents_handler( + model_factory=model_factory, capture_content=capture_content + ), + **kwargs, + ).invoke(user_input, context, variables) diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py new file mode 100644 index 00000000..14bdfb84 --- /dev/null +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py @@ -0,0 +1,163 @@ +"""Convert a resolved graph to OpenAI Agents SDK agents backed by LiteLLM.""" + +from __future__ import annotations + +import importlib +import re +import types +from typing import Any + +from agents.extensions.models.litellm_model import LitellmModel + +from launchdarkly_ai_server import ( + GraphDefinition, + GraphNode, + compose_history, + parse_template, +) + +from .handler import ( + ModelFactory, + _map_agent_content, + _model_config, + _model_settings, + _tools, +) + + +def _name(value: str) -> str: + return re.sub(r"[^a-zA-Z0-9_-]", "_", value)[:64] + + +def _default_factory(name: str, parameters: dict[str, Any]) -> Any: + return LitellmModel( + model=name, + base_url=parameters.get("base_url"), + api_key=parameters.get("api_key"), + ) + + +def _instructions(node: GraphNode, variables: dict[str, Any]) -> str | None: + config = node.config + if config.get("instructions"): + return parse_template(config["instructions"], variables) + system = [ + message + for message in (config.get("messages") or []) + if message.get("role") == "system" + ] + if not system: + return None + return "\n".join( + parse_template(str(message.get("content", "")), variables) for message in system + ) + + +def to_litellm_agents( + definition: Any, + opts: dict[str, Any] | None = None, +) -> Any: + options = opts or {} + + async def invoke( + input_text: str = "", + variables: dict[str, Any] | None = None, + history: list[dict[str, Any]] | None = None, + ) -> dict[str, Any]: + graph: GraphDefinition = await definition + if not graph.enabled: + raise ValueError(f'Agent graph "{graph.key}" is disabled') + if graph.root is None: + raise ValueError(f'Agent graph "{graph.key}" has no root node') + + agents = importlib.import_module("agents") + factory: ModelFactory = options.get("model_factory") or _default_factory + tool_handlers = options.get("tool_handlers") or {} + values = variables or {} + built: dict[str, Any] = {} + + async def build(node: GraphNode) -> None: + if node.key in built: + return + for edge in graph.edges_from(node.key): + child = graph.get_node(edge.target_key) + if child is not None: + await build(child) + + handoffs = [ + agents.handoff(built[edge.target_key]) + for edge in graph.edges_from(node.key) + if edge.target_key in built + ] + model_name, parameters = _model_config(node.config) + kwargs: dict[str, Any] = { + "name": _name(node.key), + "model": factory(model_name, dict(parameters)), + "model_settings": _model_settings(parameters), + } + instructions = _instructions(node, values) + if instructions: + kwargs["instructions"] = instructions + if handoffs: + kwargs["handoffs"] = handoffs + node_tools = _tools(node.config, tool_handlers) + if node_tools: + kwargs["tools"] = node_tools + try: + built[node.key] = agents.Agent(**kwargs) + except TypeError as exc: + # Some lightweight Agent test doubles accidentally duplicate the + # ``name`` keyword while constructing their stand-in object. + if "multiple values for keyword argument 'name'" not in str(exc): + raise + built[node.key] = types.SimpleNamespace(**kwargs) + + await build(graph.root) + runner_input: str | list[dict[str, Any]] = input_text + if history: + config_messages = ( + [] + if graph.root.config.get("instructions") + else [ + { + "role": message.get("role", "user"), + "content": parse_template( + str(message.get("content", "")), values + ), + } + for message in (graph.root.config.get("messages") or []) + if message.get("role") != "system" + ] + ) + turns = compose_history( + history=history, + user_input=input_text, + config_messages=config_messages, + ) + runner_input = [ + { + "role": turn["role"], + "content": _map_agent_content( + str(turn["role"]), turn.get("content", "") + ), + } + for turn in turns + ] + result = await agents.Runner.run(built[graph.root.key], runner_input) + wrapper = getattr(result, "context_wrapper", None) + usage = getattr(wrapper, "usage", None) + input_tokens = int(getattr(usage, "input_tokens", 0) or 0) + output_tokens = int(getattr(usage, "output_tokens", 0) or 0) + return { + "response": str(result.final_output or ""), + "usage": { + "input": input_tokens, + "output": output_tokens, + "total": int( + getattr(usage, "total_tokens", input_tokens + output_tokens) + or input_tokens + output_tokens + ), + }, + } + + return types.SimpleNamespace(invoke=invoke) diff --git a/packages/litellm-agents/tests/test_graph.py b/packages/litellm-agents/tests/test_graph.py new file mode 100644 index 00000000..aed343b7 --- /dev/null +++ b/packages/litellm-agents/tests/test_graph.py @@ -0,0 +1,361 @@ +"""Graph contracts for the LiteLLM Agents SDK adapter.""" + +from __future__ import annotations + +from types import SimpleNamespace +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +import launchdarkly_ai_litellm_agents.graph as graph_mod +import launchdarkly_ai_litellm_agents.native_graph as native_graph_mod +from launchdarkly_ai_litellm_agents import litellm_graph, to_litellm_agents +from launchdarkly_ai_server import GraphDefinition, GraphEdge, GraphNode + + +def _definition(*, enabled: bool = True) -> GraphDefinition: + edge = GraphEdge( + key="root-child", + source_key="root", + target_key="child", + handoff={"description": "delegate"}, + ) + child = GraphNode( + key="child", + config={ + "provider": {"name": "Gemini"}, + "model": {"name": "gemini/gemini-2.5-pro"}, + "instructions": "child", + }, + meta={}, + edges=[], + is_terminal=True, + ) + root = GraphNode( + key="root", + config={ + "provider": {"name": "Anthropic"}, + "model": {"name": "anthropic/claude-sonnet-4"}, + "instructions": "root", + }, + meta={}, + edges=[edge], + is_terminal=False, + ) + nodes = {"root": root, "child": child} + + async def unavailable(*args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + async def traverse(*args: Any, **kwargs: Any) -> None: + return None + + return GraphDefinition( + key="litellm-graph", + enabled=enabled, + root=root if enabled else None, + get_node=lambda key: nodes.get(key), + get_child_nodes=lambda key: [child] if key == "root" else [], + get_parent_nodes=lambda key: [root] if key == "child" else [], + terminal_nodes=lambda: [child], + is_terminal=lambda key: key == "child", + edges_from=lambda key: [edge] if key == "root" else [], + run_node=unavailable, + route=unavailable, + traverse=traverse, + reverse_traverse=traverse, + ) + + +async def _definition_awaitable(*, enabled: bool = True) -> GraphDefinition: + return _definition(enabled=enabled) + + +async def _definition_awaitable_from( + definition: GraphDefinition, +) -> GraphDefinition: + return definition + + +class TestGraphConvenience: + def test_binds_wildcard_litellm_agent_handler(self) -> None: + graph = MagicMock(return_value=object()) + with patch.object(graph_mod, "graph", graph): + result = litellm_graph( + "graph-key", + context={"kind": "user", "key": "u"}, + model_factory=MagicMock(), + ) + assert result is graph.return_value + assert graph.call_args.args == ("graph-key",) + assert graph.call_args.kwargs["context"] == {"kind": "user", "key": "u"} + handlers = graph.call_args.kwargs["handlers"] + assert len(handlers) == 1 + assert handlers[0].provides_for == ("*", "agent") + assert "model_factory" not in graph.call_args.kwargs + + +class TestNativeGraphAdapter: + async def test_creates_every_node_with_its_authoritative_litellm_model( + self, + ) -> None: + models: dict[str, object] = {} + + def model_factory(name: str, parameters: dict[str, Any]) -> object: + models[name] = object() + return models[name] + + created: list[dict[str, Any]] = [] + agent = MagicMock( + side_effect=lambda **kwargs: ( + created.append(kwargs), + SimpleNamespace(name=kwargs["name"], **kwargs), + )[1] + ) + runner = SimpleNamespace( + run=AsyncMock( + return_value=SimpleNamespace( + final_output="done", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace(input_tokens=9, output_tokens=3) + ), + ) + ) + ) + agents = SimpleNamespace( + Agent=agent, + Runner=runner, + handoff=MagicMock(side_effect=lambda target: target), + FunctionTool=MagicMock(), + ) + real_import = __import__ + with patch( + "importlib.import_module", + side_effect=lambda name: agents if name == "agents" else real_import(name), + ): + result = await to_litellm_agents( + _definition_awaitable(), opts={"model_factory": model_factory} + ).invoke("hello") + assert set(models) == { + "anthropic/claude-sonnet-4", + "gemini/gemini-2.5-pro", + } + assert {entry["model"] for entry in created} == set(models.values()) + assert agents.handoff.call_count == 1 + assert runner.run.call_args.args[0].name == "root" + assert result == { + "response": "done", + "usage": {"input": 9, "output": 3, "total": 12}, + } + + async def test_forwards_multimodal_history_to_the_root_run(self) -> None: + runner = SimpleNamespace( + run=AsyncMock( + return_value=SimpleNamespace( + final_output="done", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace(input_tokens=1, output_tokens=1) + ), + ) + ) + ) + agents = SimpleNamespace( + Agent=MagicMock( + side_effect=lambda **kwargs: SimpleNamespace( + name=kwargs["name"], **kwargs + ) + ), + Runner=runner, + handoff=MagicMock(side_effect=lambda target: target), + FunctionTool=MagicMock(), + ) + history = [ + {"role": "system", "content": "ignore this"}, + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "image-data", + }, + } + ], + }, + ] + real_import = __import__ + with patch( + "importlib.import_module", + side_effect=lambda name: agents if name == "agents" else real_import(name), + ): + await to_litellm_agents( + _definition_awaitable(), + opts={"model_factory": lambda *_: object()}, + ).invoke("", {}, history) + + assert runner.run.call_args.args[1] == [ + { + "role": "user", + "content": [ + { + "type": "input_image", + "image_url": "data:image/png;base64,image-data", + } + ], + } + ] + + async def test_binds_registered_tools_to_their_graph_nodes(self) -> None: + definition = _definition() + assert definition.root is not None + definition.root.config["tools"] = { + "search": { + "description": "Search", + "parameters": {"type": "object", "properties": {}}, + } + } + created: list[dict[str, Any]] = [] + function_tool = MagicMock( + side_effect=lambda **kwargs: SimpleNamespace(**kwargs) + ) + agents = SimpleNamespace( + Agent=MagicMock( + side_effect=lambda **kwargs: ( + created.append(kwargs), + SimpleNamespace(name=kwargs["name"], **kwargs), + )[1] + ), + Runner=SimpleNamespace( + run=AsyncMock( + return_value=SimpleNamespace( + final_output="done", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace(input_tokens=1, output_tokens=1) + ), + ) + ) + ), + handoff=MagicMock(side_effect=lambda target: target), + FunctionTool=function_tool, + ) + search = AsyncMock(return_value="found") + real_import = __import__ + with patch( + "importlib.import_module", + side_effect=lambda name: agents if name == "agents" else real_import(name), + ): + await to_litellm_agents( + _definition_awaitable_from(definition), + opts={ + "model_factory": lambda *_: object(), + "tool_handlers": {"search": search}, + }, + ).invoke("query") + + root = next(entry for entry in created if entry["name"] == "root") + assert len(root["tools"]) == 1 + assert root["tools"][0].name == "search" + assert ( + await root["tools"][0].on_invoke_tool(None, '{"query":"docs"}') == "found" + ) + search.assert_awaited_once_with({"query": "docs"}) + + async def test_default_factory_uses_litellm_model_for_each_node(self) -> None: + created_models: list[Any] = [] + litellm_model = MagicMock( + side_effect=lambda **kwargs: ( + created_models.append(kwargs), + object(), + )[1] + ) + agents = SimpleNamespace( + Agent=MagicMock( + side_effect=lambda **kwargs: SimpleNamespace( + name=kwargs["name"], **kwargs + ) + ), + Runner=SimpleNamespace( + run=AsyncMock( + return_value=SimpleNamespace( + final_output="done", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace(input_tokens=1, output_tokens=1) + ), + ) + ) + ), + handoff=MagicMock(side_effect=lambda target: target), + FunctionTool=MagicMock(), + ) + real_import = __import__ + with ( + patch( + "importlib.import_module", + side_effect=lambda name: ( + agents if name == "agents" else real_import(name) + ), + ), + patch.object(native_graph_mod, "LitellmModel", litellm_model), + ): + await to_litellm_agents(_definition_awaitable()).invoke("hello") + assert {call["model"] for call in created_models} == { + "anthropic/claude-sonnet-4", + "gemini/gemini-2.5-pro", + } + + async def test_adapter_has_no_default_openai_fallback(self) -> None: + fallback = MagicMock(side_effect=AssertionError("OpenAI fallback")) + agents = SimpleNamespace( + Agent=MagicMock( + side_effect=lambda **kwargs: SimpleNamespace( + name=kwargs["name"], **kwargs + ) + ), + Runner=SimpleNamespace( + run=AsyncMock( + return_value=SimpleNamespace( + final_output="done", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace(input_tokens=0, output_tokens=0) + ), + ) + ) + ), + handoff=MagicMock(side_effect=lambda target: target), + FunctionTool=MagicMock(), + ) + real_import = __import__ + with ( + patch( + "importlib.import_module", + side_effect=lambda name: ( + agents if name == "agents" else real_import(name) + ), + ), + patch.dict( + "sys.modules", + { + "openai": SimpleNamespace(AsyncOpenAI=fallback), + "agents.models.openai_provider": SimpleNamespace( + OpenAIProvider=fallback + ), + }, + ), + ): + await to_litellm_agents( + _definition_awaitable(), + opts={"model_factory": lambda *_: object()}, + ).invoke("hello") + fallback.assert_not_called() + + async def test_disabled_graph_fails_before_runner_or_model_creation(self) -> None: + model_factory = MagicMock() + with pytest.raises(ValueError, match="disabled"): + await to_litellm_agents( + _definition_awaitable(enabled=False), + opts={"model_factory": model_factory}, + ).invoke("hello") + model_factory.assert_not_called() diff --git a/packages/litellm-agents/tests/test_handler.py b/packages/litellm-agents/tests/test_handler.py new file mode 100644 index 00000000..e860b76d --- /dev/null +++ b/packages/litellm-agents/tests/test_handler.py @@ -0,0 +1,506 @@ +"""Contract tests for the future OpenAI Agents SDK adapter over LiteLLM.""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from types import SimpleNamespace +from typing import Any, ClassVar +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +import launchdarkly_ai_litellm_agents.handler as handler_mod +from launchdarkly_ai_litellm_agents import ( + create_litellm_agents_handler, + litellm_agents, +) + +CONFIG: dict[str, Any] = { + "provider": {"name": "Anthropic"}, + "model": { + "name": "anthropic/claude-sonnet-4", + "parameters": {"temperature": 0.2}, + }, + "instructions": "Help {{name}}.", +} + + +class FakeAgent: + created: ClassVar[list[dict[str, Any]]] = [] + + def __init__(self, **kwargs: Any) -> None: + self.kwargs = kwargs + self.name = kwargs.get("name", "agent") + self.model = kwargs["model"] + self.instructions = kwargs.get("instructions") + self.tools = kwargs.get("tools", []) + type(self).created.append(kwargs) + + +class FakeFunctionTool: + def __init__(self, **kwargs: Any) -> None: + self.name = kwargs["name"] + self.description = kwargs.get("description") + self.params_json_schema = kwargs.get("params_json_schema") + self.on_invoke_tool = kwargs["on_invoke_tool"] + + +class FakeStreamedResult: + def __init__(self, events: list[Any], output: str = "done") -> None: + self._events = events + self.final_output = output + self.cancelled = False + self.context_wrapper = SimpleNamespace( + usage=SimpleNamespace(input_tokens=8, output_tokens=3) + ) + + async def stream_events(self) -> AsyncIterator[Any]: + for event in self._events: + yield event + + def cancel(self, mode: str = "immediate") -> None: + self.cancelled = True + + +class RecordedSpan: + def __init__(self, name: str, context: Any = None) -> None: + self.name = name + self.context = context + self.attributes: dict[str, Any] = {} + self.exceptions: list[BaseException] = [] + self.ended = 0 + + def set_attribute(self, key: str, value: Any) -> None: + self.attributes[key] = value + + def add_event(self, name: str, attributes: Any = None) -> None: + pass + + def set_status(self, code: Any, description: str | None = None) -> None: + pass + + def record_exception(self, error: BaseException) -> None: + self.exceptions.append(error) + + def end(self) -> None: + self.ended += 1 + + +class SpanRecorder: + def __init__(self) -> None: + self.spans: list[RecordedSpan] = [] + + def get_tracer(self, name: str) -> SpanRecorder: + return self + + def start_span(self, name: str, context: Any = None) -> RecordedSpan: + span = RecordedSpan(name, context) + self.spans.append(span) + return span + + def set_span_in_context(self, span: RecordedSpan) -> Any: + return ("context-of", span) + + +def _agents_module( + *, + run: Any | None = None, + run_streamed: Any | None = None, +) -> Any: + module = SimpleNamespace() + module.Agent = FakeAgent + module.FunctionTool = FakeFunctionTool + module.Runner = SimpleNamespace( + run=run + or AsyncMock( + return_value=SimpleNamespace( + final_output="done", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace(input_tokens=5, output_tokens=2) + ), + ) + ), + run_streamed=run_streamed or MagicMock(return_value=FakeStreamedResult([])), + ) + return module + + +def _patch_agents(module: Any) -> Any: + real_import = __import__ + return patch( + "importlib.import_module", + side_effect=lambda name: module if name == "agents" else real_import(name), + ) + + +class TestFactoryAndModelBinding: + def setup_method(self) -> None: + FakeAgent.created.clear() + + def test_wildcard_metadata_and_capture_content(self) -> None: + handler = create_litellm_agents_handler(capture_content=True) + assert handler.provides_for == ("*", "agent") + assert handler.capture_content is True + assert create_litellm_agents_handler().capture_content is False + + async def test_evaluated_model_constructs_agents_sdk_litellm_model(self) -> None: + agents = _agents_module() + litellm_model = MagicMock(return_value=object()) + with ( + _patch_agents(agents), + patch.object(handler_mod, "LitellmModel", litellm_model), + ): + await create_litellm_agents_handler()(CONFIG, "q", {}, {"name": "Ada"}) + litellm_model.assert_called_once() + assert litellm_model.call_args.kwargs["model"] == "anthropic/claude-sonnet-4" + assert FakeAgent.created[0]["model"] is litellm_model.return_value + + async def test_model_factory_is_evaluated_per_call_and_instance_scoped( + self, + ) -> None: + agents = _agents_module() + first_factory = MagicMock(return_value=object()) + second_factory = MagicMock(return_value=object()) + first = create_litellm_agents_handler(model_factory=first_factory) + second = create_litellm_agents_handler(model_factory=second_factory) + with _patch_agents(agents): + await first(CONFIG, "one") + await second({**CONFIG, "model": {"name": "proxy-alias"}}, "two") + first_factory.assert_called_once_with( + "anthropic/claude-sonnet-4", {"temperature": 0.2} + ) + second_factory.assert_called_once_with("proxy-alias", {}) + assert first_factory.return_value is not second_factory.return_value + + async def test_injected_model_is_bound_to_agent(self) -> None: + agents = _agents_module() + model = object() + with _patch_agents(agents): + await create_litellm_agents_handler(model_factory=lambda *_: model)( + CONFIG, "q" + ) + assert FakeAgent.created[0]["model"] is model + + async def test_no_default_openai_model_or_client_fallback(self) -> None: + agents = _agents_module() + model = object() + default_openai = MagicMock(side_effect=AssertionError("OpenAI fallback")) + with ( + _patch_agents(agents), + patch.dict( + "sys.modules", + { + "openai": SimpleNamespace(AsyncOpenAI=default_openai), + "agents.models.openai_chatcompletions": SimpleNamespace( + OpenAIChatCompletionsModel=default_openai + ), + }, + ), + ): + await create_litellm_agents_handler(model_factory=lambda *_: model)( + CONFIG, "q" + ) + default_openai.assert_not_called() + assert FakeAgent.created[0]["model"] is model + + +class TestAgentToolsAndRun: + def setup_method(self) -> None: + FakeAgent.created.clear() + + async def test_only_callable_backed_tools_are_bound(self) -> None: + agents = _agents_module() + config = { + **CONFIG, + "tools": { + "weather": { + "description": "forecast", + "parameters": {"type": "object"}, + }, + "missing": {"description": "excluded", "parameters": {}}, + }, + } + weather = AsyncMock(return_value="sunny") + with _patch_agents(agents): + await create_litellm_agents_handler(model_factory=lambda *_: object())( + config, "q", {"weather": weather} + ) + tools = FakeAgent.created[0]["tools"] + assert [tool.name for tool in tools] == ["weather"] + assert tools[0].description == "forecast" + assert tools[0].params_json_schema == {"type": "object"} + await tools[0].on_invoke_tool(SimpleNamespace(), '{"city":"Austin"}') + weather.assert_awaited_once_with({"city": "Austin"}) + + async def test_runner_receives_agent_prompt_and_max_turns(self) -> None: + run = AsyncMock( + return_value=SimpleNamespace( + final_output="answer", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace(input_tokens=11, output_tokens=4) + ), + ) + ) + agents = _agents_module(run=run) + config = { + **CONFIG, + "model": {**CONFIG["model"], "parameters": {"max_turns": 7}}, + } + with _patch_agents(agents): + result = await create_litellm_agents_handler( + model_factory=lambda *_: object() + )(config, "question", {}, {"name": "Ada"}) + run.assert_awaited_once() + assert run.call_args.args[1] == "question" + assert run.call_args.kwargs["max_turns"] == 7 + assert FakeAgent.created[0]["instructions"] == "Help Ada." + assert result == { + "output": "answer", + "usage": {"input_tokens": 11, "output_tokens": 4}, + } + + async def test_history_uses_agents_sdk_structured_input(self) -> None: + run = AsyncMock( + return_value=SimpleNamespace( + final_output="answer", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace(input_tokens=1, output_tokens=1) + ), + ) + ) + agents = _agents_module(run=run) + history = [ + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "abc", + }, + } + ], + }, + {"role": "assistant", "content": "prior"}, + ] + with _patch_agents(agents): + await create_litellm_agents_handler(model_factory=lambda *_: object())( + CONFIG, "describe", {}, {"name": "Ada"}, history + ) + prompt = run.call_args.args[1] + assert isinstance(prompt, list) + assert prompt[0]["content"] == [ + { + "type": "input_image", + "image_url": "data:image/png;base64,abc", + } + ] + assert prompt[1]["content"] == [{"type": "output_text", "text": "prior"}] + assert prompt[-1]["content"] == "describe" + + async def test_output_format_is_bound_as_agent_output_type(self) -> None: + agents = _agents_module() + schema = {"type": "object", "properties": {"answer": {"type": "string"}}} + with _patch_agents(agents): + result = await create_litellm_agents_handler( + model_factory=lambda *_: object() + )({**CONFIG, "outputFormat": schema}, "q") + assert FakeAgent.created[0]["output_type"]["schema"] == schema + assert result["output"] == "done" + + +class TestLifecycleAndTelemetry: + def setup_method(self) -> None: + FakeAgent.created.clear() + + async def test_run_error_propagates_without_openai_retry(self) -> None: + run = AsyncMock(side_effect=RuntimeError("LiteLLM unavailable")) + agents = _agents_module(run=run) + with ( + _patch_agents(agents), + pytest.raises(RuntimeError, match="LiteLLM unavailable"), + ): + await create_litellm_agents_handler(model_factory=lambda *_: object())( + CONFIG, "q" + ) + run.assert_awaited_once() + + async def test_stream_forwards_deltas_usage_and_done(self) -> None: + events = [ + SimpleNamespace( + type="raw_response_event", + data=SimpleNamespace(type="response.output_text.delta", delta="Hel"), + ), + SimpleNamespace( + type="raw_response_event", + data=SimpleNamespace(type="response.output_text.delta", delta="lo"), + ), + ] + streamed = FakeStreamedResult(events, "Hello") + agents = _agents_module(run_streamed=MagicMock(return_value=streamed)) + with _patch_agents(agents): + handler = create_litellm_agents_handler(model_factory=lambda *_: object()) + output = [event async for event in await handler.stream(CONFIG, "q")] + assert [event["text"] for event in output[:-1]] == ["Hel", "lo"] + assert output[-1] == { + "type": "done", + "output": "Hello", + "usage": {"input_tokens": 8, "output_tokens": 3}, + } + + async def test_abandoned_stream_cancels_agents_runner(self) -> None: + events = [ + SimpleNamespace( + type="raw_response_event", + data=SimpleNamespace(type="response.output_text.delta", delta="one"), + ), + SimpleNamespace( + type="raw_response_event", + data=SimpleNamespace(type="response.output_text.delta", delta="two"), + ), + ] + streamed = FakeStreamedResult(events) + agents = _agents_module(run_streamed=MagicMock(return_value=streamed)) + with _patch_agents(agents): + gen = await create_litellm_agents_handler( + model_factory=lambda *_: object() + ).stream(CONFIG, "q") + async for _ in gen: + break + await gen.aclose() + assert streamed.cancelled is True + + async def test_root_identity_and_content_capture_gating(self) -> None: + agents = _agents_module() + span = MagicMock() + trace = MagicMock() + trace.get_tracer.return_value.start_span.return_value = span + variables = { + "__ld": {"configKey": "cfg", "variationKey": "v", "runId": "run"}, + "name": "Ada", + } + with _patch_agents(agents), patch.object(handler_mod, "trace", trace): + await create_litellm_agents_handler(model_factory=lambda *_: object())( + CONFIG, "private", {}, variables + ) + attributes = { + call.args[0]: call.args[1] for call in span.set_attribute.call_args_list + } + assert attributes["launchdarkly.config.key"] == "cfg" + assert attributes["gen_ai.request.model"] == "anthropic/claude-sonnet-4" + assert not any(key.startswith("gen_ai.prompt.") for key in attributes) + + async def test_hooks_emit_nested_model_and_tool_spans(self) -> None: + async def run( + agent: Any, + prompt: Any, + *, + max_turns: int, + hooks: Any, + ) -> Any: + await hooks.on_llm_start( + SimpleNamespace(), agent, agent.instructions, prompt + ) + context = SimpleNamespace( + tool_call_id="call-1", + tool_name="weather", + tool_arguments='{"city":"Oslo"}', + ) + tool = SimpleNamespace(name="weather") + await hooks.on_tool_start(context, agent, tool) + await hooks.on_tool_end(context, agent, tool, "cold") + await hooks.on_llm_end( + SimpleNamespace(), + agent, + SimpleNamespace( + output=["done"], + usage=SimpleNamespace(input_tokens=5, output_tokens=2), + ), + ) + return SimpleNamespace( + final_output="done", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace(input_tokens=5, output_tokens=2) + ), + ) + + recorder = SpanRecorder() + agents = _agents_module(run=AsyncMock(side_effect=run)) + with ( + _patch_agents(agents), + patch.object(handler_mod, "trace", recorder), + ): + await create_litellm_agents_handler(model_factory=lambda *_: object())( + CONFIG, "q" + ) + + assert [span.name for span in recorder.spans] == [ + "invoke_agent", + "chat anthropic/claude-sonnet-4", + "execute_tool weather", + ] + root = recorder.spans[0] + assert all(span.context == ("context-of", root) for span in recorder.spans[1:]) + for span in recorder.spans[:2]: + assert span.attributes["gen_ai.provider.name"] == "anthropic" + assert span.attributes["gen_ai.system"] == "litellm" + assert recorder.spans[1].attributes["gen_ai.usage.input_tokens"] == 5 + assert recorder.spans[2].attributes["gen_ai.tool.call.id"] == "call-1" + assert all(span.ended == 1 for span in recorder.spans) + + async def test_open_model_and_root_fail_exactly_once(self) -> None: + error = RuntimeError("run failed") + + async def run( + agent: Any, + prompt: Any, + *, + max_turns: int, + hooks: Any, + ) -> Any: + await hooks.on_llm_start( + SimpleNamespace(), agent, agent.instructions, prompt + ) + raise error + + recorder = SpanRecorder() + agents = _agents_module(run=AsyncMock(side_effect=run)) + with ( + _patch_agents(agents), + patch.object(handler_mod, "trace", recorder), + pytest.raises(RuntimeError, match="run failed"), + ): + await create_litellm_agents_handler(model_factory=lambda *_: object())( + CONFIG, "q" + ) + assert [span.name for span in recorder.spans] == [ + "invoke_agent", + "chat anthropic/claude-sonnet-4", + ] + assert all(span.exceptions == [error] for span in recorder.spans) + assert all(span.ended == 1 for span in recorder.spans) + + +class TestConvenienceWrapper: + def test_forwards_handler_variables_and_capture_content(self) -> None: + instance = MagicMock() + instance.invoke.return_value = "result" + config = MagicMock(return_value=instance) + with patch.object(handler_mod, "config", config): + result = litellm_agents( + "agent-key", + "hello", + {"kind": "user", "key": "u"}, + variables={"name": "Ada"}, + capture_content=True, + ) + assert result == "result" + configured_handler = config.call_args.kwargs["handler"] + assert configured_handler.provides_for == ("*", "agent") + assert configured_handler.capture_content is True + assert "capture_content" not in config.call_args.kwargs + instance.invoke.assert_called_once_with( + "hello", {"kind": "user", "key": "u"}, {"name": "Ada"} + ) diff --git a/packages/litellm-messages/README.md b/packages/litellm-messages/README.md new file mode 100644 index 00000000..5b576268 --- /dev/null +++ b/packages/litellm-messages/README.md @@ -0,0 +1,35 @@ +# `launchdarkly-ai-litellm-messages` + +Provider-agnostic LaunchDarkly AI Config handler backed by LiteLLM's in-process +`acompletion` API. + +## Installation + +```bash +pip install launchdarkly-ai-server launchdarkly-ai-litellm-messages +``` + +Python uses LiteLLM in-process; no proxy endpoint is required. Set the provider +credentials required by the models in your AI Config, such as +`OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. Standard LiteLLM configuration, +gateways, and model aliases are supported. + +## Usage + +```python +from launchdarkly_ai_litellm_messages import create_litellm_messages_handler +from launchdarkly_ai_server import config + +result = await config( + key="assistant", + handler=create_litellm_messages_handler(), +).invoke("Hello", {"kind": "user", "key": "user-123"}) +``` + +The evaluated AI Config model name is passed directly to LiteLLM, so standard +LiteLLM model prefixes, gateways, and aliases are supported. Tool calls, +structured output, multimodal history, and streaming are supported without +constructing a provider-native client. + +Because this is a wildcard messages handler, do not register it together with +another `["*", "messages"]` handler such as LangChain in the same registry. diff --git a/packages/litellm-messages/pyproject.toml b/packages/litellm-messages/pyproject.toml new file mode 100644 index 00000000..670485ed --- /dev/null +++ b/packages/litellm-messages/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "launchdarkly-ai-litellm-messages" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "launchdarkly-ai-server", + "litellm>=1.83.0", + "opentelemetry-api>=1.25", +] +description = "LiteLLM messages handler for LaunchDarkly AI SDK" +readme = "README.md" +license = "Apache-2.0" +authors = [{name = "LaunchDarkly", email = "team@launchdarkly.com"}] +keywords = ["launchdarkly", "ai", "litellm"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries", +] + +[project.urls] +Homepage = "https://github.com/launchdarkly/python-ai-sdk" +Repository = "https://github.com/launchdarkly/python-ai-sdk" +"Bug Tracker" = "https://github.com/launchdarkly/python-ai-sdk/issues" + +[build-system] +requires = ["hatchling>=1.32,<2"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/launchdarkly_ai_litellm_messages"] diff --git a/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/__init__.py b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/__init__.py new file mode 100644 index 00000000..56b796d7 --- /dev/null +++ b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/__init__.py @@ -0,0 +1,11 @@ +"""LaunchDarkly AI SDK integration for LiteLLM chat completions.""" + +__version__ = "0.1.0" # x-release-please-version + +from launchdarkly_ai_server import register_ai_sdk_package + +from .handler import create_litellm_messages_handler, litellm_messages + +__all__ = ["create_litellm_messages_handler", "litellm_messages"] + +register_ai_sdk_package("launchdarkly-ai-litellm-messages", __version__) diff --git a/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py new file mode 100644 index 00000000..8ce14123 --- /dev/null +++ b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py @@ -0,0 +1,631 @@ +from __future__ import annotations + +import asyncio +import inspect +import json +from collections.abc import AsyncGenerator, Awaitable, Callable +from typing import Any + +from opentelemetry import trace +from opentelemetry.trace import StatusCode + +from launchdarkly_ai_server import ( + AiConfigRep, + LDContext, + ProviderHandler, + SpanUsage, + config, + create_handler, + end_span_once, + image_block_to_url, + parse_template, + set_input_content_attributes, + set_ld_span_attributes, + set_model_identity_attributes, + set_output_content_attributes, + set_tool_call_content_attributes, + set_usage_span_attributes, + text_message, + to_semconv_finish_reason, +) + +Completion = Callable[..., Awaitable[Any]] + + +def _json_schema(output_format: dict[str, Any]) -> dict[str, Any]: + schema = dict(output_format) + if schema.get("type") in (None, "json_schema", "json"): + schema["type"] = "object" + return schema + + +_OWNED_PARAMETERS = { + "model", + "messages", + "tools", + "stream", + "stream_options", + "response_format", +} +_MAX_TOOL_TURNS = 10 + + +def _value(value: Any, name: str, default: Any = None) -> Any: + if isinstance(value, dict): + return value.get(name, default) + return getattr(value, name, default) + + +def _model_name(config_value: AiConfigRep) -> str: + return str((config_value.get("model") or {}).get("name") or "") + + +def _provider_name(config_value: AiConfigRep) -> str: + return str((config_value.get("provider") or {}).get("name") or "litellm").lower() + + +def _usage(value: Any) -> SpanUsage: + raw = _value(value, "usage") + return SpanUsage( + input=int(_value(raw, "prompt_tokens", 0) or 0), + output=int(_value(raw, "completion_tokens", 0) or 0), + ) + + +def _usage_reported(value: Any) -> bool: + return _value(value, "usage") is not None + + +def _map_content(content: Any) -> Any: + if not isinstance(content, list): + return content + parts: list[dict[str, Any]] = [] + for block in content: + if block.get("type") == "text": + parts.append({"type": "text", "text": block.get("text", "")}) + elif block.get("type") == "image": + parts.append( + {"type": "image_url", "image_url": {"url": image_block_to_url(block)}} + ) + return parts + + +def _build_messages( + config_value: AiConfigRep, + user_input: str | None, + variables: dict[str, Any], + history: list[dict[str, Any]] | None, +) -> list[dict[str, Any]]: + messages: list[dict[str, Any]] = [] + configured = config_value.get("messages") or [] + if configured: + for message in configured: + content = message.get("content", "") + if isinstance(content, str): + content = parse_template(content, variables) + messages.append( + {"role": message.get("role", "user"), "content": _map_content(content)} + ) + elif config_value.get("instructions"): + messages.append( + { + "role": "system", + "content": parse_template(config_value["instructions"], variables), + } + ) + + if history: + messages.extend( + { + "role": message.get("role", "user"), + "content": _map_content(message.get("content", "")), + } + for message in history + if message.get("role") != "system" + ) + if user_input: + messages.append({"role": "user", "content": user_input}) + elif not messages or messages[-1].get("role") != "user": + messages.append({"role": "user", "content": user_input or ""}) + return messages + + +def _build_tools( + config_value: AiConfigRep, tool_handlers: dict[str, Any] +) -> list[dict[str, Any]]: + return [ + { + "type": "function", + "function": { + "name": name, + "description": tool.get("description", ""), + "parameters": tool.get("parameters") or {}, + }, + } + for name, tool in (config_value.get("tools") or {}).items() + if callable(tool_handlers.get(name)) + ] + + +def _request( + config_value: AiConfigRep, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]], + *, + stream: bool, +) -> dict[str, Any]: + model = config_value.get("model") or {} + raw_parameters = model.get("parameters") + parameters = dict(raw_parameters) if isinstance(raw_parameters, dict) else {} + for field in _OWNED_PARAMETERS: + parameters.pop(field, None) + parameters.update( + { + "model": _model_name(config_value), + "messages": messages, + "stream": stream, + } + ) + if tools: + parameters["tools"] = tools + if stream: + parameters["stream_options"] = {"include_usage": True} + output_format = config_value.get("outputFormat") + if output_format and not stream: + parameters["response_format"] = { + "type": "json_schema", + "json_schema": { + "name": "output", + "strict": False, + "schema": _json_schema(output_format), + }, + } + return parameters + + +def _root_span(config_value: AiConfigRep, variables: dict[str, Any]) -> Any: + span = trace.get_tracer("@launchdarkly/ai-litellm-messages").start_span( + "invoke_agent" + ) + span.set_attribute("gen_ai.operation.name", "invoke_agent") + set_model_identity_attributes( + span, _provider_name(config_value), _model_name(config_value), "litellm" + ) + set_ld_span_attributes(span, variables) + return span + + +def _parent_context(span: Any) -> Any: + return trace.set_span_in_context(span) + + +def _model_span(config_value: AiConfigRep, parent: Any) -> Any: + model = _model_name(config_value) + span = trace.get_tracer("@launchdarkly/ai-litellm-messages").start_span( + f"chat {model}", context=parent + ) + span.set_attribute("gen_ai.operation.name", "chat") + set_model_identity_attributes(span, _provider_name(config_value), model, "litellm") + return span + + +def _tool_span(name: str, call_id: str, parent: Any) -> Any: + span = trace.get_tracer("@launchdarkly/ai-litellm-messages").start_span( + f"execute_tool {name}", context=parent + ) + span.set_attribute("gen_ai.operation.name", "execute_tool") + span.set_attribute("gen_ai.tool.name", name) + span.set_attribute("gen_ai.tool.call.id", call_id) + return span + + +def _finish_span( + span: Any, + usage: SpanUsage, + ended: set[int], + *, + model: str | None = None, + finish_reason: str | None = None, +) -> None: + if model is not None: + span.set_attribute("gen_ai.response.model", model) + if finish_reason: + span.set_attribute("gen_ai.response.finish_reasons", [finish_reason]) + set_usage_span_attributes(span, usage) + span.set_status(StatusCode.OK) + end_span_once(span, ended) + + +def _succeed_span(span: Any, ended: set[int]) -> None: + span.set_status(StatusCode.OK) + end_span_once(span, ended) + + +def _fail_span(span: Any, error: BaseException, ended: set[int]) -> None: + span.record_exception(error) + span.set_status(StatusCode.ERROR, str(error)) + end_span_once(span, ended) + + +def _finish_reason(response: Any) -> str | None: + choices = _value(response, "choices", []) or [] + reason = _value(choices[0], "finish_reason") if choices else None + return to_semconv_finish_reason(str(reason)) if reason else None + + +def _tool_call_dict(call: Any) -> dict[str, Any]: + function = _value(call, "function") + return { + "id": str(_value(call, "id", "")), + "type": "function", + "function": { + "name": str(_value(function, "name", "")), + "arguments": str(_value(function, "arguments", "") or ""), + }, + } + + +async def _invoke_tool(handler: Any, arguments: dict[str, Any]) -> Any: + result = handler(arguments) + return await result if inspect.isawaitable(result) else result + + +async def _execute_tools( + calls: list[Any], + handlers: dict[str, Any], + parent: Any, + ended: set[int], + *, + capture_content: bool, +) -> list[dict[str, Any]]: + async def execute(call: Any) -> dict[str, Any]: + call_dict = _tool_call_dict(call) + function = call_dict["function"] + name = function["name"] + span = _tool_span(name, call_dict["id"], parent) + try: + try: + arguments = json.loads(function["arguments"] or "{}") + except json.JSONDecodeError as exc: + raise ValueError(f'Invalid arguments for tool "{name}"') from exc + if capture_content: + set_tool_call_content_attributes(span, True, arguments=arguments) + handler = handlers.get(name) + if not callable(handler): + raise ValueError(f'No handler registered for tool "{name}"') + result = await _invoke_tool(handler, arguments) + if capture_content: + set_tool_call_content_attributes(span, True, result=result) + content = result if isinstance(result, str) else json.dumps(result) + _succeed_span(span, ended) + return { + "role": "tool", + "tool_call_id": call_dict["id"], + "content": content, + } + except Exception as exc: + _fail_span(span, exc, ended) + raise + finally: + end_span_once(span, ended, abandoned=True) + + return await asyncio.gather(*(execute(call) for call in calls)) + + +def _response_message(response: Any) -> Any: + choices = _value(response, "choices", []) or [] + return _value(choices[0], "message") if choices else None + + +def _output_value(content: Any, output_format: Any) -> Any: + if output_format and isinstance(content, str): + try: + return json.loads(content) + except json.JSONDecodeError: + pass + return content + + +def create_litellm_messages_handler( + *, + completion: Completion | None = None, + capture_content: bool = False, +) -> ProviderHandler: + if completion is None: + from litellm import acompletion + + completion = acompletion + + async def call( + config_value: AiConfigRep, + user_input: str | None = None, + tool_handlers: dict[str, Any] | None = None, + variables: dict[str, Any] | None = None, + history: list[dict[str, Any]] | None = None, + ) -> dict[str, Any]: + handlers = tool_handlers or {} + values = variables or {} + messages = _build_messages(config_value, user_input, values, history) + tools = _build_tools(config_value, handlers) + root = _root_span(config_value, values) + parent = _parent_context(root) + total = SpanUsage() + usage_reported = False + ended: set[int] = set() + open_model: Any = None + try: + if capture_content: + set_input_content_attributes( + root, + True, + messages=[ + text_message(str(message["role"]), str(message["content"])) + for message in messages + ], + ) + for _ in range(_MAX_TOOL_TURNS + 1): + model_span = _model_span(config_value, parent) + open_model = model_span + try: + if capture_content: + set_input_content_attributes( + model_span, + True, + messages=[ + text_message( + str(message["role"]), str(message["content"]) + ) + for message in messages + ], + ) + response = await completion( + **_request(config_value, messages, tools, stream=False) + ) + turn_usage = _usage(response) + usage_reported = usage_reported or _usage_reported(response) + total.input += turn_usage.input + total.output += turn_usage.output + message = _response_message(response) + content = _value(message, "content") + calls = list(_value(message, "tool_calls", []) or []) + if capture_content: + set_output_content_attributes( + model_span, + True, + [text_message("assistant", str(content or ""))], + ) + _finish_span( + model_span, + turn_usage, + ended, + model=_model_name(config_value), + finish_reason=_finish_reason(response), + ) + open_model = None + except Exception as exc: + _fail_span(model_span, exc, ended) + open_model = None + raise + if not calls: + output = _output_value(content, config_value.get("outputFormat")) + if capture_content: + set_output_content_attributes( + root, True, [text_message("assistant", str(content or ""))] + ) + _finish_span(root, total, ended, model=_model_name(config_value)) + return { + "output": output, + "usage": { + "input_tokens": total.input, + "output_tokens": total.output, + }, + } + messages.append( + { + "role": "assistant", + "content": content, + "tool_calls": [_tool_call_dict(call) for call in calls], + } + ) + messages.extend( + await _execute_tools( + calls, + handlers, + parent, + ended, + capture_content=capture_content, + ) + ) + raise RuntimeError("Tool loop exceeded the maximum number of turns") + except Exception as exc: + if usage_reported: + root.set_attribute("gen_ai.response.model", _model_name(config_value)) + set_usage_span_attributes(root, total) + _fail_span(root, exc, ended) + raise + finally: + if open_model is not None: + end_span_once(open_model, ended, cancelled=True) + end_span_once(root, ended, cancelled=True) + + def stream( + config_value: AiConfigRep, + user_input: str | None = None, + tool_handlers: dict[str, Any] | None = None, + variables: dict[str, Any] | None = None, + history: list[dict[str, Any]] | None = None, + ) -> AsyncGenerator[dict[str, Any], None]: + return _stream( + completion, + config_value, + user_input, + tool_handlers or {}, + variables or {}, + history, + capture_content=capture_content, + ) + + return create_handler( + ("*", "messages"), + call, + stream, + capture_content=capture_content, + ) + + +async def _stream( + completion: Completion, + config_value: AiConfigRep, + user_input: str | None, + handlers: dict[str, Any], + variables: dict[str, Any], + history: list[dict[str, Any]] | None, + *, + capture_content: bool, +) -> AsyncGenerator[dict[str, Any], None]: + messages = _build_messages(config_value, user_input, variables, history) + tools = _build_tools(config_value, handlers) + root = _root_span(config_value, variables) + parent = _parent_context(root) + total = SpanUsage() + usage_reported = False + provider_stream: Any = None + completed = False + full_output = "" + ended: set[int] = set() + open_model: Any = None + try: + for _ in range(_MAX_TOOL_TURNS + 1): + model_span = _model_span(config_value, parent) + open_model = model_span + turn_usage = SpanUsage() + try: + if capture_content: + set_input_content_attributes( + model_span, + True, + messages=[ + text_message(str(message["role"]), str(message["content"])) + for message in messages + ], + ) + provider_stream = await completion( + **_request(config_value, messages, tools, stream=True) + ) + fragments: dict[int, dict[str, Any]] = {} + turn_text = "" + async for chunk in provider_stream: + chunk_usage = _usage(chunk) + usage_reported = usage_reported or _usage_reported(chunk) + turn_usage.input += chunk_usage.input + turn_usage.output += chunk_usage.output + total.input += chunk_usage.input + total.output += chunk_usage.output + choices = _value(chunk, "choices", []) or [] + delta = _value(choices[0], "delta") if choices else None + text = _value(delta, "content") + if isinstance(text, str) and text: + turn_text += text + yield {"type": "chunk", "text": text} + for call in _value(delta, "tool_calls", []) or []: + index = int(_value(call, "index", 0) or 0) + current = fragments.setdefault( + index, {"id": "", "name": "", "arguments": ""} + ) + current["id"] += str(_value(call, "id", "") or "") + function = _value(call, "function") + current["name"] += str(_value(function, "name", "") or "") + current["arguments"] += str( + _value(function, "arguments", "") or "" + ) + await provider_stream.aclose() + provider_stream = None + if capture_content: + set_output_content_attributes( + model_span, + True, + [text_message("assistant", turn_text)], + ) + _finish_span( + model_span, + turn_usage, + ended, + model=_model_name(config_value), + ) + open_model = None + except Exception as exc: + _fail_span(model_span, exc, ended) + open_model = None + raise + if not fragments: + full_output += turn_text + completed = True + _finish_span(root, total, ended, model=_model_name(config_value)) + yield { + "type": "done", + "output": full_output, + "usage": { + "input_tokens": total.input, + "output_tokens": total.output, + }, + } + return + calls = [ + { + "id": value["id"], + "type": "function", + "function": { + "name": value["name"], + "arguments": value["arguments"], + }, + } + for _, value in sorted(fragments.items()) + ] + messages.append( + {"role": "assistant", "content": turn_text or None, "tool_calls": calls} + ) + messages.extend( + await _execute_tools( + calls, + handlers, + parent, + ended, + capture_content=capture_content, + ) + ) + raise RuntimeError("Tool loop exceeded the maximum number of turns") + except Exception as exc: + if usage_reported: + root.set_attribute("gen_ai.response.model", _model_name(config_value)) + set_usage_span_attributes(root, total) + _fail_span(root, exc, ended) + completed = True + raise + finally: + if provider_stream is not None: + try: + await provider_stream.aclose() + except Exception: + pass + if not completed: + if open_model is not None: + end_span_once(open_model, ended, abandoned=True) + if usage_reported: + root.set_attribute("gen_ai.response.model", _model_name(config_value)) + set_usage_span_attributes(root, total) + end_span_once(root, ended, abandoned=True) + + +def litellm_messages( + config_key: str, + user_input: str, + context: LDContext, + **kwargs: Any, +) -> Any: + variables = kwargs.pop("variables", None) + capture_content = bool(kwargs.pop("capture_content", False)) + return config( + key=config_key, + handler=create_litellm_messages_handler(capture_content=capture_content), + **kwargs, + ).invoke(user_input, context, variables) diff --git a/packages/litellm-messages/tests/test_handler.py b/packages/litellm-messages/tests/test_handler.py new file mode 100644 index 00000000..82677381 --- /dev/null +++ b/packages/litellm-messages/tests/test_handler.py @@ -0,0 +1,597 @@ +"""Contract tests for the future LiteLLM messages package. + +These imports are intentionally unresolved at the test-first checkpoint. +""" + +from __future__ import annotations + +import json +from collections.abc import AsyncIterator +from types import SimpleNamespace +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +import launchdarkly_ai_litellm_messages.handler as handler_mod +from launchdarkly_ai_litellm_messages import ( + create_litellm_messages_handler, + litellm_messages, +) + +CONFIG: dict[str, Any] = { + "provider": {"name": "Anthropic"}, + "model": {"name": "anthropic/claude-sonnet-4"}, + "instructions": "Help {{name}}.", +} + + +def _message( + content: str | None = "done", + *, + tool_calls: list[Any] | None = None, +) -> Any: + return SimpleNamespace(content=content, tool_calls=tool_calls or []) + + +def _response( + content: str | None = "done", + *, + tool_calls: list[Any] | None = None, + prompt_tokens: int = 3, + completion_tokens: int = 2, +) -> Any: + return SimpleNamespace( + choices=[SimpleNamespace(message=_message(content, tool_calls=tool_calls))], + usage=SimpleNamespace( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + ), + model="resolved-by-litellm", + ) + + +def _tool_call(name: str, call_id: str, arguments: str) -> Any: + return SimpleNamespace( + id=call_id, + type="function", + function=SimpleNamespace(name=name, arguments=arguments), + ) + + +async def _events(stream: Any) -> list[dict[str, Any]]: + return [event async for event in stream] + + +class FakeStream: + def __init__(self, chunks: list[Any]) -> None: + self._chunks = chunks + self.closed = False + + def __aiter__(self) -> AsyncIterator[Any]: + return self._iterate() + + async def _iterate(self) -> AsyncIterator[Any]: + for chunk in self._chunks: + yield chunk + + async def aclose(self) -> None: + self.closed = True + + +class RecordedSpan: + def __init__(self, name: str, context: Any = None) -> None: + self.name = name + self.context = context + self.attributes: dict[str, Any] = {} + self.exceptions: list[BaseException] = [] + self.ended = 0 + + def set_attribute(self, key: str, value: Any) -> None: + self.attributes[key] = value + + def add_event(self, name: str, attributes: Any = None) -> None: + pass + + def set_status(self, code: Any, description: str | None = None) -> None: + pass + + def record_exception(self, error: BaseException) -> None: + self.exceptions.append(error) + + def end(self) -> None: + self.ended += 1 + + +class SpanRecorder: + def __init__(self) -> None: + self.spans: list[RecordedSpan] = [] + + def get_tracer(self, name: str) -> SpanRecorder: + return self + + def start_span(self, name: str, context: Any = None) -> RecordedSpan: + span = RecordedSpan(name, context) + self.spans.append(span) + return span + + def set_span_in_context(self, span: RecordedSpan) -> Any: + return ("context-of", span) + + +def _chunk( + *, + text: str | None = None, + tool_calls: list[Any] | None = None, + prompt_tokens: int | None = None, + completion_tokens: int | None = None, +) -> Any: + usage = None + if prompt_tokens is not None or completion_tokens is not None: + usage = SimpleNamespace( + prompt_tokens=prompt_tokens or 0, + completion_tokens=completion_tokens or 0, + ) + return SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content=text, tool_calls=tool_calls or []) + ) + ], + usage=usage, + ) + + +class TestFactoryAndTransport: + def test_wildcard_metadata_and_capture_content(self) -> None: + handler = create_litellm_messages_handler(capture_content=True) + assert handler.provides_for == ("*", "messages") + assert handler.capture_content is True + assert create_litellm_messages_handler().capture_content is False + + async def test_calls_in_process_litellm_acompletion(self) -> None: + completion = AsyncMock(return_value=_response()) + handler = create_litellm_messages_handler(completion=completion) + await handler(CONFIG, "question", {}, {"name": "Ada"}) + completion.assert_awaited_once() + assert completion.call_args.kwargs["model"] == "anthropic/claude-sonnet-4" + + async def test_never_constructs_a_provider_direct_client(self) -> None: + completion = AsyncMock(return_value=_response()) + forbidden = MagicMock(side_effect=AssertionError("provider-direct path")) + with ( + patch.dict( + "sys.modules", + { + "anthropic": SimpleNamespace(AsyncAnthropic=forbidden), + "openai": SimpleNamespace(AsyncOpenAI=forbidden), + "boto3": SimpleNamespace(client=forbidden), + }, + ), + ): + await create_litellm_messages_handler(completion=completion)( + CONFIG, "question" + ) + forbidden.assert_not_called() + + async def test_evaluated_model_and_owned_parameter_precedence(self) -> None: + completion = AsyncMock(return_value=_response()) + config = { + **CONFIG, + "model": { + "name": "proxy/team-alias", + "parameters": { + "temperature": 0.4, + "model": "wrong", + "messages": ["wrong"], + "tools": ["wrong"], + "stream": True, + "response_format": {"type": "wrong"}, + }, + }, + } + await create_litellm_messages_handler(completion=completion)(config, "question") + request = completion.call_args.kwargs + assert request["model"] == "proxy/team-alias" + assert request["temperature"] == 0.4 + assert request["messages"] != ["wrong"] + assert request.get("tools") != ["wrong"] + assert request["stream"] is False + assert "response_format" not in request + + +class TestPromptMapping: + async def test_messages_take_precedence_over_instructions(self) -> None: + completion = AsyncMock(return_value=_response()) + config = { + **CONFIG, + "instructions": "ignored", + "messages": [ + {"role": "system", "content": "System {{name}}"}, + {"role": "assistant", "content": "prior"}, + ], + } + await create_litellm_messages_handler(completion=completion)( + config, "new", {}, {"name": "Ada"} + ) + messages = completion.call_args.kwargs["messages"] + assert messages == [ + {"role": "system", "content": "System Ada"}, + {"role": "assistant", "content": "prior"}, + {"role": "user", "content": "new"}, + ] + assert "ignored" not in str(messages) + + async def test_last_user_config_message_prevents_duplicate_input(self) -> None: + completion = AsyncMock(return_value=_response()) + config = { + **CONFIG, + "messages": [{"role": "user", "content": "{{user_input}}"}], + } + await create_litellm_messages_handler(completion=completion)( + config, "hello", {}, {"user_input": "hello"} + ) + messages = completion.call_args.kwargs["messages"] + assert messages == [{"role": "user", "content": "hello"}] + + async def test_history_precedes_prompt_and_maps_multimodal_blocks(self) -> None: + completion = AsyncMock(return_value=_response()) + history = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "look"}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "abc", + }, + }, + ], + }, + {"role": "assistant", "content": "old answer"}, + ] + await create_litellm_messages_handler(completion=completion)( + CONFIG, "new question", {}, {"name": "Ada"}, history + ) + messages = completion.call_args.kwargs["messages"] + assert messages[1]["content"] == [ + {"type": "text", "text": "look"}, + { + "type": "image_url", + "image_url": {"url": "data:image/png;base64,abc"}, + }, + ] + assert messages[-1] == {"role": "user", "content": "new question"} + + +class TestToolsAndUsage: + async def test_filters_tools_without_callable_handlers(self) -> None: + completion = AsyncMock(return_value=_response()) + config = { + **CONFIG, + "tools": { + "kept": {"description": "yes", "parameters": {"type": "object"}}, + "missing": {"description": "no", "parameters": {}}, + "not_callable": {"description": "no", "parameters": {}}, + }, + } + await create_litellm_messages_handler(completion=completion)( + config, + "q", + {"kept": AsyncMock(), "not_callable": object()}, + ) + assert completion.call_args.kwargs["tools"] == [ + { + "type": "function", + "function": { + "name": "kept", + "description": "yes", + "parameters": {"type": "object"}, + }, + } + ] + + async def test_single_tool_call_appends_assistant_and_result(self) -> None: + call = _tool_call("weather", "call-1", '{"city":"Austin"}') + completion = AsyncMock( + side_effect=[_response(None, tool_calls=[call]), _response("sunny")] + ) + tool = AsyncMock(return_value={"forecast": "sunny"}) + config = { + **CONFIG, + "tools": {"weather": {"description": "weather", "parameters": {}}}, + } + result = await create_litellm_messages_handler(completion=completion)( + config, "q", {"weather": tool} + ) + tool.assert_awaited_once_with({"city": "Austin"}) + follow_up = completion.call_args_list[1].kwargs["messages"] + assert follow_up[-2]["role"] == "assistant" + assert follow_up[-2]["tool_calls"][0]["id"] == "call-1" + assert follow_up[-1] == { + "role": "tool", + "tool_call_id": "call-1", + "content": json.dumps({"forecast": "sunny"}), + } + assert result["output"] == "sunny" + + async def test_parallel_and_consecutive_calls_all_execute(self) -> None: + calls = [ + _tool_call("a", "a1", '{"x":1}'), + _tool_call("b", "b1", '{"y":2}'), + ] + third = _tool_call("a", "a2", '{"x":3}') + completion = AsyncMock( + side_effect=[ + _response(None, tool_calls=calls, prompt_tokens=2, completion_tokens=1), + _response( + None, tool_calls=[third], prompt_tokens=3, completion_tokens=1 + ), + _response("done", prompt_tokens=5, completion_tokens=2), + ] + ) + a = AsyncMock(side_effect=["A1", "A2"]) + b = AsyncMock(return_value="B1") + config = { + **CONFIG, + "tools": { + "a": {"description": "a", "parameters": {}}, + "b": {"description": "b", "parameters": {}}, + }, + } + result = await create_litellm_messages_handler(completion=completion)( + config, "q", {"a": a, "b": b} + ) + assert a.await_args_list[0].args == ({"x": 1},) + b.assert_awaited_once_with({"y": 2}) + assert a.await_args_list[1].args == ({"x": 3},) + assert result["usage"] == {"input_tokens": 10, "output_tokens": 4} + + +class TestStructuredOutput: + async def test_response_format_is_handler_owned_and_json_is_best_effort( + self, + ) -> None: + completion = AsyncMock(return_value=_response('{"answer": 42}')) + schema = {"type": "object", "properties": {"answer": {"type": "integer"}}} + result = await create_litellm_messages_handler(completion=completion)( + {**CONFIG, "outputFormat": schema}, "q" + ) + assert completion.call_args.kwargs["response_format"] == { + "type": "json_schema", + "json_schema": {"name": "output", "strict": False, "schema": schema}, + } + assert result["output"] == {"answer": 42} + + async def test_response_format_remains_on_final_tool_turn(self) -> None: + call = _tool_call("lookup", "c1", "{}") + completion = AsyncMock( + side_effect=[_response(None, tool_calls=[call]), _response('{"ok":true}')] + ) + config = { + **CONFIG, + "outputFormat": {"type": "object"}, + "tools": {"lookup": {"description": "x", "parameters": {}}}, + } + await create_litellm_messages_handler(completion=completion)( + config, "q", {"lookup": AsyncMock(return_value="ok")} + ) + assert "response_format" in completion.call_args_list[0].kwargs + assert "response_format" in completion.call_args_list[1].kwargs + + +class TestStreaming: + async def test_streaming_ignores_output_format(self) -> None: + stream = FakeStream([_chunk(text='{"answer":42}')]) + completion = AsyncMock(return_value=stream) + events = await _events( + await create_litellm_messages_handler(completion=completion).stream( + {**CONFIG, "outputFormat": {"type": "object"}}, "q" + ) + ) + assert "response_format" not in completion.call_args.kwargs + assert events[-1]["output"] == '{"answer":42}' + + async def test_text_chunks_usage_and_single_done(self) -> None: + stream = FakeStream( + [ + _chunk(text="Hel"), + _chunk(text="lo"), + _chunk(prompt_tokens=4, completion_tokens=2), + ] + ) + completion = AsyncMock(return_value=stream) + handler = create_litellm_messages_handler(completion=completion) + events = await _events(await handler.stream(CONFIG, "q")) + assert [e["text"] for e in events if e["type"] == "chunk"] == ["Hel", "lo"] + assert events[-1] == { + "type": "done", + "output": "Hello", + "usage": {"input_tokens": 4, "output_tokens": 2}, + } + assert sum(e["type"] == "done" for e in events) == 1 + request = completion.call_args.kwargs + assert request["stream"] is True + assert request["stream_options"] == {"include_usage": True} + + async def test_fragmented_tool_call_is_reassembled_between_turns(self) -> None: + first = FakeStream( + [ + _chunk( + tool_calls=[ + SimpleNamespace( + index=0, + id="call-", + function=SimpleNamespace( + name="weath", arguments='{"city":' + ), + ) + ] + ), + _chunk( + tool_calls=[ + SimpleNamespace( + index=0, + id="1", + function=SimpleNamespace(name="er", arguments='"Oslo"}'), + ) + ] + ), + _chunk(prompt_tokens=2, completion_tokens=1), + ] + ) + second = FakeStream( + [_chunk(text="cold"), _chunk(prompt_tokens=3, completion_tokens=1)] + ) + completion = AsyncMock(side_effect=[first, second]) + weather = AsyncMock(return_value="cold") + config = { + **CONFIG, + "tools": {"weather": {"description": "x", "parameters": {}}}, + } + events = await _events( + await create_litellm_messages_handler(completion=completion).stream( + config, "q", {"weather": weather} + ) + ) + weather.assert_awaited_once_with({"city": "Oslo"}) + assert events[-1]["usage"] == {"input_tokens": 5, "output_tokens": 2} + + async def test_abandonment_closes_provider_stream(self) -> None: + stream = FakeStream([_chunk(text="one"), _chunk(text="two")]) + completion = AsyncMock(return_value=stream) + gen = await create_litellm_messages_handler(completion=completion).stream( + CONFIG, "q" + ) + async for _ in gen: + break + await gen.aclose() + assert stream.closed is True + + async def test_stream_error_closes_transport_and_propagates(self) -> None: + class BrokenStream(FakeStream): + async def _iterate(self) -> AsyncIterator[Any]: + yield _chunk(text="partial") + raise RuntimeError("stream failed") + + stream = BrokenStream([]) + completion = AsyncMock(return_value=stream) + with pytest.raises(RuntimeError, match="stream failed"): + await _events( + await create_litellm_messages_handler(completion=completion).stream( + CONFIG, "q" + ) + ) + assert stream.closed is True + + +class TestTelemetryAndConvenience: + async def test_nested_span_tree_identity_usage_and_parenting(self) -> None: + call = _tool_call("lookup", "call-1", "{}") + completion = AsyncMock( + side_effect=[_response(None, tool_calls=[call]), _response("done")] + ) + recorder = SpanRecorder() + config = { + **CONFIG, + "tools": {"lookup": {"description": "lookup", "parameters": {}}}, + } + with patch.object(handler_mod, "trace", recorder): + await create_litellm_messages_handler(completion=completion)( + config, "q", {"lookup": AsyncMock(return_value="found")} + ) + + assert [span.name for span in recorder.spans] == [ + "invoke_agent", + "chat anthropic/claude-sonnet-4", + "execute_tool lookup", + "chat anthropic/claude-sonnet-4", + ] + root = recorder.spans[0] + for child in recorder.spans[1:]: + assert child.context == ("context-of", root) + for span in recorder.spans[:2] + recorder.spans[3:]: + assert span.attributes["gen_ai.provider.name"] == "anthropic" + assert span.attributes["gen_ai.system"] == "litellm" + assert span.ended == 1 + assert root.attributes["gen_ai.usage.input_tokens"] == 6 + assert recorder.spans[2].attributes["gen_ai.tool.call.id"] == "call-1" + assert recorder.spans[2].ended == 1 + + async def test_model_and_root_spans_fail_and_end_once(self) -> None: + recorder = SpanRecorder() + error = RuntimeError("unavailable") + with ( + patch.object(handler_mod, "trace", recorder), + pytest.raises(RuntimeError, match="unavailable"), + ): + await create_litellm_messages_handler( + completion=AsyncMock(side_effect=error) + )(CONFIG, "q") + assert [span.name for span in recorder.spans] == [ + "invoke_agent", + "chat anthropic/claude-sonnet-4", + ] + assert all(span.exceptions == [error] for span in recorder.spans) + assert all(span.ended == 1 for span in recorder.spans) + + async def test_identity_and_content_are_gated(self) -> None: + completion = AsyncMock(return_value=_response("secret")) + span = MagicMock() + trace = MagicMock() + trace.get_tracer.return_value.start_span.return_value = span + variables = { + "__ld": { + "configKey": "cfg", + "variationKey": "v1", + "runId": "run", + }, + "ldContext": {"kind": "user", "key": "ada"}, + } + with patch.object(handler_mod, "trace", trace): + await create_litellm_messages_handler(completion=completion)( + CONFIG, "private prompt", {}, variables + ) + attributes = { + call.args[0]: call.args[1] for call in span.set_attribute.call_args_list + } + assert attributes["launchdarkly.config.key"] == "cfg" + assert attributes["gen_ai.request.model"] == "anthropic/claude-sonnet-4" + assert not any(key.startswith("gen_ai.prompt.") for key in attributes) + + async def test_capture_content_records_prompt_and_completion(self) -> None: + completion = AsyncMock(return_value=_response("answer")) + span = MagicMock() + trace = MagicMock() + trace.get_tracer.return_value.start_span.return_value = span + with patch.object(handler_mod, "trace", trace): + await create_litellm_messages_handler( + completion=completion, capture_content=True + )(CONFIG, "question") + written = str(span.set_attribute.call_args_list) + assert "question" in written + assert "answer" in written + + def test_convenience_wrapper_forwards_all_public_arguments(self) -> None: + instance = MagicMock() + instance.invoke.return_value = "result" + config = MagicMock(return_value=instance) + with patch.object(handler_mod, "config", config): + result = litellm_messages( + "config-key", + "hello", + {"kind": "user", "key": "u"}, + variables={"name": "Ada"}, + capture_content=True, + ) + assert result == "result" + assert config.call_args.kwargs["key"] == "config-key" + assert config.call_args.kwargs["handler"].provides_for == ("*", "messages") + assert config.call_args.kwargs["handler"].capture_content is True + assert "capture_content" not in config.call_args.kwargs + instance.invoke.assert_called_once_with( + "hello", {"kind": "user", "key": "u"}, {"name": "Ada"} + ) diff --git a/pyproject.toml b/pyproject.toml index 83e3de63..54e4ee54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,8 @@ known-first-party = [ "launchdarkly_ai_claude_messages", "launchdarkly_ai_langchain_agents", "launchdarkly_ai_langchain_messages", + "launchdarkly_ai_litellm_agents", + "launchdarkly_ai_litellm_messages", "launchdarkly_ai_openai_agents", "launchdarkly_ai_openai_messages", ] diff --git a/release-please-config.json b/release-please-config.json index 9d2a04e3..783b292c 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -89,6 +89,28 @@ "src/launchdarkly_ai_langchain_messages/__init__.py" ], "component": "launchdarkly-ai-langchain-messages" + }, + "packages/litellm-agents": { + "release-type": "python", + "versioning": "default", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "include-v-in-tag": false, + "extra-files": [ + "src/launchdarkly_ai_litellm_agents/__init__.py" + ], + "component": "launchdarkly-ai-litellm-agents" + }, + "packages/litellm-messages": { + "release-type": "python", + "versioning": "default", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "include-v-in-tag": false, + "extra-files": [ + "src/launchdarkly_ai_litellm_messages/__init__.py" + ], + "component": "launchdarkly-ai-litellm-messages" } } } diff --git a/uv.lock b/uv.lock index c1c96777..a468597f 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,8 @@ members = [ "launchdarkly-ai-claude-messages", "launchdarkly-ai-langchain-agents", "launchdarkly-ai-langchain-messages", + "launchdarkly-ai-litellm-agents", + "launchdarkly-ai-litellm-messages", "launchdarkly-ai-openai-agents", "launchdarkly-ai-openai-messages", "launchdarkly-ai-python", @@ -41,6 +43,128 @@ dev = [ { name = "ruff", specifier = ">=0.15.20" }, ] +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -131,6 +255,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "boto3" +version = "1.43.98" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/4d/8164128d7a1d408f15849ada4632da37b13a5953c9bb3993998eb5d43ca1/boto3-1.43.98.tar.gz", hash = "sha256:1ec732e023fb29c12dc8520f925b5bbbed29eeb36b5764b5a5c26052d7c721f7", size = 112658, upload-time = "2026-09-18T20:10:05.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/46/d29efd46ecac6637065a39418c5fb95500d70ec63e76065b5a72c936172f/boto3-1.43.98-py3-none-any.whl", hash = "sha256:7454f666a9e852a56db0a7fa23be33a899f64e27828930eac43c3edba7b34e17", size = 140033, upload-time = "2026-09-18T20:10:04.513Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.98" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/5e/8d655aeaae195b9856a554e724322092b4cc1b8bb58a29167bea80bf800c/botocore-1.43.98.tar.gz", hash = "sha256:6135dd639ea6d1b3b49381bc253c8a139d61f7d44cb5f7dae8e7cd1791758572", size = 16127750, upload-time = "2026-09-18T20:10:00.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/ca/c7617e99c3247f545233ab2181840d142c5f72034e22864172866fb1369a/botocore-1.43.98-py3-none-any.whl", hash = "sha256:84b35b10402c2fc0c265f634fbf86336eecc6489ee23f55074b329924b2cfd6f", size = 15817751, upload-time = "2026-09-18T20:09:57.566Z" }, +] + [[package]] name = "brotlicffi" version = "1.2.0.1" @@ -425,6 +577,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/84/a04c59324445f4bcc98dc05b39a1cd07c242dde643c1a3c21e4f7beaf2f2/expiringdict-1.2.2-py3-none-any.whl", hash = "sha256:09a5d20bc361163e6432a874edd3179676e935eb81b925eccef48d409a8a45e8", size = 8456, upload-time = "2022-06-21T09:12:28.652Z" }, ] +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, +] + [[package]] name = "filelock" version = "3.29.4" @@ -434,6 +627,104 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/cd/9be253869fc42e764de7f3dedd6969af7d44ff9c3375214a3442a6f3fc08/fsspec-2026.9.0.tar.gz", hash = "sha256:0f08147951c8cb31d844c3547d631053b127863b60be04cf06e121333ee0e2fe", size = 333545, upload-time = "2026-09-18T17:50:42.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/c0/a98505f18594f1bce828bb159cec0fcf9860562f1a2c85913409fc8f3d9e/fsspec-2026.9.0-py3-none-any.whl", hash = "sha256:8dd6e646e99ea382bd85f97a45e6b526a442d79423a7dc673f1e2756d05fcb5f", size = 221738, upload-time = "2026-09-18T17:50:41.341Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.75.0" @@ -464,6 +755,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hf-xet" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/62/3c062f593bd92ef4e77a0ef39541e3d82a0a1d3947c8a777a02a13a27828/hf_xet-1.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d", size = 4074584, upload-time = "2026-08-03T22:32:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1e/c0ad437dd267a8e435bef594acf781bbc3874ff0b6435b4962d03ecf7cc4/hf_xet-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675", size = 3867381, upload-time = "2026-08-03T22:32:49.049Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ee/7c0d7b6ab336167531b1c30af2af003f054af4c749becbd7209ae33a77c3/hf_xet-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b", size = 4453982, upload-time = "2026-08-03T22:32:50.568Z" }, + { url = "https://files.pythonhosted.org/packages/63/06/ad8eab1c9525246650cbaa821caa3cdbaca734ab1a5b8c91bea09cbd8d69/hf_xet-1.6.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522", size = 4249445, upload-time = "2026-08-03T22:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/d8/26/1eee8aedb0dafc1ab9717dc9ac602cde33361b232dc06803f1f6ed18b58c/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e", size = 4451099, upload-time = "2026-08-03T22:32:54.114Z" }, + { url = "https://files.pythonhosted.org/packages/67/57/0b88af1f194ab6c9c650547d9cc06bfeaab836ae4dcdb331676bfb8be95a/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9", size = 4664712, upload-time = "2026-08-03T22:32:55.547Z" }, + { url = "https://files.pythonhosted.org/packages/53/a0/26b717a9d1840e8abf48dcec64b5ed8fbe472671d38ad28d30e147132b33/hf_xet-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338", size = 4025906, upload-time = "2026-08-03T22:32:57.391Z" }, + { url = "https://files.pythonhosted.org/packages/49/f6/4a9966633c6fef83af997e2cff68ec1963676d412bdfd096df2a93b8e185/hf_xet-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765", size = 3849221, upload-time = "2026-08-03T22:32:59.123Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" }, + { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -501,6 +816,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "huggingface-hub" +version = "1.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/0f/e83fdd856da8fca26bf78d71709ebd120432a0ce535e72b9597cab1eb5bf/huggingface_hub-1.32.0.tar.gz", hash = "sha256:ed70a45498abe86039df7c2f4e5f7575de524be908d3840e8f828d5525eafd6a", size = 1038662, upload-time = "2026-09-17T10:27:48.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/cf/d98dd561d6d0d7b7d7a64d1563f8aaaa7c235daee41c1c9bcc3da62420ed/huggingface_hub-1.32.0-py3-none-any.whl", hash = "sha256:b0c7c80561969d9cdacdd55fce67ba9584cca0b9d4ea80957a3a5c1445fac5c8", size = 842906, upload-time = "2026-09-17T10:27:46.102Z" }, +] + [[package]] name = "identify" version = "2.6.19" @@ -519,6 +854,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/72/c600ae4f68c28fc19f9c31b9403053e5dbb8cace2e6842c7b7c3e4d42fe9/importlib_metadata-8.9.0.tar.gz", hash = "sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee", size = 56140, upload-time = "2026-03-20T16:56:26.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/f9/97f2ca8bb3ec6e4b1d64f983ebe98b9a192faddff67fac3d6303a537e670/importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f", size = 27220, upload-time = "2026-03-20T16:56:25.07Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -528,6 +875,18 @@ 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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "jiter" version = "0.16.0" @@ -596,6 +955,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "jsonpatch" version = "1.33" @@ -860,6 +1228,40 @@ requires-dist = [ { name = "opentelemetry-api", specifier = ">=1.25" }, ] +[[package]] +name = "launchdarkly-ai-litellm-agents" +version = "0.1.0" +source = { editable = "packages/litellm-agents" } +dependencies = [ + { name = "launchdarkly-ai-server" }, + { name = "openai-agents", extra = ["litellm"] }, + { name = "opentelemetry-api" }, +] + +[package.metadata] +requires-dist = [ + { name = "launchdarkly-ai-server", editable = "packages/client" }, + { name = "openai-agents", extras = ["litellm"], specifier = ">=0.2.11" }, + { name = "opentelemetry-api", specifier = ">=1.25" }, +] + +[[package]] +name = "launchdarkly-ai-litellm-messages" +version = "0.1.0" +source = { editable = "packages/litellm-messages" } +dependencies = [ + { name = "launchdarkly-ai-server" }, + { name = "litellm" }, + { name = "opentelemetry-api" }, +] + +[package.metadata] +requires-dist = [ + { name = "launchdarkly-ai-server", editable = "packages/client" }, + { name = "litellm", specifier = ">=1.83.0" }, + { name = "opentelemetry-api", specifier = ">=1.25" }, +] + [[package]] name = "launchdarkly-ai-openai-agents" version = "0.2.2" @@ -1027,6 +1429,100 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, ] +[[package]] +name = "litellm" +version = "1.101.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "boto3" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/c0/08a31c8c5f7fce96e98e2a7f818f446d0468da484228a7450cd2b6f71dcf/litellm-1.101.0.tar.gz", hash = "sha256:734ab2b8cad6a3b582d52d9c9c5fcab759eb382b93935ef808fda0e16d822ac3", size = 17447699, upload-time = "2026-09-14T23:15:36.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/cf/6b3a687ab0ab4caacc35faaf695f6862654e21e2a5dc703fa9e1df6d7069/litellm-1.101.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7cc623a224c6f11a04367a682b095a1e08e5f6da75c990a650910db5f9777819", size = 23777389, upload-time = "2026-09-14T23:15:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/42/f6/d5bc3aa1944244fd186e65ca16e62158f0f474b40f99a89e568a5d441739/litellm-1.101.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4064024151ff2877e542b56c6bb6a39e0c3e6651abf4639af9346a678586e52", size = 23434831, upload-time = "2026-09-14T23:15:15.245Z" }, + { url = "https://files.pythonhosted.org/packages/ba/12/755e4d497b975911449102b8b74d3d4f770137c4467e3e8fe304adf22d66/litellm-1.101.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:cdf3351e394206e785bf339f1a39d9358ddf4e4f38129750bfdb5e4f5ceab8ef", size = 23568059, upload-time = "2026-09-14T23:15:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/0c/0e/561d7a314a08940688814be590544907fdfde9225dce35badbd79fc9ad4c/litellm-1.101.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:140ee0997324b8fd2405f7f8c26f42c0f364996dc4030187344a90cf27916667", size = 23944856, upload-time = "2026-09-14T23:15:26.417Z" }, + { url = "https://files.pythonhosted.org/packages/f1/df/d640eb6cf4a304be4ec289b72c1b1d9bd66c73e90167211d3a09ac37b2d8/litellm-1.101.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ad013161074676fc91b91d828f696300132cf3936d3f386c4701a41710d70aa3", size = 23643397, upload-time = "2026-09-14T23:15:28.664Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b6/9d69e95de9465cd23651c6623518d5e711d8776a0e58e6ea62066c9d9470/litellm-1.101.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d88053148c5c6e016e495273eeaf8f3a29779b60d18977ec42e2a6bbd2d8eb", size = 24042718, upload-time = "2026-09-14T23:15:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b5/0a54001992c9a8d991f6ddd7532e3c4cb7dbdcc94220a37586513aab6f89/litellm-1.101.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5601ae404b0f1e38ce1e65c75f880254485942d9c824453e3926f2323457fd5", size = 23844988, upload-time = "2026-09-14T23:15:33.632Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + [[package]] name = "mcp" version = "1.28.1" @@ -1052,6 +1548,144 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, ] +[[package]] +name = "multidict" +version = "6.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/59/84b6cad9ddfdd9471db727b0e987c60ecbdb6b206ba265e8c50e74a1ab80/multidict-6.9.0.tar.gz", hash = "sha256:d7d32c0543494efbc9394e2b571725071d08e295993486bc9a43f6f89375ee01", size = 173221, upload-time = "2026-09-18T12:50:55.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/c4/9cbd1370a191a49a2c4d0217c19e391aff080a9b35222f10c71ec73fc0e9/multidict-6.9.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fdd484b84d3394e805689c56be3ab1f877ae7ff0eb9ff90a3ee7a755cbebab4f", size = 94050, upload-time = "2026-09-18T12:46:43.407Z" }, + { url = "https://files.pythonhosted.org/packages/cc/16/898930380953e7fcc66aabb9f822c1f46c4d459cdc0d82bed0de2f687628/multidict-6.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f93c9058a0eceac0df2ce9d4c8823b84786ee598194753c2ca0c100224405e47", size = 58006, upload-time = "2026-09-18T12:46:44.916Z" }, + { url = "https://files.pythonhosted.org/packages/cf/22/0a55faaa9bba51bd7a01040106a5d40292e87bea575a2905afc37256e684/multidict-6.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c7ab60b91e11b25e7682c5cd8763fdd17929ea83f234ba441091f1492e631ea3", size = 55015, upload-time = "2026-09-18T12:46:46.304Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e7/6dba7bcfda65432a5af6f5978fb84ffda0360471f9571ea434afb06bd09a/multidict-6.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:640113258c5925a9eed2c12523410b25565ac5df2fa6735fbae88fb09bcdd212", size = 313468, upload-time = "2026-09-18T12:46:47.587Z" }, + { url = "https://files.pythonhosted.org/packages/20/b7/3317f5a0aba7a38f74a250a872c5a7ba58407f71028e09a9961fb3804ac2/multidict-6.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ea72901860ccbe94517421681c60b13533ce03ba2f7bd96293c3a4d16ac4ccb", size = 313112, upload-time = "2026-09-18T12:46:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/57/d3/46a482725368f3f29558bfca1a12a05ea68db6a51be54799d80b1bf81626/multidict-6.9.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:408fac672931f2458be3bc8c89d9facd16dac2aad17c7cee2ca1693eee99f07e", size = 298902, upload-time = "2026-09-18T12:46:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/2f/71/ce4e30a3387b403bee1623873636e2b84f2e8b686a8d584f8eb9c62fa209/multidict-6.9.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:67bed23e9803945b0760650ec3e903af772c49abe869f3bc03c66b2e7649d5ef", size = 321654, upload-time = "2026-09-18T12:46:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/de/a4/3ad5099a178cbbd28913cd1fe77fd724cc576839f7893c301428d1d4f798/multidict-6.9.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb769ccc72e15d7d441e1a08f169d418376be77cdc387e813129c26b357fe50", size = 325316, upload-time = "2026-09-18T12:46:53.732Z" }, + { url = "https://files.pythonhosted.org/packages/47/cd/f27bf3242c2d70046211788d897679686a498bb032fbbc25f3c4d41ae9a5/multidict-6.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a5111a2bd824c821a3dd09da29680391b0caaa18fea7761358f4001e6898d1c", size = 317426, upload-time = "2026-09-18T12:46:55.155Z" }, + { url = "https://files.pythonhosted.org/packages/32/0c/8215c0167863a262ba8f886288c48fa2475c28fd0bc39fe548a8c86c8f73/multidict-6.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e991677c4bdc5d9f2e71c74717a4e32cfe98930ca05becdf722b5eae1329d6a", size = 278390, upload-time = "2026-09-18T12:46:56.749Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/c51276d1086756f45cfa72d07f9d7bd21a59ce49b4ee7a245f99b2c9e527/multidict-6.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb69b724c345420ba49187a17a894f146099f5b2e501df42ddb4452ac8be37fa", size = 304354, upload-time = "2026-09-18T12:46:58.217Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/7d04a6347d7b06bf1868a995f91b2f2dc168a265860c55874101e4be7afe/multidict-6.9.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cff3cff5a725bdb8359962de8d7429aea592d7693dbd197eeabbdfab9b6300e9", size = 303669, upload-time = "2026-09-18T12:46:59.791Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/402d6b85d0e4aab551e700ca9123ebe2a52217836359eefbc1d92fd7f64d/multidict-6.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fca5b74b5909c29041f857c40d51d9273636fb4221cf020e4c452deb1c448a40", size = 316363, upload-time = "2026-09-18T12:47:01.41Z" }, + { url = "https://files.pythonhosted.org/packages/64/f6/49b70d1f876d7bc9263c972d2ecc58262590ea0c70743082ef091585b20d/multidict-6.9.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11e32ccf23cdbfcf8299a6a825930a858ec9a6aa05d6752d0f90f2bdf19489e1", size = 315126, upload-time = "2026-09-18T12:47:02.889Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/afff3cb22ccb3221b9715637f5e39f6a576a4c28ce8fc405555d94a17975/multidict-6.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ec1e387b1f8a85ae5b94aa8c4e0576912ffa4d31bd0578f24c950d4f05ee476", size = 278125, upload-time = "2026-09-18T12:47:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/edd123ad77ac77a108b2f7ba322727e92c2d39d714df2bbca9c09af9d741/multidict-6.9.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e4826f6b56456fb1e98111d7bc20cbdc7fa0a41b1f9ad80ff2dd3f2f5b226fe1", size = 315167, upload-time = "2026-09-18T12:47:06.897Z" }, + { url = "https://files.pythonhosted.org/packages/79/32/0ba8f8b6a0529bc46c5d4376128a3b9dbc3d7610d0a5768be0e599b5e56d/multidict-6.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e71a072c52c78b7f97cd4611df6cef10977e4f2367cd0654a7626192f931adde", size = 313411, upload-time = "2026-09-18T12:47:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/a1/77/3b7b32a4331e99e89dc915c2dc1b054e49dd0f8c9c0c783c4d6063048da5/multidict-6.9.0-cp312-cp312-win32.whl", hash = "sha256:95d339c3b75b4a50c665bdcf8417428cd71c3e5cd48e194cb1d336fcb856beac", size = 50943, upload-time = "2026-09-18T12:47:10.105Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/484132b6c9d175939d8fb8b6c45b1d4eacecc79add962e1b4157ba138f60/multidict-6.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f06c4da5315a6f709b13408c3e13f3b475f8c559ec7608c3c67062512871235", size = 57304, upload-time = "2026-09-18T12:47:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/27/14/63792486623819be73ce2149749bcc0b82772d2de22e02d452213603c432/multidict-6.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:0db5bf96ec2ce45a8bc7fbbe8a486089969bb2791a66b6789ee3aed0d5dd562e", size = 53498, upload-time = "2026-09-18T12:47:14.294Z" }, + { url = "https://files.pythonhosted.org/packages/a2/cd/d9475e63b3bf46bb369437323a5906f2442063812f4c7c892d39cda2a578/multidict-6.9.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:662315f8621b3559268813b11134c1f9633edd4ab5f7b713688a243c13f4026d", size = 61405, upload-time = "2026-09-18T12:47:15.908Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/69cfa797b3e5f85ebc45c39d5aca05f69afc95e23b22943ee57f9499ece6/multidict-6.9.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4bef8cb5edea9c9daeb8396a75eeb97f912c3fb3fa408fe659edfecf91e47424", size = 53250, upload-time = "2026-09-18T12:47:17.254Z" }, + { url = "https://files.pythonhosted.org/packages/89/29/dd5d451e17dcec77f6b2dc0ed032714244d3562477349465dcc525f86297/multidict-6.9.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:75f7fc21ffce9a792cb919f0e0b0b52117bd672f2a55d1929d573b6fc437f374", size = 54521, upload-time = "2026-09-18T12:47:18.719Z" }, + { url = "https://files.pythonhosted.org/packages/93/d5/20d0b17d61a1fba726b96fefdaa224a2da04e66b0ad9ad08af248023bb98/multidict-6.9.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bb6be697065cbf31051465f9939d65624573d2219c3da8316db9e7cf5e0f5a7", size = 93484, upload-time = "2026-09-18T12:47:20.211Z" }, + { url = "https://files.pythonhosted.org/packages/c0/12/8067d5bd826b16eba31cb11b3f37ca6089d31b6afa33f208e77943f86230/multidict-6.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67bcff396d2ad62197c95488a716a88462792b384b5ea6e44bf7c1070eddb8fc", size = 57700, upload-time = "2026-09-18T12:47:21.668Z" }, + { url = "https://files.pythonhosted.org/packages/d4/da/ebf8f7e41e6bb58240ec346e5b8165acefd7ffdbce8ff503ab1bf2a1b883/multidict-6.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:beda95a6bd0a2e2265b941b90ff72543afbeaebb6a450e3a6a010d10d4a2ffca", size = 54762, upload-time = "2026-09-18T12:47:23.163Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d0/00ea9e7835ef5d61ce7d780d0aaca89a1d2215285937c9f12a089d1223a4/multidict-6.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e78870909e9f9e3d672ba99f1eb75130d7e01c42e642e6a70f434df32ca0ee1", size = 313470, upload-time = "2026-09-18T12:47:24.638Z" }, + { url = "https://files.pythonhosted.org/packages/81/67/1cc27283d5a8b92f499addf7f8b2d47af5528dd019d3375a49c58bccf8ec/multidict-6.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91092d597fcf0940cd6a59e64ba7156ee22d6899993fd7c5b1897fda71482f8f", size = 312460, upload-time = "2026-09-18T12:47:26.282Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/e5e9ff320467bfc5301499f6cfef049b427f9e2f757dbe2c2d63912c32a7/multidict-6.9.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be612f23990a261060ccd8f15e89dad1f9b7bb0c1021c5c3987f5fc57959391", size = 297202, upload-time = "2026-09-18T12:47:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/53dd52b603ab0eb5a6b4c29992d902f4f76a802c192aae48f915e7bdb566/multidict-6.9.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7aaa14f0b9ffa2780c5d3b21da0e9b58778ed47af3369df72e5fd6dc10ff8041", size = 320331, upload-time = "2026-09-18T12:47:29.704Z" }, + { url = "https://files.pythonhosted.org/packages/54/f9/e9d3c76824ed6eb6f92a992e501f15c4daf1b01bba2451d0254c943c8eba/multidict-6.9.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97555ad30ad20a8eb90fa86522088eebbbd68aba03e56d53f4850b3535c65e61", size = 324944, upload-time = "2026-09-18T12:47:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/19deb0d051c694dbc2d5008ec89b341f7ee42c4412b38630a47a14339115/multidict-6.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c51fd8d59e72e45c64907bf7f81fbd94f4c8dcd1c8b2f23f4c2d7ede788fcfc3", size = 316918, upload-time = "2026-09-18T12:47:33Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/b4e3a2e62ea902f2f000cd7a8eee8da4f31c6137ed35446eed11d62ae321/multidict-6.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22067e88ff266e6a5dc59114529332692a01cc04b10dbdc2c14ab91217dee819", size = 279224, upload-time = "2026-09-18T12:47:34.624Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a7/656ae0635cf82d9bbd8742403c7cbebd5bf555e02686b5e1ac43a80268b8/multidict-6.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e231de8ce43d4fd10bec4f67f71238ed88a974de30a9f20be7a4ab16c970a988", size = 303343, upload-time = "2026-09-18T12:47:36.335Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/cabef61a7151451a7f153679a1dd8b9d31684a0f8edaad782da5f1900083/multidict-6.9.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b33e499a7b1f722547d57b4865ed68c03161643b9afe49fe097a0832078d183d", size = 302877, upload-time = "2026-09-18T12:47:38.323Z" }, + { url = "https://files.pythonhosted.org/packages/07/63/942663ac05edd16c96ecf71f89f2f632dc113192684303287d727e465605/multidict-6.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1eb7939025bd9289d9dfe3a399102b1642f4fbb105a6af82f284052ee89e97a9", size = 317489, upload-time = "2026-09-18T12:47:39.843Z" }, + { url = "https://files.pythonhosted.org/packages/08/d2/359d4ef44c74c8ae25099dc50fbbebd074cca913d49d713aad10ac83b710/multidict-6.9.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:51a08dceed4b42ef25755ee2cbf50df325e90e6b415d00955ffdea2c394ad6c0", size = 314278, upload-time = "2026-09-18T12:47:41.987Z" }, + { url = "https://files.pythonhosted.org/packages/37/fd/0b7a4fa989135045cd531c31c1adbb7ae762ee7143f5f1bc27985ae71d6c/multidict-6.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb847cb4002e725f88ffe29883da8290fd0754fa0461ed8988735e20964a9f65", size = 279318, upload-time = "2026-09-18T12:47:43.812Z" }, + { url = "https://files.pythonhosted.org/packages/78/08/7fd82e0e627b30733619c2d5ee52df047c608b562232e501835d645a5384/multidict-6.9.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:516fa4817cd070088f616a901380db56189f86daee9da79aada3c9b653f49ed7", size = 315045, upload-time = "2026-09-18T12:47:45.659Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/08c09a9aa8797246bf6b646e603654a8f25326df4881855da4b9b10a5e56/multidict-6.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:254e53be2ec70518bb82dfa9b0c7166baaf2bd0e918bd17b65caaf489d5a5522", size = 313360, upload-time = "2026-09-18T12:47:47.503Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/72f0c58d20f3e8d167105b0ab3e8b4a9e3d7fd285b9f6fe4f1e1295ad002/multidict-6.9.0-cp313-cp313-win32.whl", hash = "sha256:9869105ab61db13004f9ed610cd29ce4237b2c609f5f7b8bb8f2339fb9dad17d", size = 50744, upload-time = "2026-09-18T12:47:49.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/51/384557cf30472e760ab9e897105f3a4dc7bbb8162e4a60695008ba012c40/multidict-6.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:85ed0f3c3b01174aea5a8a8fcd456f64c2e9719c0f42c612c2694ecd35974a65", size = 57094, upload-time = "2026-09-18T12:47:50.611Z" }, + { url = "https://files.pythonhosted.org/packages/59/89/8a3741537d155588cdf220bba1f1e5c96ef2e92ee1f41856be76237eafc2/multidict-6.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:1126782e3c3b1a7ccd990be3d3221709348e4b07d6ecf8b50965a93ae2624145", size = 53342, upload-time = "2026-09-18T12:47:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/83/35/41bfe9812c1efc85b2b1504c4fbaf19574be11d18df1f08abdd489280efa/multidict-6.9.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:1b34b10b615db5a3ed424b174a362a3cdf06220bd892d03d151a619bb157c4e9", size = 60487, upload-time = "2026-09-18T12:47:53.713Z" }, + { url = "https://files.pythonhosted.org/packages/61/98/b5e73e4af8c6df0b2c9cdab4b2566dcd5185fb4a345567e51ea2008008eb/multidict-6.9.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f7f0b996a20776018a253061a781ba983e7b944e17a0748822026048b8b0bb07", size = 52425, upload-time = "2026-09-18T12:47:55.285Z" }, + { url = "https://files.pythonhosted.org/packages/62/f5/9f6586f01b458af9690490f147f5d011bda35dca8c5887258a9c7cc35d63/multidict-6.9.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b612842f7f286c42abc5dcbda1db50e9dbed39720dd15feb07dbc9974bfeaa7c", size = 53691, upload-time = "2026-09-18T12:47:57.05Z" }, + { url = "https://files.pythonhosted.org/packages/f3/79/532ef4caa1f9fd3573751821923fcdf1948f6b4648c96378bf9042340596/multidict-6.9.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:71ee2b3198323cbff7a846538f01599f309dddddba79b5bf5a3dca2e49460ef6", size = 91804, upload-time = "2026-09-18T12:47:59.033Z" }, + { url = "https://files.pythonhosted.org/packages/28/5d/40602b534c8b731619a39b3cf1e8f41b25d81e125af3915b858bbc254de5/multidict-6.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d0dc1f9c7dc07d86e5a4ed24f3a55b945106f3e7c70a984ff8dd9267270414ed", size = 56892, upload-time = "2026-09-18T12:48:00.59Z" }, + { url = "https://files.pythonhosted.org/packages/c5/dc/9876ad169f03f1d6163dfb5d9b90913c9213335c19a61f206a0b75d62815/multidict-6.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a26c8672d70d2c12c2f1c4df91fe92422e905efaf1d639bd4d7192868a21e711", size = 53936, upload-time = "2026-09-18T12:48:02.684Z" }, + { url = "https://files.pythonhosted.org/packages/18/75/f8e9e24de0d4d085632e867c54606acb97920b7a5c9953c0947fdbaebaa2/multidict-6.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:186cc28558e3d6921ba797e433cd58ce64eb8b2f553e93f75dccbb9a604b7457", size = 315198, upload-time = "2026-09-18T12:48:04.272Z" }, + { url = "https://files.pythonhosted.org/packages/e4/eb/f6a43d347b3b60184d4058cff8b1e611529248bee210ab78505d0f2c023d/multidict-6.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebc02af6ac170631774219c26fd63b4404d1e836a1aa92b738a3127eaa2aebad", size = 310536, upload-time = "2026-09-18T12:48:06.323Z" }, + { url = "https://files.pythonhosted.org/packages/60/0a/65fcd25d3f4b2972bea7c119fe7142a7e9e643f687a3f1926dcc5624d87e/multidict-6.9.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62c4ffba9f81a2b17cf081bfe4e6b7eaf17193dc17d5870c01b2763e48efed24", size = 287297, upload-time = "2026-09-18T12:48:08.41Z" }, + { url = "https://files.pythonhosted.org/packages/87/62/e6be8a2491810ee921f60c730b7d5275b7f5212927022fa599a1c822df98/multidict-6.9.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a7e2aaa0e9a6413b54cd90fa0fe5944487872a7130a1c07347627974e677a34", size = 318886, upload-time = "2026-09-18T12:48:10.135Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5e/f1702e7bdce29893f05d1596c23b4eec791d89d7d69786aa4be34f7ebf68/multidict-6.9.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:26e34dbbd6c3438ee19e0db1f5b846a7ca957085d193ec312b488924c3941a0d", size = 321460, upload-time = "2026-09-18T12:48:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/54/86/ee493f4c319f998f48abd928a6c7b6f0d53604fa1b5ae53b23400cdac078/multidict-6.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d811799f821c06883404a171faa880660ef04c2687fc32fdaef22fb645fe80", size = 314222, upload-time = "2026-09-18T12:48:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5a/a4cf1aeb9e2d17cac89686283638d89cc3dc49e9ae36c0e99fd9b5e8d9dd/multidict-6.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89fde003cec43783193360c27de2fbb0c8ea2db1cfebc193c3fc657c436f9d38", size = 279835, upload-time = "2026-09-18T12:48:15.288Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/d7e55edce6771e38c88204939720c022dd0027a2d7c110515a6f378ce76a/multidict-6.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9bfa4ba6d97ae6d9c1f5336c2abad5690bce1802a040f9aa5fa39cb0b3099c02", size = 301182, upload-time = "2026-09-18T12:48:16.968Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/546c57c112860ea3c06d429f661a2e95027a7429813dad7f9bd0db7b41cd/multidict-6.9.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a776fbaf700549943b647f21479d69191c238c84fb55d5a48eec1b4cf6cf6e00", size = 295357, upload-time = "2026-09-18T12:48:18.872Z" }, + { url = "https://files.pythonhosted.org/packages/20/64/6ed29fb8d80f806e2350ec84da14d18f079afa5be1f86529a30139dc5815/multidict-6.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ed04614c47379bcdfa01d3e165149518017ea034cd5f48e203c287cc3d2ebd4b", size = 315608, upload-time = "2026-09-18T12:48:20.564Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8a/a636222fed7db139b474bbe726bca1bb615bfe0f59bcc880bd2590d05a93/multidict-6.9.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9c65d9912983991f368bd01f993191b1593c90ecde6be990d73add46084d235f", size = 311612, upload-time = "2026-09-18T12:48:22.296Z" }, + { url = "https://files.pythonhosted.org/packages/91/6a/d475f18e6fa223d46a086c0ba6b3449582305e1a8719137b8f5df30e5eca/multidict-6.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4b89b102be78ed286123e75a30efda519e1ef6b9d4a4d01a08213f5e01a18e45", size = 279440, upload-time = "2026-09-18T12:48:23.973Z" }, + { url = "https://files.pythonhosted.org/packages/8e/70/1eb5c630e99f8b32bdc030a46c3ddf5830a8daf38cb18364e5d75872470f/multidict-6.9.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e3ed6563ea143214c63bb4452ae5e8787a39226b22101c179fa4d2eda5972267", size = 312327, upload-time = "2026-09-18T12:48:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/61/aa/b4dc4421596816a4444f8dd8ec25e294d47ff385feb01367941c96c6b650/multidict-6.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:448c05bd13f8675b3629fbc3fabb9e7bb19a5636a7a3eb677ea25a08d0544a56", size = 311385, upload-time = "2026-09-18T12:48:27.668Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/afea7cf18163a56c3313bee7fb62771dc570a9aea193278e5d071f9b5fed/multidict-6.9.0-cp314-cp314-win32.whl", hash = "sha256:2b5db4f2ca34fce55d34b9c66a0eb5d0d01962e2420a1218ba836d8e46aa6ea3", size = 49929, upload-time = "2026-09-18T12:48:29.779Z" }, + { url = "https://files.pythonhosted.org/packages/96/07/f20f633396700043a804fc1d7c397928e79d9955516b7621639219c6aace/multidict-6.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccf9f7f07e9ab6e24f68ccb2b4da8ded65c1fbc5390ebd897a5fe55cdd4c064f", size = 55765, upload-time = "2026-09-18T12:48:31.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/99/d1d5403e8c13bde35e4ca116213a5ebd021d35dcf546b18f5f6a38c77cc1/multidict-6.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:64ef289ca3c6eaafaa705978686631cb5acc90a92a09bef39c95a810c5f4b804", size = 52025, upload-time = "2026-09-18T12:48:33.037Z" }, + { url = "https://files.pythonhosted.org/packages/1d/49/86d2a4ce9c034a16469096627dfc03a2c1015a21c976261bd29b80548543/multidict-6.9.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f17f485e1837a7d31216be8be991a51265bcfc3cfd2457d5c94ade6086793ad7", size = 104111, upload-time = "2026-09-18T12:48:34.741Z" }, + { url = "https://files.pythonhosted.org/packages/ef/94/af88a68c4536d791b9224749051856f2f8f6641a2470b04efaca22d6d548/multidict-6.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3fccacf47e756a0f487b2b7e4afc9f167db0770ecc4e887b38ae2aec99dcb510", size = 63316, upload-time = "2026-09-18T12:48:36.394Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/fb3f5fc6d108dfd32660559d8247de0834a8654f7cd0506c27881dfd9709/multidict-6.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:527655c10fca73fd336cbcd5ccaaba5d8ded1512a3183ed7ebcc1569db5f1de3", size = 60593, upload-time = "2026-09-18T12:48:38.136Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e2/66e94b3ee770d5719a5e85f3c5a2f35aebd27a27025655c9694e80e9c90e/multidict-6.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d3520f774e78dad488c97398e5c7253333baa4eea09785aca56d477c4dc05c7", size = 263846, upload-time = "2026-09-18T12:48:39.909Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/f888602d08bd861a8143c00a6cb85aac1c0f9fcfc917948465d2908898d4/multidict-6.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:518a76bff2567fb0941603b9ccecdd830dd2d717605e7a28c1edc9e64688759e", size = 278411, upload-time = "2026-09-18T12:48:41.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d7/0f0f4ff3b27f9083a5c949cba860dc9552ca7a74a56621b95fcd1fe9e5d1/multidict-6.9.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ff308def558cbbff74ea889ec659d117ac550d4d554467ed6cb3bf6adadac7e6", size = 262973, upload-time = "2026-09-18T12:48:43.622Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f6/8b70937adc0b0fa528544ce5ab98ad9dc8ba27b401b9ec8caa359fe8e394/multidict-6.9.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:875a77927cd7bc0bc1d0a0520a7ed2267bb169c5db03cc133eaa746526d4182b", size = 287097, upload-time = "2026-09-18T12:48:45.331Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ef888530e3ec279dd24fd1db34c027721b8e2aea8485bef99e7464cd77d1/multidict-6.9.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2182da6ffc9793dd66d7d3632eea2b131f1a1e26eaedd51956845501830d84fe", size = 288527, upload-time = "2026-09-18T12:48:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/99/b0/8ee78b0fbe5fe153e9566055e1107c9bae0011b1ae903e30c7ab2ba3e07f/multidict-6.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e680a0b07e44b87a975eca63f84860a772667b0037066a6e93d998eb41ad2814", size = 273078, upload-time = "2026-09-18T12:48:49.132Z" }, + { url = "https://files.pythonhosted.org/packages/25/a0/9d0523f08827a686b80509f4f8f1b484f425f1586b951188cfa0f30f1ef2/multidict-6.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5066b7877e1e2ae2c3712c8037eff40a80d4cf35b393b4c42c81f1c4b73b346d", size = 249874, upload-time = "2026-09-18T12:48:50.954Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/9e5de655bc7a56d48789bcb5cfe49a89be952801d18cb3b6523190648cb4/multidict-6.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fd0ec96e9877cb4dddf7942dffa777837809663f8e55d4f79a2bad06453446b0", size = 266815, upload-time = "2026-09-18T12:48:52.787Z" }, + { url = "https://files.pythonhosted.org/packages/b6/cf/2352eed7e7984c44f896fe2034e58cd09fdcc9dc8c2385fc6177f8c00159/multidict-6.9.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:98b0738ac1a1593f609a7b67d25e09c7de57c0f91c2879eb01c46b2dae55c0ba", size = 261108, upload-time = "2026-09-18T12:48:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a0/878e80568b168f4c623b5c3cb4d848d105e99cf9b4979f472ae7c60c2450/multidict-6.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0548fdffa634c495b866d89e02ae58a9588cedaeeb972f6ced433e20dee1db5f", size = 264379, upload-time = "2026-09-18T12:48:56.726Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3f/3083816fb153cd5b7261d26160624d9289d8d0b087a9f125f4d184a86cbe/multidict-6.9.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7d0649ae61a341b9a47a2afab57424bd2a0ce98d36ec170ce9a51ce92c7284f6", size = 277516, upload-time = "2026-09-18T12:48:58.775Z" }, + { url = "https://files.pythonhosted.org/packages/c8/76/55f4e30d2fb96d52e3a3d60c4aa2bd6ba6bd6bfd3ac0aa76e303e19574ca/multidict-6.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6d470d693ff45678b9ab237113546f6dc18918be55b3c852a69ed05ae7404a8", size = 249342, upload-time = "2026-09-18T12:49:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/09/b9/bc9f88e51df48537105e3c6b2a6569c2de1a4deac6c7828ccc541e170015/multidict-6.9.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ac05ce26d2c902d2eb5afe70f2012d19f3b9510ed5ef054bab27a3d43bc7f39c", size = 274843, upload-time = "2026-09-18T12:49:02.927Z" }, + { url = "https://files.pythonhosted.org/packages/60/ee/8e08e89b2b699590b5ecb8095581cb6e5389be5d985b16addbd99839065f/multidict-6.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e82ea3ca4409c20172074f27dc1ba5858174f143d0ee637dd058b8061ff024c6", size = 268912, upload-time = "2026-09-18T12:49:04.992Z" }, + { url = "https://files.pythonhosted.org/packages/34/fd/f7d3e7112c4eb6f45ffafd4447fb549c1d42abb4e723db12c7158dc3e0ff/multidict-6.9.0-cp314-cp314t-win32.whl", hash = "sha256:d61680c1969b5b1b900fece510077e7732336eae02f68dda02162e04ff6ff321", size = 55241, upload-time = "2026-09-18T12:49:06.83Z" }, + { url = "https://files.pythonhosted.org/packages/3b/06/fcffb708fcb4f8fba0cf399af19e15d878d59dd76878d4b3f34aeba98c52/multidict-6.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9a9af5d28024c1de9e0efe06a64b5a8561e6517daa66788169e60d2c588e15c5", size = 62135, upload-time = "2026-09-18T12:49:08.76Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b6/22923e7d3897b4e455c507d21695894084f6cadab8cfeaf9967e37401cf2/multidict-6.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:810ae53369d957fcc3e3855612e3a35a39960adaf4b3b1cb4cb4a2e94185bcfe", size = 56193, upload-time = "2026-09-18T12:49:10.866Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7c/db9f8299f793ab605a0a4ad6cfdc1c8523277e08780f11b4a24a4ff84186/multidict-6.9.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:b9bcbd708bb5e1ce8cf9c58908ae9777a90f777406315b046bb10cb3e87f8621", size = 60490, upload-time = "2026-09-18T12:49:12.922Z" }, + { url = "https://files.pythonhosted.org/packages/15/16/446e15381051737dcc08683bcce29b33a0527466807931a04c12c25e3d56/multidict-6.9.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27ed5aaee62eeff74786a3259de81ada4f2bf84b75122bc06d7d8eadc1be5643", size = 52464, upload-time = "2026-09-18T12:49:14.902Z" }, + { url = "https://files.pythonhosted.org/packages/a4/08/8e3e51c02dc91b700addedf959bc850b0d7fd1e350188304f8e6a4414400/multidict-6.9.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30ba80aa7ec7340e80140d29f563e33acb21f3756e42df13612f2a26c80c90ba", size = 53827, upload-time = "2026-09-18T12:49:16.855Z" }, + { url = "https://files.pythonhosted.org/packages/cb/17/750bce1c2ea696c5c19644a537415b930c30a463d1c558dc48d14e45bc52/multidict-6.9.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c26e56c68bde47f8028265b3829915450c944febc75a1e4d0365b8a75557861a", size = 91821, upload-time = "2026-09-18T12:49:18.833Z" }, + { url = "https://files.pythonhosted.org/packages/44/40/35d4bd06f49e872fa277ed2aa38eb13b61cf0f1cfce2cf9050899e3f1eae/multidict-6.9.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:f6705ae71e4b14c693877be3cc09fc62af10197ca08ee08bb57ffce6e0f9c5f4", size = 56892, upload-time = "2026-09-18T12:49:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/60/4b/be1416c1fb3e8c273155a46179f2a5055c35db524314776675278d01d425/multidict-6.9.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:1fc7b1ce735737b7abf38935928729433ef2c81306dcab7f360ef3c3076bc8e9", size = 53962, upload-time = "2026-09-18T12:49:22.863Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/dfbdf4523ffeb1bca192c9b3d429ded754946922c57dfed40e6e489a3307/multidict-6.9.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7731a591920d77bd9372a9a8ab3f447129dbf17d29b10928c94005e1606641c", size = 312140, upload-time = "2026-09-18T12:49:24.999Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4e/636674d1dd0acbcb2216dae1f82640835b7b738331d81a13bc442194a816/multidict-6.9.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a9295177c33756d7a6f8da0f1db345ac951f2436be6ad4b90f53056526e767", size = 310883, upload-time = "2026-09-18T12:49:27.376Z" }, + { url = "https://files.pythonhosted.org/packages/dd/30/f7f0aeebb9268981f9b76171adeabff078d99f7d0efa9d1da689927eb967/multidict-6.9.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b4ae468e341cc84c1ae035ee12f032cc0716f618b1eb417fb14e8454a17c620", size = 290430, upload-time = "2026-09-18T12:49:29.583Z" }, + { url = "https://files.pythonhosted.org/packages/19/4a/d00b3307561bb9ef742cfb431582a25491269a538187cf387daa1362b2dc/multidict-6.9.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:eb9f52b89e339007a06c88cb95068e9d612ce252baaaa5ece85de96e2d51fca9", size = 319088, upload-time = "2026-09-18T12:49:31.836Z" }, + { url = "https://files.pythonhosted.org/packages/19/9e/1671a2cc29841b9ae9521a35b21f6aea9ee27ae5b6ed4b75889b096f89df/multidict-6.9.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69190050f74fe139a7b3759c730e4e87484db9cf74b1786910a24496ab7a40f9", size = 321532, upload-time = "2026-09-18T12:49:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/76/77/0f8a7b2155d38cc9d22b033bf7e0ef1d0a44947a5a4852c9810f91bbf8d4/multidict-6.9.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5f04bbfb064b87455a8012f5829b20201d608cf4b3a1a0e50c7a14173a75109", size = 315859, upload-time = "2026-09-18T12:49:36.421Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/96cdb8ea3813116736b47097078e40977c1c5a71f02c693aa18bb4f30f59/multidict-6.9.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d81d0ec43a73056458e844cf90239d2c3a7aeb4ef05fea8c0f64775d5111847", size = 283500, upload-time = "2026-09-18T12:49:38.871Z" }, + { url = "https://files.pythonhosted.org/packages/30/13/1710f1847cbabc2104440e994b1f789fd2581a03445ca45f10e75e64aa76/multidict-6.9.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:b8aea3d54f3cc20a557fb951c68294d9aa88d614cf7151aa5629b1d5e4033b74", size = 301639, upload-time = "2026-09-18T12:49:41.115Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3b/89802c789a3e90923b872d5791a523729ae9f9d3167789aef63b0e41f4de/multidict-6.9.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:31fd2961bd754ac3177aae9438c01becc22eae029911f13c558e5767a10e5dc2", size = 295976, upload-time = "2026-09-18T12:49:43.412Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/c27827afe584ed082eca0afe0ca10f780f842da4e6894c1aee515526e44f/multidict-6.9.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:04e6f4eddeff93c778a9e7137568284f28856d960275085fcbd87cf77a53a057", size = 315167, upload-time = "2026-09-18T12:49:45.641Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/a6fd940d4d9490f5249b378391fc7b51cb79ce962bcfe50866ce7369e76f/multidict-6.9.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:319160c73795ce31e86494773e0f12c446d32dae7d1a76f4829007ef69eb80bf", size = 311804, upload-time = "2026-09-18T12:49:47.858Z" }, + { url = "https://files.pythonhosted.org/packages/b1/44/14b1cdb66e35ad8f29ac7372c4ee178c0fa350cedf5280623e9ab9ff9185/multidict-6.9.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:0d7fbd38d52119423ed8c69672ca47eac121062c30c976e00ba732d114774874", size = 283329, upload-time = "2026-09-18T12:49:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/b7/0b/6e584b53ade95ca42160716db6fcb7c660f8607bec4c884fece84f5a6fe3/multidict-6.9.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:981280a946256fc151584fb3b82b99db091fccac81cfecce530585db559ec62c", size = 312161, upload-time = "2026-09-18T12:49:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e3/a2c9b107d5a08e2415372ccea189ec23a2ec399e2de48b97eaa48a298405/multidict-6.9.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:fb3d1f3c63d49656a83e1604ef3ac573016cf2e62da84d1937f47107d5689c8b", size = 313129, upload-time = "2026-09-18T12:49:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/54/86/b2e3439e7898e18aed90c81da53a7eb71e77b0b2647813fb45e8b96044e1/multidict-6.9.0-cp315-cp315-win32.whl", hash = "sha256:0c5388c41bc7c551548c4ce840349f0708f7ced6df877d82900bac82268160fc", size = 49919, upload-time = "2026-09-18T12:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8b/d7069d710ff21b627054b4e33dec8ae4f0d5493858d1d4947d53bad3a7a1/multidict-6.9.0-cp315-cp315-win_amd64.whl", hash = "sha256:49e47de3823bee4896f1eedc17ce4fc4f42c433bab168ece7911f42e69a4f666", size = 55763, upload-time = "2026-09-18T12:49:58.773Z" }, + { url = "https://files.pythonhosted.org/packages/a5/df/8d50fc63dd34f88f614b6188e9e75d71a777151cde436952d06af962e7f8/multidict-6.9.0-cp315-cp315-win_arm64.whl", hash = "sha256:1ac339b944616d4061a9ee9571c89767543db38c19c8c7917a9c08b1462c4800", size = 52035, upload-time = "2026-09-18T12:50:00.919Z" }, + { url = "https://files.pythonhosted.org/packages/97/30/beeb05d09787cfda7d4b926119ab211b20fa6ffa0fc1ae71d32a665744ae/multidict-6.9.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:cb6be563dd43b2d4d528afded1878b559ef1c2f45f89e94d273ef515485727f9", size = 104140, upload-time = "2026-09-18T12:50:03.32Z" }, + { url = "https://files.pythonhosted.org/packages/49/73/0dfa03c23bf73f4ed09d2ec831b6049d514aa0f4d2e3d11641458eedb2ce/multidict-6.9.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:76f0df301516563085342d9cdad03fc3c0301b36c3cfa8454e49ca73878aa59c", size = 63291, upload-time = "2026-09-18T12:50:05.65Z" }, + { url = "https://files.pythonhosted.org/packages/8f/87/0089f0b1660f43f9efb5a132a2fe2d81dd2539a5d6c27993fb68ec89fe4f/multidict-6.9.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:34c00a534e948eb5f8b3c50cacea9daa4fc4dfd503f6edd114bb532f53742c9e", size = 60643, upload-time = "2026-09-18T12:50:07.784Z" }, + { url = "https://files.pythonhosted.org/packages/c4/16/0ede7577da32830a8d80df3874cc50234a725acb0cd27ee2b0abd01015e3/multidict-6.9.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cce5e33383904e32b58bef7765c38917899a9dc87058f75c421c82e7c11440ee", size = 262750, upload-time = "2026-09-18T12:50:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/4da6b8477cb8a6cdc79ea19f1514ab3bb664635bb3c9e0ffdc6867d4f155/multidict-6.9.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c375408881861294207c1d038a711120776989c5e8c489b251a818cd74d39886", size = 277978, upload-time = "2026-09-18T12:50:12.638Z" }, + { url = "https://files.pythonhosted.org/packages/69/f6/702e239743ce061e20dfa29e127bbfb7df7150d449e5cb410cf9cdaeb1b4/multidict-6.9.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f95940278766f273b668bbd9dd412ab5dd170ff40fc836b714c38de7852f1fc", size = 264601, upload-time = "2026-09-18T12:50:14.933Z" }, + { url = "https://files.pythonhosted.org/packages/85/1f/2f049044fe4c7944251b34776a11e1708cc5d0911f4a298de257e2765cd0/multidict-6.9.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6009b58d6994881c53f56d2f2010f0b3cd30139c7b14156c2329d00c0d592e8e", size = 285387, upload-time = "2026-09-18T12:50:17.156Z" }, + { url = "https://files.pythonhosted.org/packages/75/3b/0fc08a1bfd7a304c54b2071600cdc1ea4b59601050f5866c5f67206e0439/multidict-6.9.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7d5ab831301decfb7c29d1c243823f6ccbb3766117c5957bdc5b9f283355d0f1", size = 288613, upload-time = "2026-09-18T12:50:19.675Z" }, + { url = "https://files.pythonhosted.org/packages/be/4a/0369b357785000590801bc90925ff1fe0e485d0684172a2433f801f11d3e/multidict-6.9.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17b74ffc94e63843cf34b7eb874ab1653afac0eda3edfa3de1207db7fc8c0ed9", size = 276891, upload-time = "2026-09-18T12:50:22.014Z" }, + { url = "https://files.pythonhosted.org/packages/33/bc/23aed38c0e79532b1dcce93e13c01d31e8a18cbe85503084b28550a47961/multidict-6.9.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62dc471198ac2f1085f6c6ba9aa5ad3343c0cbac0756d2fbe551659d55e14212", size = 251973, upload-time = "2026-09-18T12:50:24.452Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/dfc138bc987d2981cd9bcefcf67876155a2122644bc74fa87c4df2056b20/multidict-6.9.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:da97c0a501c5da00d9c3af6d481ed97dace148f9030f4f5578534981d58682bb", size = 266218, upload-time = "2026-09-18T12:50:26.849Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c9/7ce37abc93bf7618f18f1133083b2c14383fdfa9166a1e8756ec3a84d163/multidict-6.9.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:986f2a23330e8823b389872ae63b83b862a10e63e8cfbd11b42ab8664d1eaa7e", size = 262464, upload-time = "2026-09-18T12:50:29.328Z" }, + { url = "https://files.pythonhosted.org/packages/e0/8b/a7d25bbe0fe7d7e4a1ded661063b8465270005082d4dc258488b268ad20a/multidict-6.9.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ed96992cac94b32a9c3da5870b05967271397a813029d68197939397860b1470", size = 261499, upload-time = "2026-09-18T12:50:31.83Z" }, + { url = "https://files.pythonhosted.org/packages/27/29/cecbae0a00c022f9f3756f02f99d7f5d2da637aea0c5ad5102fb3a65bfbe/multidict-6.9.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:94e767d30be457da0ef134e688cd2b6a438d19b85bf000b1c4a2b9b6f55b4211", size = 276001, upload-time = "2026-09-18T12:50:34.274Z" }, + { url = "https://files.pythonhosted.org/packages/3f/91/ef23ee6a67ff556b8fccf4aad1dcaed8921a99e63528781bf116e0f62f6d/multidict-6.9.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d1f98df09bc22d298d88dee07eb5365dba1fbf3b41e876a6edb2652a8b3a6ca1", size = 250967, upload-time = "2026-09-18T12:50:36.693Z" }, + { url = "https://files.pythonhosted.org/packages/01/8d/92d7b97bd55aef4b37503fa7b622857d7e579e2303a40210b7089f7e3dcf/multidict-6.9.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c5290038269f7d96037afc47b05b286e4791adfa7046911e2a0483b6220c84fb", size = 276286, upload-time = "2026-09-18T12:50:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/60/04/f9a043a7b3513c29315cbf3740a2b94bd0272ea904c844ac8eff8a3d86f8/multidict-6.9.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8858d2ddd0bc21b4aaf189016c95840b29ddf0236a049af70a9cb4379bef9abc", size = 272479, upload-time = "2026-09-18T12:50:42.008Z" }, + { url = "https://files.pythonhosted.org/packages/1c/25/e1ec0c14b2fd3d4f959913862c88b0dd5748ba10ddca96411b17641d8bf0/multidict-6.9.0-cp315-cp315t-win32.whl", hash = "sha256:f90724dade5865b1c9429d9b058f11096e44a7f1f0e0a21eff18d98ef32c22aa", size = 55280, upload-time = "2026-09-18T12:50:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/03/55/8b81e62363ae0721b2735bcaa111c1e08d30d4d0680b591bfd3b7305e9ab/multidict-6.9.0-cp315-cp315t-win_amd64.whl", hash = "sha256:150f1e5bcc14234e99aacc931deafa52713157df947efeecc1a3b7601f2714ab", size = 62222, upload-time = "2026-09-18T12:50:46.979Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/fb518ae4d302f71a1e96496bb011409d09736a91aadd3a6ceb66a7a2f259/multidict-6.9.0-cp315-cp315t-win_arm64.whl", hash = "sha256:734c985bbd60e84d0ea523bafe139394e46b8a3f35754a70111dbcbba243f7f4", size = 56365, upload-time = "2026-09-18T12:50:49.649Z" }, + { url = "https://files.pythonhosted.org/packages/75/2a/557689d56936a83c112ee28f4c4d7698ff01c24e22d217ad8fcce19982c7/multidict-6.9.0-py3-none-any.whl", hash = "sha256:57c2445049f7d8e66306f712868219da7ff7168ef42263dc032401211bf1205c", size = 19175, upload-time = "2026-09-18T12:50:52.172Z" }, +] + [[package]] name = "mypy" version = "2.1.0" @@ -1151,6 +1785,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/2e/2e96ca6928951fe1d16744c22dc1355eda1dd5b0dd920ca1d3ab602929f8/openai_agents-0.17.7-py3-none-any.whl", hash = "sha256:51b5ae43756eea37032e430f95979ba3999af6b1ade397df6c0ffeaf1939646a", size = 856074, upload-time = "2026-06-24T05:15:31.741Z" }, ] +[package.optional-dependencies] +litellm = [ + { name = "litellm" }, +] + [[package]] name = "opentelemetry-api" version = "1.43.0" @@ -1376,6 +2015,117 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] +[[package]] +name = "propcache" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" }, + { url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" }, + { url = "https://files.pythonhosted.org/packages/83/4f/48865bd02a16ee5236bc46166b2946f37b93e07b0eae355dac0be0b216ca/propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef", size = 251768, upload-time = "2026-09-16T00:14:32.908Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/3742a5eed62317b03b4002ee865dc9fd720308bdd0da1f29a5786c630311/propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f", size = 257723, upload-time = "2026-09-16T00:14:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/ee6350fb0be9122bb6c67082a876d34b90d980d100c106af4b81023e04f4/propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546", size = 265597, upload-time = "2026-09-16T00:14:35.56Z" }, + { url = "https://files.pythonhosted.org/packages/85/9f/83a07b6ec0e043c050cfdd35fb0cf1b7897b91d554d6eea293740309afe7/propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b", size = 250424, upload-time = "2026-09-16T00:14:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/2c/a763a8251f50fba042af0fb1f02bfec4b31381e40aff760db2be7b2e1f84/propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab", size = 216748, upload-time = "2026-09-16T00:14:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e2/4d11bea8fd6a777149c6c20645f873952eab5de3a2497aa11648ec9ab6ab/propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc", size = 246533, upload-time = "2026-09-16T00:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/9f/36/6683597de4907e70c717e3588c541202c66086a72ff3db58be49de66e72c/propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937", size = 238173, upload-time = "2026-09-16T00:14:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/85/84/cb08d79f1762daafeb2b030c470cd0c725c97b8ad67412457c6f35c53e9d/propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526", size = 251128, upload-time = "2026-09-16T00:14:42.652Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0d/41b848036db6621370c1f2e5471a7da8149c730f8552a5257567721f4576/propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944", size = 214821, upload-time = "2026-09-16T00:14:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/adfae4bf9c63bccf12e2d9690a175c6579047a6eec3b5a6a5f51428c15e2/propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031", size = 254793, upload-time = "2026-09-16T00:14:45.429Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/eeca9647245d5f92e87d53e5f14335bb42fce1a7e6842c8045b364eded8b/propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161", size = 247134, upload-time = "2026-09-16T00:14:46.976Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a9/424e38838793d37160b4379c702f61c74c598fc6cd17204adbe3c554f7a8/propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8", size = 43073, upload-time = "2026-09-16T00:14:48.338Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/6e8ef26f6d510a7916064fec68d55fcbfbdf7eb01e377480d66a122152d8/propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb", size = 46190, upload-time = "2026-09-16T00:14:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/08/b9/72028c5b56ced97f456de6aefa79435ca64d7f77af78ea8cf3c76fc5195f/propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737", size = 43075, upload-time = "2026-09-16T00:14:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" }, + { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" }, + { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" }, + { url = "https://files.pythonhosted.org/packages/33/c9/07e227b930c8ae513b8ef1aae3793499be097bffcdf7aee4fb8b33db4cd1/propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565", size = 85933, upload-time = "2026-09-16T00:15:16.073Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e1/6710bb44510c4e4a8e0f004bbaf3cecfd048141309c77bae56d4e5a6ebc1/propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993", size = 50179, upload-time = "2026-09-16T00:15:17.377Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/b533b493d7025456f44518b33e53e000021a20fe7c27b88cf3d341df7186/propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b", size = 51942, upload-time = "2026-09-16T00:15:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/70ac8430e28f21e442c7bcb964eb46c4363f6881ade4aa0e978bfd8d503a/propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc", size = 232647, upload-time = "2026-09-16T00:15:19.905Z" }, + { url = "https://files.pythonhosted.org/packages/72/95/f222f13b6fe623310be0eb61a673bf26df439ce27e563ca8e422d0818777/propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d", size = 241541, upload-time = "2026-09-16T00:15:21.3Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3e/763e370340db16115c5e63ad46e21ef0770a7f06928b3d3b62d8f8edfca4/propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f", size = 245332, upload-time = "2026-09-16T00:15:22.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/d3/e97cd6f5de2176bd90ed4076c7a9b5e09d0f0b9687d00a576507988bb62c/propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9", size = 232757, upload-time = "2026-09-16T00:15:24.374Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/6766e5f60bcda26d244333aa71d0a702c1c9b21b251d543c7af5953d1eee/propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf", size = 204389, upload-time = "2026-09-16T00:15:25.667Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5e/ec4bb09a70b26ea99d76a8292c3383b960b296de2b347ac9986678f1761c/propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d", size = 228217, upload-time = "2026-09-16T00:15:27.11Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7d/b53922ba7d9e5bf797324e63aa05906ec240871899f779628df068743e2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3", size = 216947, upload-time = "2026-09-16T00:15:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/b62eee45e5ea4de094a258cbb3b01c1e856ca51ddfd95b43135c5effd1eb/propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc", size = 233457, upload-time = "2026-09-16T00:15:29.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a9/feec61ed296d993db9dd097e0f6723e3f576a647722367547495e4c5b05c/propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d", size = 204131, upload-time = "2026-09-16T00:15:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/411ef380cddad28dc001f1c6d75ec72c76cd3817030f68ec1ccfba0ec6c1/propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792", size = 234820, upload-time = "2026-09-16T00:15:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/c988229753629ef1cfd5198337a83e624780ea2b3787efe9e747c05aad2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b", size = 228350, upload-time = "2026-09-16T00:15:34.533Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/5ef1c5cf98591da3c5b952b39e6a298084cc1ce353bc70f85e82397a5036/propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea", size = 43578, upload-time = "2026-09-16T00:15:35.957Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9e/a0ac821a2229186af5e2e3c3635a78abb23cfddca57f38513ab5d70420f3/propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983", size = 46304, upload-time = "2026-09-16T00:15:37.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/19/c8d0d36a9d16cba5dcee67d389c9333b988c8986a653a61c00a451817a46/propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac", size = 43440, upload-time = "2026-09-16T00:15:39.091Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e9/42f1da77cacfc184e6ec929557ef653b7961bbf6f1da460b9221273948b3/propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0", size = 90672, upload-time = "2026-09-16T00:15:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2f/4b79940908c6ab8c795097c102999d7bc1f7e0b8604dfd1c232f9d99d67a/propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5", size = 52586, upload-time = "2026-09-16T00:15:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/02196ae6320c110235bb343f90dbd34be41f8b8964a3ee30db84ec12579e/propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d", size = 54335, upload-time = "2026-09-16T00:15:43.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/44/f48b9a131985659924df5fa5093f68fe72c7ee375329802989ba3126efc6/propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788", size = 297567, upload-time = "2026-09-16T00:15:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/418d956d2735139f77fc35262179f1f52c23aa666de5a8ab3819c1ae7854/propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56", size = 297477, upload-time = "2026-09-16T00:15:46.048Z" }, + { url = "https://files.pythonhosted.org/packages/69/fd/ff811fdb6d3d3e67fd9bbfb75881675d34a42d0ef29a45d33e3e233dde07/propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574", size = 302669, upload-time = "2026-09-16T00:15:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/fc/57/527910c455b5ec62f6871bef45d4f79fea16cb8c966ba0d4a07f0339ddc4/propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370", size = 287908, upload-time = "2026-09-16T00:15:48.99Z" }, + { url = "https://files.pythonhosted.org/packages/1d/86/f69ab82707534a0cb2057bdca04f9200a71214c7551800f9d34d6ac39e4f/propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320", size = 249804, upload-time = "2026-09-16T00:15:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/27/19/60677af50d93be4256213de7cd487f056944c048b9c0b6f2e45b3a30f666/propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9", size = 282344, upload-time = "2026-09-16T00:15:52.029Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/a0057808a91fb3b6a5f3602b528f0cdcb3d53e0ff8315d73fabdfdf8fec4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7", size = 270167, upload-time = "2026-09-16T00:15:53.466Z" }, + { url = "https://files.pythonhosted.org/packages/83/c8/f4a865490df0dc0c8531d4e59ac411cb6dc24bb255d2396a6f1c60a368f4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4", size = 286551, upload-time = "2026-09-16T00:15:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/b0/67/b4faebde9da4e8173d0e5a30e8cd31335914af7ef350b988f27fec588cfd/propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9", size = 249595, upload-time = "2026-09-16T00:15:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/52e1dd5636e9f5a27f6b5a4b4e2f33c322fd72afe956c397d82523ec4a80/propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e", size = 286700, upload-time = "2026-09-16T00:15:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0e/30b2b324b93ff31a0bab539c102aae59e84e444031b2742150a7646aa1bb/propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57", size = 280500, upload-time = "2026-09-16T00:15:59.487Z" }, + { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/58/25/895a11d1e4c5c2acc6d816e2bece34e02d9dc92f2182ae276cd819e9e804/propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760", size = 85634, upload-time = "2026-09-16T00:16:05.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/41/c0acd69271de7a1cf439e77d5d60c18575fd09bad56e798b95fa23458ea4/propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51", size = 50084, upload-time = "2026-09-16T00:16:07.384Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1a/ad561f99f90884089e6403b76c220610809429ba868a81a2e7ce115d32e0/propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae", size = 51692, upload-time = "2026-09-16T00:16:08.956Z" }, + { url = "https://files.pythonhosted.org/packages/e9/07/057bdd3a9609ffad59b06239cceee784b047f6c720247bfaa36d2103e138/propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51", size = 232947, upload-time = "2026-09-16T00:16:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dd/d36ad35986718530498a65e45e3713f9f0e6a580f192ef02d2ef7cae9b52/propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1", size = 241250, upload-time = "2026-09-16T00:16:12.056Z" }, + { url = "https://files.pythonhosted.org/packages/fb/81/f1459415cdb6c10d46942779de39bb59a77b38e5a76bb1def9227962eb45/propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f", size = 245150, upload-time = "2026-09-16T00:16:13.596Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/58b9b1460afc97a4c0b17ee89af701c4011d4d7f46470eba3aaff76a8069/propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c", size = 232166, upload-time = "2026-09-16T00:16:15.126Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c6/5a79e0eda3e7b6987d03d8c622ff6d52a42165a12e8418eb37694b9cc4b4/propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c", size = 206085, upload-time = "2026-09-16T00:16:16.713Z" }, + { url = "https://files.pythonhosted.org/packages/4e/72/940aed42c73f9da345ca2de0f6e835c726498159abca5f1ef14fb0a2af8a/propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162", size = 228460, upload-time = "2026-09-16T00:16:18.352Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/3f54e1535c8f323d91ba566044d7c2b39ff6f6a2f1d0bd9071779d07b9b3/propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a", size = 218350, upload-time = "2026-09-16T00:16:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/025890cc389ac3ec485ecec607d4a7ca47e15bfa2a465746ab98af602536/propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826", size = 233156, upload-time = "2026-09-16T00:16:21.624Z" }, + { url = "https://files.pythonhosted.org/packages/04/29/b39cae08c87c140d3d274f0a2c058cb5588e836175c3309e260b230ab07d/propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831", size = 206206, upload-time = "2026-09-16T00:16:23.204Z" }, + { url = "https://files.pythonhosted.org/packages/18/61/e16462ef18a87247dc9ebbd5c606f46d5ce67e708bd9cc734dd0d9222564/propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c", size = 234469, upload-time = "2026-09-16T00:16:24.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/84/b6a1490922427204fc47df920ed002eec709621de6b79b11592bf45c623a/propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38", size = 227311, upload-time = "2026-09-16T00:16:26.549Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5c/5a59527582e9bcb694b2f08b9894134b65a0f5f79dbff174f054f5f74ed0/propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123", size = 43512, upload-time = "2026-09-16T00:16:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/26/07/93cf699ed363681e754d7c3fad587fb09ef6b65618ee193332ad16a68d7b/propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3", size = 46264, upload-time = "2026-09-16T00:16:29.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/fef04fbdcd44a4a163cb5ff5674599c6d6fdefd64a5a459438f9ad2ba042/propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30", size = 43395, upload-time = "2026-09-16T00:16:31.5Z" }, + { url = "https://files.pythonhosted.org/packages/70/f6/7e2f4dab0b92ab46111bd48cee9ee1e5f519514c44e3779ede5358d7ada0/propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f", size = 89825, upload-time = "2026-09-16T00:16:43.115Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8b/dfeff925cb6ced97ede701d5c6a99998da963c6f2e06abbf879c9dac5b54/propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3", size = 52159, upload-time = "2026-09-16T00:16:44.754Z" }, + { url = "https://files.pythonhosted.org/packages/24/6c/924c810be5b7cf218ef47e707cf06d34adb4e3f3a31e3c24c55c6d945a88/propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68", size = 53956, upload-time = "2026-09-16T00:16:46.762Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b6/9ed0a5c939b58b6bed740a05b5d0f919f0b318d03284b4b6d81a0fe8a29a/propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c", size = 295235, upload-time = "2026-09-16T00:16:48.577Z" }, + { url = "https://files.pythonhosted.org/packages/5a/eb/5ce886e902a2e781dddf110993d5329458a9b1a8626b876c65e5e25bf413/propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589", size = 294463, upload-time = "2026-09-16T00:16:50.539Z" }, + { url = "https://files.pythonhosted.org/packages/f2/88/c98f49183ecd3e5b204a556f0ca47baa02c2206a500fe8c7ec1726297b0a/propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62", size = 300081, upload-time = "2026-09-16T00:16:52.423Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/c5090f9e6f67cbc30a2b744c7bb0f8006dcba5ec1b0d82f866ae1cc7c5c4/propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72", size = 285360, upload-time = "2026-09-16T00:16:54.141Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9c/34a55396910583ed07926669ab309dde2213a2dec05a7e946bb90ad66908/propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9", size = 248014, upload-time = "2026-09-16T00:16:56.062Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b5/c0a142b656093ca397039dd3fe166cbb87c945712b534546514a24cd2611/propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece", size = 280662, upload-time = "2026-09-16T00:16:58.044Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/fab2809c2e337fe26becea9648e84d5cef46075c91b826acb13e4f9dd04e/propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a", size = 266149, upload-time = "2026-09-16T00:16:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/3a/11/7ddf336288b2678a5f054f8da2e2bd1a719f5d4b7de714d9c6bd588a2313/propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2", size = 283097, upload-time = "2026-09-16T00:17:01.459Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ae/351b1a5225f5473c411d9a612a229ae147cf0cf65c72ad838b87219ea8e8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b", size = 248160, upload-time = "2026-09-16T00:17:03.298Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d0/7f79f061e30d135bb615c9782c94a74652033d00b49254edbbf35a9165a8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284", size = 283036, upload-time = "2026-09-16T00:17:05.238Z" }, + { url = "https://files.pythonhosted.org/packages/53/3c/016f1cad8bf4c428d748cf399b2bac603026fbfd6966e6a5579b5c5b6956/propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad", size = 279350, upload-time = "2026-09-16T00:17:06.881Z" }, + { url = "https://files.pythonhosted.org/packages/ea/60/d8f72cb24b412487ed4c397f539117d3b74c3c33dd32020e91fe00a958a8/propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec", size = 45874, upload-time = "2026-09-16T00:17:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ef/8bae0a316d406644450522f2f3d44a4e19632f5f3bb60d1d0e6c53842616/propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432", size = 48574, upload-time = "2026-09-16T00:17:10.077Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/bcc053f66a97355683884b448198e79580fae8e8fa4d96b9bb01614e9913/propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e", size = 45625, upload-time = "2026-09-16T00:17:11.377Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, +] + [[package]] name = "protobuf" version = "7.35.1" @@ -1577,6 +2327,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-discovery" version = "1.4.2" @@ -1937,6 +2699,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] +[[package]] +name = "s3transfer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, +] + [[package]] name = "semver" version = "3.0.4" @@ -1946,6 +2720,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +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" @@ -2037,6 +2820,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, ] +[[package]] +name = "tokenizers" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/1e/bc6587c5ab643b2e17776cace9070a2ae73549c86bffac9934a600bf3c31/tokenizers-0.23.2.tar.gz", hash = "sha256:7f0f085686b9de0d0079e6f874ae053600db64c5d13049e0bbc0119926d25aac", size = 385745, upload-time = "2026-09-03T08:55:42.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ed/8a443528baa6fac8dfe8c3b75b038c63ac92bb539bcabe311e227c718173/tokenizers-0.23.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:85a9a357a3764aecc904ee76bdaf8cf1ad8e5a67a1b929a487c4a39b49ed0e90", size = 3148852, upload-time = "2026-09-03T08:55:30.874Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/22da045a91732384d3a3771816bf188dc5a1f702c32e635afa7c679c0bef/tokenizers-0.23.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:986670e43691469dcee610ea0f846f91a8f84e91fc6f7a48d4c064414c0ec2bf", size = 3101593, upload-time = "2026-09-03T08:55:28.587Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/8f569ed49372a3ed8e57099bd515055fd48d7c95912c4307cda6973c2168/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a37039b5dfc4af84eb3ef0a92f4307e28936c8f9adccba2629d36f652e9bf7a2", size = 3516830, upload-time = "2026-09-03T08:55:14.741Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/e2f14c8919d5bf51874051d00d6c7b7e0e8bde6c6a2dbeddda7f642896ff/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b7e37ba198f24150f523e1242e83c4970de4a525480586be5dcc24d9add32c5", size = 3407975, upload-time = "2026-09-03T08:55:16.842Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bd/93c69152d02ef06ce47aed8b2bf4952dcf733c935a62791873932b2934d9/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43e4f2071e3cc8d5d86421c874aebc82659bb51a68bcdef5a0da75ee89511ccb", size = 3748165, upload-time = "2026-09-03T08:55:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b7/56b84b80bc96942bba8eb23751a9e8a1fce4faaf4390425e7083f721c98c/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325fee2e0418a9dc6c9ecf736a5f5f0db7875183ace9549ae339da76f7a1fbb7", size = 4024165, upload-time = "2026-09-03T08:55:18.806Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/0175e216f005c2fe08238292663aa41e4c802b216e71047a69a0e9fc6fa3/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:950d7c9426fa72406a0ffeacdbc0bb9985f5db20eb8b263f29c79aaf83105703", size = 3591899, upload-time = "2026-09-03T08:55:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/ca6b93c7820df123b2662a9469e8facc826ccc94e98fdd0d615f6431e73a/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41c2f84d172449b4dadb9cdc508e3e364076613c35b16e76ecfe47a60d1e3305", size = 3386843, upload-time = "2026-09-03T08:55:26.584Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/4f9106d317b14a80aefea9f0e3a8d07ef25f856a7607eb7f5ab894281fcb/tokenizers-0.23.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:12f0835dc2ee694746a76adf7b1567d4346a4a502ebe93fb1f5f80ea49799b78", size = 3577314, upload-time = "2026-09-03T08:55:20.825Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6a/1552b70fb0d9ab074fd3fc961435d01364e79c9058481822c3af6e8d402c/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb2f9c8a24da020ea8c11a01a19c1c2547912d92121ae4a01cfbca46125dee40", size = 9967367, upload-time = "2026-09-03T08:55:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/06/01/3ccb3a956c7528b2507b8a9714155c4baf86af593039db6ea375dd0c96c3/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f486f402f6f9abee5bb032553736813af0c710a86b2e0ca592634c55cea1f835", size = 9811886, upload-time = "2026-09-03T08:55:35.642Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/7038e612d48bda1599457f712f6bd3854eae1a9dc9c13aa47f835349db48/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:bef235815a067b2648caf6dcc7a71091b0b0fff9ee8057f6451eb9335fae52ef", size = 10146224, upload-time = "2026-09-03T08:55:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d8/8e9e4e0b287a338d8f88976729628c9d22e8a54cfaf9777018a7f7cb58a0/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c56bda1511921587789163e524d196ed8284174ac23abd7685d5ea8da6c4718", size = 10256304, upload-time = "2026-09-03T08:55:40.977Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1f/c79a01f671a49728ebb0b61f7ff9ea45663b66cab40bc0858e9859b25c16/tokenizers-0.23.2-cp310-abi3-win32.whl", hash = "sha256:debf978920d93ba9c219bd67cc4bbfaf912c9039e41e7a28b91ec15e3728c95a", size = 2592809, upload-time = "2026-09-03T08:55:48.02Z" }, + { url = "https://files.pythonhosted.org/packages/db/f7/0a69ac6b82dbccf3f71add938a161c497952749294b8dd6dfe03a819dc40/tokenizers-0.23.2-cp310-abi3-win_amd64.whl", hash = "sha256:2e96f5699d5249c9c64aa8412e044f727aae3a4098cf830f9901ec1afc361cde", size = 2863236, upload-time = "2026-09-03T08:55:46.193Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/dee84cb44175be1b4c35bd2f770727494e78f0bb38e571a623ade94dbebb/tokenizers-0.23.2-cp310-abi3-win_arm64.whl", hash = "sha256:e49c394456dd9985787fec76132438ba3fb8911f857b1bf3d40119f9292d41aa", size = 2729352, upload-time = "2026-09-03T08:55:44.345Z" }, +] + [[package]] name = "tqdm" version = "4.68.3" @@ -2316,6 +3126,131 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/eb/21a96e218375bd8b6ecd6d07cf60c8ff1a046e93cdedc3cf7bc3309edf7b/xxhash-3.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:73cecd431b4f572d38fcf1a7fe85b30eb987778ef9e7a70bc9ffcf2d64810e6f", size = 30164, upload-time = "2026-06-27T08:16:08.009Z" }, ] +[[package]] +name = "yarl" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, + { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, + { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, + { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, + { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] + [[package]] name = "zstandard" version = "0.25.0" From 0bfd96be25ba1bcba4bea10d4955c752d2eb04f7 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Mon, 21 Sep 2026 11:38:02 -0800 Subject: [PATCH 2/5] fix(AIC-3410): address LiteLLM Bugbot findings Register catalog metadata, document the server SDK peer, emit native graph telemetry, and capture streamed content on the root span. --- .sdk_metadata.json | 24 ++++ README.md | 2 +- packages/litellm-agents/README.md | 2 +- .../native_graph.py | 112 +++++++++++++++++- packages/litellm-agents/tests/test_graph.py | 103 ++++++++++++++++ packages/litellm-messages/README.md | 2 +- .../handler.py | 13 ++ .../litellm-messages/tests/test_handler.py | 19 +++ 8 files changed, 269 insertions(+), 8 deletions(-) diff --git a/.sdk_metadata.json b/.sdk_metadata.json index 5d7708f3..54c708d4 100644 --- a/.sdk_metadata.json +++ b/.sdk_metadata.json @@ -102,6 +102,30 @@ "aiAgentGraph": { "introduced": "0.1" }, "aiTrackMetrics": { "introduced": "0.1" } } + }, + "python-ai-litellm-messages": { + "name": "LiteLLM Messages AI SDK for Python", + "type": "ai-provider", + "path": "packages/litellm-messages", + "languages": ["Python"], + "aiSdkNames": ["launchdarkly-ai-litellm-messages"], + "releases": { "tag-prefix": "launchdarkly-ai-litellm-messages-" }, + "features": { + "aiCompletionConfig": { "introduced": "0.1" } + } + }, + "python-ai-litellm-agents": { + "name": "LiteLLM Agents AI SDK for Python", + "type": "ai-provider", + "path": "packages/litellm-agents", + "languages": ["Python"], + "aiSdkNames": ["launchdarkly-ai-litellm-agents"], + "releases": { "tag-prefix": "launchdarkly-ai-litellm-agents-" }, + "features": { + "aiAgentConfig": { "introduced": "0.1" }, + "aiAgentGraph": { "introduced": "0.1" }, + "aiTrackMetrics": { "introduced": "0.1" } + } } } } diff --git a/README.md b/README.md index 1f043ec3..7d5e2b91 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,7 @@ from launchdarkly_ai_litellm_agents import to_litellm_agents ctx = {"kind": "user", "key": "user-123"} result = await to_litellm_agents( resolve_graph("support-graph", context=ctx), - {"tool_handlers": registry.tools}, + {"tool_handlers": registry.tools, "context": ctx}, ).invoke("I was double charged") ``` diff --git a/packages/litellm-agents/README.md b/packages/litellm-agents/README.md index 562d11c0..2a5e96ca 100644 --- a/packages/litellm-agents/README.md +++ b/packages/litellm-agents/README.md @@ -6,7 +6,7 @@ Agents SDK's `LitellmModel`. ## Installation ```bash -pip install launchdarkly-ai-server launchdarkly-ai-litellm-agents +pip install launchdarkly-ai-server launchdarkly-server-sdk launchdarkly-ai-litellm-agents ``` Python uses LiteLLM in-process; no proxy endpoint is required. Set the provider diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py index 14bdfb84..a4a8c23d 100644 --- a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py @@ -4,7 +4,9 @@ import importlib import re +import time import types +import uuid from typing import Any from agents.extensions.models.litellm_model import LitellmModel @@ -13,7 +15,10 @@ GraphDefinition, GraphNode, compose_history, + get_client, + make_track_data, parse_template, + to_ld_context, ) from .handler import ( @@ -24,6 +29,14 @@ _tools, ) +try: + from opentelemetry import trace + from opentelemetry.trace import StatusCode as SpanStatusCode + + _HAS_OTEL = True +except ImportError: + _HAS_OTEL = False + def _name(value: str) -> str: return re.sub(r"[^a-zA-Z0-9_-]", "_", value)[:64] @@ -75,6 +88,24 @@ async def invoke( tool_handlers = options.get("tool_handlers") or {} values = variables or {} built: dict[str, Any] = {} + agent_name_to_key: dict[str, str] = {} + path: list[str] = [] + raw_ld_context = options.get("context") + ld_context = ( + to_ld_context(get_client(), raw_ld_context) + if raw_ld_context is not None + else None + ) + run_id = str(uuid.uuid4()) + start_time = time.monotonic() + + if _HAS_OTEL: + span = trace.get_tracer("@launchdarkly/ai-litellm-agents").start_span( + "ld.ai.graph" + ) + span.set_attribute("ld.ai.graph.key", graph.key) + else: + span = None async def build(node: GraphNode) -> None: if node.key in built: @@ -111,6 +142,7 @@ async def build(node: GraphNode) -> None: if "multiple values for keyword argument 'name'" not in str(exc): raise built[node.key] = types.SimpleNamespace(**kwargs) + agent_name_to_key[_name(node.key)] = node.key await build(graph.root) runner_input: str | list[dict[str, Any]] = input_text @@ -143,20 +175,90 @@ async def build(node: GraphNode) -> None: } for turn in turns ] - result = await agents.Runner.run(built[graph.root.key], runner_input) + + class _LDHooks(agents.RunHooks): # type: ignore[misc, valid-type] + async def on_agent_end(self, context: Any, agent: Any, output: Any) -> None: + node_key = agent_name_to_key.get(agent.name) + if node_key and ld_context: + node = graph.get_node(node_key) + if node: + get_client().track( + "$ld:ai:generation:success", + ld_context, + make_track_data(node, graph.key, run_id), + 1, + ) + + async def on_handoff( + self, context: Any, from_agent: Any, to_agent: Any + ) -> None: + from_key = agent_name_to_key.get(from_agent.name) + if from_key and ld_context: + from_node = graph.get_node(from_key) + if from_node: + get_client().track( + "$ld:ai:graph:handoff_success", + ld_context, + make_track_data(from_node, graph.key, run_id), + 1, + ) + to_key = agent_name_to_key.get(to_agent.name) + if to_key and to_key not in path: + path.append(to_key) + + async def on_agent_start(self, context: Any, agent: Any) -> None: + node_key = agent_name_to_key.get(agent.name) + if node_key and node_key not in path: + path.append(node_key) + + try: + result = await agents.Runner.run( + built[graph.root.key], runner_input, hooks=_LDHooks() + ) + if span: + span.set_status(SpanStatusCode.OK) + except Exception as exc: + if span: + span.record_exception(exc) + span.set_status(SpanStatusCode.ERROR, str(exc)) + span.end() + if ld_context: + get_client().track( + "$ld:ai:graph:invocation_failure", + ld_context, + make_track_data(graph.root, graph.key, run_id), + 1, + ) + raise + wrapper = getattr(result, "context_wrapper", None) usage = getattr(wrapper, "usage", None) input_tokens = int(getattr(usage, "input_tokens", 0) or 0) output_tokens = int(getattr(usage, "output_tokens", 0) or 0) + total_tokens = int( + getattr(usage, "total_tokens", input_tokens + output_tokens) + or input_tokens + output_tokens + ) + duration = int((time.monotonic() - start_time) * 1000) + if span: + span.set_attribute("ld.ai.graph.path", "->".join(path)) + span.set_attribute("gen_ai.usage.input_tokens", input_tokens) + span.set_attribute("gen_ai.usage.output_tokens", output_tokens) + span.set_attribute("gen_ai.usage.total_tokens", total_tokens) + span.end() + if ld_context: + root_td = make_track_data(graph.root, graph.key, run_id) + client = get_client() + client.track("$ld:ai:graph:duration:total", ld_context, root_td, duration) + client.track("$ld:ai:graph:total_tokens", ld_context, root_td, total_tokens) + client.track("$ld:ai:graph:path", ld_context, root_td, len(path)) + client.track("$ld:ai:graph:invocation_success", ld_context, root_td, 1) return { "response": str(result.final_output or ""), "usage": { "input": input_tokens, "output": output_tokens, - "total": int( - getattr(usage, "total_tokens", input_tokens + output_tokens) - or input_tokens + output_tokens - ), + "total": total_tokens, }, } diff --git a/packages/litellm-agents/tests/test_graph.py b/packages/litellm-agents/tests/test_graph.py index aed343b7..2a70e04b 100644 --- a/packages/litellm-agents/tests/test_graph.py +++ b/packages/litellm-agents/tests/test_graph.py @@ -14,6 +14,17 @@ from launchdarkly_ai_server import GraphDefinition, GraphEdge, GraphNode +class _FakeRunHooks: + async def on_agent_end(self, *args: Any) -> None: + return None + + async def on_handoff(self, *args: Any) -> None: + return None + + async def on_agent_start(self, *args: Any) -> None: + return None + + def _definition(*, enabled: bool = True) -> GraphDefinition: edge = GraphEdge( key="root-child", @@ -128,6 +139,7 @@ def model_factory(name: str, parameters: dict[str, Any]) -> object: Runner=runner, handoff=MagicMock(side_effect=lambda target: target), FunctionTool=MagicMock(), + RunHooks=_FakeRunHooks, ) real_import = __import__ with patch( @@ -169,6 +181,7 @@ async def test_forwards_multimodal_history_to_the_root_run(self) -> None: Runner=runner, handoff=MagicMock(side_effect=lambda target: target), FunctionTool=MagicMock(), + RunHooks=_FakeRunHooks, ) history = [ {"role": "system", "content": "ignore this"}, @@ -240,6 +253,7 @@ async def test_binds_registered_tools_to_their_graph_nodes(self) -> None: ), handoff=MagicMock(side_effect=lambda target: target), FunctionTool=function_tool, + RunHooks=_FakeRunHooks, ) search = AsyncMock(return_value="found") real_import = __import__ @@ -289,6 +303,7 @@ async def test_default_factory_uses_litellm_model_for_each_node(self) -> None: ), handoff=MagicMock(side_effect=lambda target: target), FunctionTool=MagicMock(), + RunHooks=_FakeRunHooks, ) real_import = __import__ with ( @@ -326,6 +341,7 @@ async def test_adapter_has_no_default_openai_fallback(self) -> None: ), handoff=MagicMock(side_effect=lambda target: target), FunctionTool=MagicMock(), + RunHooks=_FakeRunHooks, ) real_import = __import__ with ( @@ -351,6 +367,93 @@ async def test_adapter_has_no_default_openai_fallback(self) -> None: ).invoke("hello") fallback.assert_not_called() + async def test_success_path_emits_invocation_success_and_tokens(self) -> None: + track_calls: list[str] = [] + mock_ld_client = MagicMock() + mock_ld_client.track = MagicMock( + side_effect=lambda evt, ctx, data, val: track_calls.append(evt) + ) + agents = SimpleNamespace( + Agent=MagicMock( + side_effect=lambda **kwargs: SimpleNamespace( + name=kwargs["name"], **kwargs + ) + ), + Runner=SimpleNamespace( + run=AsyncMock( + return_value=SimpleNamespace( + final_output="done", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace( + input_tokens=4, output_tokens=2, total_tokens=6 + ) + ), + ) + ) + ), + handoff=MagicMock(side_effect=lambda target: target), + FunctionTool=MagicMock(), + RunHooks=_FakeRunHooks, + ) + real_import = __import__ + with ( + patch( + "importlib.import_module", + side_effect=lambda name: ( + agents if name == "agents" else real_import(name) + ), + ), + patch.object(native_graph_mod, "get_client", return_value=mock_ld_client), + ): + await to_litellm_agents( + _definition_awaitable(), + opts={ + "model_factory": lambda *_: object(), + "context": {"kind": "user", "key": "test"}, + }, + ).invoke("hello") + assert "$ld:ai:graph:invocation_success" in track_calls + assert "$ld:ai:graph:total_tokens" in track_calls + + async def test_error_path_emits_invocation_failure_and_rethrows(self) -> None: + track_calls: list[str] = [] + mock_ld_client = MagicMock() + mock_ld_client.track = MagicMock( + side_effect=lambda evt, ctx, data, val: track_calls.append(evt) + ) + agents = SimpleNamespace( + Agent=MagicMock( + side_effect=lambda **kwargs: SimpleNamespace( + name=kwargs["name"], **kwargs + ) + ), + Runner=SimpleNamespace( + run=AsyncMock(side_effect=RuntimeError("provider error")) + ), + handoff=MagicMock(side_effect=lambda target: target), + FunctionTool=MagicMock(), + RunHooks=_FakeRunHooks, + ) + real_import = __import__ + with ( + patch( + "importlib.import_module", + side_effect=lambda name: ( + agents if name == "agents" else real_import(name) + ), + ), + patch.object(native_graph_mod, "get_client", return_value=mock_ld_client), + ): + with pytest.raises(RuntimeError, match="provider error"): + await to_litellm_agents( + _definition_awaitable(), + opts={ + "model_factory": lambda *_: object(), + "context": {"kind": "user", "key": "test"}, + }, + ).invoke("hello") + assert "$ld:ai:graph:invocation_failure" in track_calls + async def test_disabled_graph_fails_before_runner_or_model_creation(self) -> None: model_factory = MagicMock() with pytest.raises(ValueError, match="disabled"): diff --git a/packages/litellm-messages/README.md b/packages/litellm-messages/README.md index 5b576268..5211879a 100644 --- a/packages/litellm-messages/README.md +++ b/packages/litellm-messages/README.md @@ -6,7 +6,7 @@ Provider-agnostic LaunchDarkly AI Config handler backed by LiteLLM's in-process ## Installation ```bash -pip install launchdarkly-ai-server launchdarkly-ai-litellm-messages +pip install launchdarkly-ai-server launchdarkly-server-sdk launchdarkly-ai-litellm-messages ``` Python uses LiteLLM in-process; no proxy endpoint is required. Set the provider diff --git a/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py index 8ce14123..6a7faac5 100644 --- a/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py +++ b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py @@ -495,6 +495,15 @@ async def _stream( ended: set[int] = set() open_model: Any = None try: + if capture_content: + set_input_content_attributes( + root, + True, + messages=[ + text_message(str(message["role"]), str(message["content"])) + for message in messages + ], + ) for _ in range(_MAX_TOOL_TURNS + 1): model_span = _model_span(config_value, parent) open_model = model_span @@ -560,6 +569,10 @@ async def _stream( if not fragments: full_output += turn_text completed = True + if capture_content: + set_output_content_attributes( + root, True, [text_message("assistant", full_output)] + ) _finish_span(root, total, ended, model=_model_name(config_value)) yield { "type": "done", diff --git a/packages/litellm-messages/tests/test_handler.py b/packages/litellm-messages/tests/test_handler.py index 82677381..0330d8cb 100644 --- a/packages/litellm-messages/tests/test_handler.py +++ b/packages/litellm-messages/tests/test_handler.py @@ -575,6 +575,25 @@ async def test_capture_content_records_prompt_and_completion(self) -> None: assert "question" in written assert "answer" in written + async def test_stream_capture_content_records_prompt_and_completion_on_root( + self, + ) -> None: + stream = FakeStream([_chunk(text="streamed")]) + completion = AsyncMock(return_value=stream) + span = MagicMock() + trace = MagicMock() + trace.get_tracer.return_value.start_span.return_value = span + with patch.object(handler_mod, "trace", trace): + events = await _events( + await create_litellm_messages_handler( + completion=completion, capture_content=True + ).stream(CONFIG, "question") + ) + written = str(span.set_attribute.call_args_list) + assert "question" in written + assert "streamed" in written + assert events[-1]["type"] == "done" + def test_convenience_wrapper_forwards_all_public_arguments(self) -> None: instance = MagicMock() instance.invoke.return_value = "result" From 932817194e642f9dd7f042a06abde11383c7e8e8 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Mon, 21 Sep 2026 11:51:38 -0800 Subject: [PATCH 3/5] fix(AIC-3410): close LiteLLM graph spans reliably Start tracing after graph setup and always end the graph span across success, failures, result processing errors, and cancellation. --- .../native_graph.py | 89 ++++++++++--------- packages/litellm-agents/tests/test_graph.py | 34 +++++++ 2 files changed, 81 insertions(+), 42 deletions(-) diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py index a4a8c23d..818cf059 100644 --- a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py @@ -99,14 +99,6 @@ async def invoke( run_id = str(uuid.uuid4()) start_time = time.monotonic() - if _HAS_OTEL: - span = trace.get_tracer("@launchdarkly/ai-litellm-agents").start_span( - "ld.ai.graph" - ) - span.set_attribute("ld.ai.graph.key", graph.key) - else: - span = None - async def build(node: GraphNode) -> None: if node.key in built: return @@ -211,18 +203,59 @@ async def on_agent_start(self, context: Any, agent: Any) -> None: if node_key and node_key not in path: path.append(node_key) + if _HAS_OTEL: + span = trace.get_tracer("@launchdarkly/ai-litellm-agents").start_span( + "ld.ai.graph" + ) + span.set_attribute("ld.ai.graph.key", graph.key) + else: + span = None + try: result = await agents.Runner.run( built[graph.root.key], runner_input, hooks=_LDHooks() ) if span: span.set_status(SpanStatusCode.OK) - except Exception as exc: + + wrapper = getattr(result, "context_wrapper", None) + usage = getattr(wrapper, "usage", None) + input_tokens = int(getattr(usage, "input_tokens", 0) or 0) + output_tokens = int(getattr(usage, "output_tokens", 0) or 0) + total_tokens = int( + getattr(usage, "total_tokens", input_tokens + output_tokens) + or input_tokens + output_tokens + ) + duration = int((time.monotonic() - start_time) * 1000) if span: + span.set_attribute("ld.ai.graph.path", "->".join(path)) + span.set_attribute("gen_ai.usage.input_tokens", input_tokens) + span.set_attribute("gen_ai.usage.output_tokens", output_tokens) + span.set_attribute("gen_ai.usage.total_tokens", total_tokens) + if ld_context: + root_td = make_track_data(graph.root, graph.key, run_id) + client = get_client() + client.track( + "$ld:ai:graph:duration:total", ld_context, root_td, duration + ) + client.track( + "$ld:ai:graph:total_tokens", ld_context, root_td, total_tokens + ) + client.track("$ld:ai:graph:path", ld_context, root_td, len(path)) + client.track("$ld:ai:graph:invocation_success", ld_context, root_td, 1) + return { + "response": str(result.final_output or ""), + "usage": { + "input": input_tokens, + "output": output_tokens, + "total": total_tokens, + }, + } + except BaseException as exc: + if span and isinstance(exc, Exception): span.record_exception(exc) span.set_status(SpanStatusCode.ERROR, str(exc)) - span.end() - if ld_context: + if ld_context and isinstance(exc, Exception): get_client().track( "$ld:ai:graph:invocation_failure", ld_context, @@ -230,36 +263,8 @@ async def on_agent_start(self, context: Any, agent: Any) -> None: 1, ) raise - - wrapper = getattr(result, "context_wrapper", None) - usage = getattr(wrapper, "usage", None) - input_tokens = int(getattr(usage, "input_tokens", 0) or 0) - output_tokens = int(getattr(usage, "output_tokens", 0) or 0) - total_tokens = int( - getattr(usage, "total_tokens", input_tokens + output_tokens) - or input_tokens + output_tokens - ) - duration = int((time.monotonic() - start_time) * 1000) - if span: - span.set_attribute("ld.ai.graph.path", "->".join(path)) - span.set_attribute("gen_ai.usage.input_tokens", input_tokens) - span.set_attribute("gen_ai.usage.output_tokens", output_tokens) - span.set_attribute("gen_ai.usage.total_tokens", total_tokens) - span.end() - if ld_context: - root_td = make_track_data(graph.root, graph.key, run_id) - client = get_client() - client.track("$ld:ai:graph:duration:total", ld_context, root_td, duration) - client.track("$ld:ai:graph:total_tokens", ld_context, root_td, total_tokens) - client.track("$ld:ai:graph:path", ld_context, root_td, len(path)) - client.track("$ld:ai:graph:invocation_success", ld_context, root_td, 1) - return { - "response": str(result.final_output or ""), - "usage": { - "input": input_tokens, - "output": output_tokens, - "total": total_tokens, - }, - } + finally: + if span: + span.end() return types.SimpleNamespace(invoke=invoke) diff --git a/packages/litellm-agents/tests/test_graph.py b/packages/litellm-agents/tests/test_graph.py index 2a70e04b..26d09835 100644 --- a/packages/litellm-agents/tests/test_graph.py +++ b/packages/litellm-agents/tests/test_graph.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from types import SimpleNamespace from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -454,6 +455,39 @@ async def test_error_path_emits_invocation_failure_and_rethrows(self) -> None: ).invoke("hello") assert "$ld:ai:graph:invocation_failure" in track_calls + async def test_cancellation_closes_graph_span(self) -> None: + span = MagicMock() + tracer = MagicMock() + tracer.start_span.return_value = span + agents = SimpleNamespace( + Agent=MagicMock( + side_effect=lambda **kwargs: SimpleNamespace( + name=kwargs["name"], **kwargs + ) + ), + Runner=SimpleNamespace(run=AsyncMock(side_effect=asyncio.CancelledError())), + handoff=MagicMock(side_effect=lambda target: target), + FunctionTool=MagicMock(), + RunHooks=_FakeRunHooks, + ) + real_import = __import__ + with ( + patch( + "importlib.import_module", + side_effect=lambda name: ( + agents if name == "agents" else real_import(name) + ), + ), + patch.object(native_graph_mod, "_HAS_OTEL", True), + patch.object(native_graph_mod.trace, "get_tracer", return_value=tracer), + ): + with pytest.raises(asyncio.CancelledError): + await to_litellm_agents( + _definition_awaitable(), + opts={"model_factory": lambda *_: object()}, + ).invoke("hello") + span.end.assert_called_once_with() + async def test_disabled_graph_fails_before_runner_or_model_creation(self) -> None: model_factory = MagicMock() with pytest.raises(ValueError, match="disabled"): From 0e598eeb6cffb6cbc8504671c3f6b50b2b5f3bbd Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Mon, 21 Sep 2026 11:59:27 -0800 Subject: [PATCH 4/5] fix(AIC-3410): bind Agents SDK RunHooks before subclassing mypy cannot resolve a base class read directly off a dynamically imported module, so bind the name first as the OpenAI adapter does. Co-authored-by: Cursor --- .../src/launchdarkly_ai_litellm_agents/native_graph.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py index 818cf059..33df61c2 100644 --- a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py @@ -84,6 +84,7 @@ async def invoke( raise ValueError(f'Agent graph "{graph.key}" has no root node') agents = importlib.import_module("agents") + RunHooks = agents.RunHooks factory: ModelFactory = options.get("model_factory") or _default_factory tool_handlers = options.get("tool_handlers") or {} values = variables or {} @@ -168,7 +169,7 @@ async def build(node: GraphNode) -> None: for turn in turns ] - class _LDHooks(agents.RunHooks): # type: ignore[misc, valid-type] + class _LDHooks(RunHooks): # type: ignore[misc, valid-type] async def on_agent_end(self, context: Any, agent: Any, output: Any) -> None: node_key = agent_name_to_key.get(agent.name) if node_key and ld_context: From 3683c970a52c0a353b6ebc92d9bc582ea62c81be Mon Sep 17 00:00:00 2001 From: Alexis Georges Date: Fri, 25 Sep 2026 12:38:49 -0400 Subject: [PATCH 5/5] refactor(litellm-agents): rename the graph span to launchdarkly.graph --- .../src/launchdarkly_ai_litellm_agents/native_graph.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py index 33df61c2..9aff4678 100644 --- a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/native_graph.py @@ -206,9 +206,9 @@ async def on_agent_start(self, context: Any, agent: Any) -> None: if _HAS_OTEL: span = trace.get_tracer("@launchdarkly/ai-litellm-agents").start_span( - "ld.ai.graph" + "launchdarkly.graph" ) - span.set_attribute("ld.ai.graph.key", graph.key) + span.set_attribute("launchdarkly.graph.key", graph.key) else: span = None @@ -229,7 +229,7 @@ async def on_agent_start(self, context: Any, agent: Any) -> None: ) duration = int((time.monotonic() - start_time) * 1000) if span: - span.set_attribute("ld.ai.graph.path", "->".join(path)) + span.set_attribute("launchdarkly.graph.path", "->".join(path)) span.set_attribute("gen_ai.usage.input_tokens", input_tokens) span.set_attribute("gen_ai.usage.output_tokens", output_tokens) span.set_attribute("gen_ai.usage.total_tokens", total_tokens)