From 78d8b9e929788cfc3911e29dc82fde9347d77da0 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Mon, 21 Sep 2026 11:14:35 -0800 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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) From 3cee111bf9c83066534866f92ba1d5aed09749c8 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Thu, 8 Oct 2026 10:32:50 -0800 Subject: [PATCH 6/9] fix(AIC-3410): stop LiteLLM configs from choosing the request destination Allowlist generation settings, take connection settings from the handler, and record cancelled streams and graph node events the way the other adapters do. Co-authored-by: Cursor --- packages/litellm-agents/README.md | 2 + .../launchdarkly_ai_litellm_agents/handler.py | 82 ++++++++++-------- .../native_graph.py | 53 ++++++++---- packages/litellm-agents/tests/test_graph.py | 44 ++++++---- packages/litellm-agents/tests/test_handler.py | 34 ++++++++ packages/litellm-messages/README.md | 2 + .../handler.py | 84 ++++++++++++------- .../litellm-messages/tests/test_handler.py | 28 +++++++ 8 files changed, 227 insertions(+), 102 deletions(-) diff --git a/packages/litellm-agents/README.md b/packages/litellm-agents/README.md index 2a5e96ca..a76b2d68 100644 --- a/packages/litellm-agents/README.md +++ b/packages/litellm-agents/README.md @@ -1,5 +1,7 @@ # `launchdarkly-ai-litellm-agents` +**Experimental.** This package stays on 0.x until the Python and JavaScript LiteLLM integrations match. + Provider-agnostic LaunchDarkly AI Config and graph handlers built on the OpenAI Agents SDK's `LitellmModel`. diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py index 03052eb5..7343c93b 100644 --- a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py @@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator, Callable from typing import Any -from agents import ModelSettings +from agents import ModelSettings, RunConfig from agents.agent_output import AgentOutputSchemaBase from agents.extensions.models.litellm_model import LitellmModel from agents.lifecycle import RunHooksBase @@ -48,10 +48,8 @@ "store", "include_usage", "top_logprobs", - "extra_headers", - "extra_query", - "extra_body", } +_RUN_CONFIG = RunConfig(tracing_disabled=True) def _json_schema(output_format: dict[str, Any]) -> dict[str, Any]: @@ -100,41 +98,35 @@ def validate_json(self, json_str: str) -> Any: 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) + allowed = _MODEL_SETTING_FIELDS | {"max_turns"} + parameters = ( + {key: value for key, value in raw.items() if key in allowed} + if isinstance(raw, dict) + else {} + ) 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 _litellm_model( + name: str, *, base_url: str | None = None, api_key: str | None = None +) -> Any: + # Connection settings come from the handler, never from model.parameters. + kwargs: dict[str, Any] = {"model": name} + if base_url is not None: + kwargs["base_url"] = base_url + if api_key is not None: + kwargs["api_key"] = api_key + return LitellmModel(**kwargs) 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) + return ModelSettings( + **{ + key: value + for key, value in parameters.items() + if key in _MODEL_SETTING_FIELDS + } + ) def _instructions(config_value: AiConfigRep, variables: dict[str, Any]) -> str | None: @@ -490,8 +482,15 @@ def create_litellm_agents_handler( *, model_factory: ModelFactory | None = None, capture_content: bool = False, + base_url: str | None = None, + api_key: str | None = None, ) -> ProviderHandler: - factory = model_factory or _default_model_factory + if model_factory is None: + + def model_factory(name: str, _parameters: dict[str, Any]) -> Any: + return _litellm_model(name, base_url=base_url, api_key=api_key) + + factory = model_factory async def call( config_value: AiConfigRep, @@ -525,7 +524,11 @@ async def call( messages=[text_message("user", str(prompt))], ) result = await importlib.import_module("agents").Runner.run( - agent, prompt, max_turns=max_turns, hooks=hooks + agent, + prompt, + max_turns=max_turns, + hooks=hooks, + run_config=_RUN_CONFIG, ) output = result.final_output if capture_content: @@ -610,7 +613,7 @@ async def _stream( messages=[text_message("user", str(prompt))], ) streamed = importlib.import_module("agents").Runner.run_streamed( - agent, prompt, max_turns=max_turns, hooks=hooks + agent, prompt, max_turns=max_turns, hooks=hooks, run_config=_RUN_CONFIG ) collected = "" async for event in streamed.stream_events(): @@ -674,10 +677,15 @@ def litellm_agents( variables = kwargs.pop("variables", None) capture_content = bool(kwargs.pop("capture_content", False)) model_factory = kwargs.pop("model_factory", None) + base_url = kwargs.pop("base_url", None) + api_key = kwargs.pop("api_key", None) return config( key=config_key, handler=create_litellm_agents_handler( - model_factory=model_factory, capture_content=capture_content + model_factory=model_factory, + capture_content=capture_content, + base_url=base_url, + api_key=api_key, ), **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 index 9aff4678..261a2ef5 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 @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import importlib import re import time @@ -9,7 +10,7 @@ import uuid from typing import Any -from agents.extensions.models.litellm_model import LitellmModel +from agents import RunConfig from launchdarkly_ai_server import ( GraphDefinition, @@ -23,6 +24,7 @@ from .handler import ( ModelFactory, + _litellm_model, _map_agent_content, _model_config, _model_settings, @@ -42,14 +44,6 @@ 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"): @@ -85,7 +79,13 @@ async def invoke( agents = importlib.import_module("agents") RunHooks = agents.RunHooks - factory: ModelFactory = options.get("model_factory") or _default_factory + base_url = options.get("base_url") + api_key = options.get("api_key") + + def default_factory(name: str, _parameters: dict[str, Any]) -> Any: + return _litellm_model(name, base_url=base_url, api_key=api_key) + + factory: ModelFactory = options.get("model_factory") or default_factory tool_handlers = options.get("tool_handlers") or {} values = variables or {} built: dict[str, Any] = {} @@ -195,14 +195,27 @@ async def on_handoff( 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) + if not node_key or node_key in path: + return + index = len(path) + path.append(node_key) + if not ld_context: + return + node = graph.get_node(node_key) + if node: + get_client().track( + "$ld:ai:graph:node", + ld_context, + { + **make_track_data(node, graph.key, run_id), + "nodeKey": node_key, + "index": index, + }, + 1, + ) if _HAS_OTEL: span = trace.get_tracer("@launchdarkly/ai-litellm-agents").start_span( @@ -214,7 +227,10 @@ async def on_agent_start(self, context: Any, agent: Any) -> None: try: result = await agents.Runner.run( - built[graph.root.key], runner_input, hooks=_LDHooks() + built[graph.root.key], + runner_input, + hooks=_LDHooks(), + run_config=RunConfig(tracing_disabled=True), ) if span: span.set_status(SpanStatusCode.OK) @@ -242,7 +258,6 @@ async def on_agent_start(self, context: Any, agent: Any) -> None: 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 ""), @@ -252,6 +267,10 @@ async def on_agent_start(self, context: Any, agent: Any) -> None: "total": total_tokens, }, } + except asyncio.CancelledError: + if span is not None: + span.set_attribute("launchdarkly.run.cancelled", True) + raise except BaseException as exc: if span and isinstance(exc, Exception): span.record_exception(exc) diff --git a/packages/litellm-agents/tests/test_graph.py b/packages/litellm-agents/tests/test_graph.py index 26d09835..bd9aec16 100644 --- a/packages/litellm-agents/tests/test_graph.py +++ b/packages/litellm-agents/tests/test_graph.py @@ -10,6 +10,7 @@ import pytest import launchdarkly_ai_litellm_agents.graph as graph_mod +import launchdarkly_ai_litellm_agents.handler as handler_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 @@ -314,7 +315,7 @@ async def test_default_factory_uses_litellm_model_for_each_node(self) -> None: agents if name == "agents" else real_import(name) ), ), - patch.object(native_graph_mod, "LitellmModel", litellm_model), + patch.object(handler_mod, "LitellmModel", litellm_model), ): await to_litellm_agents(_definition_awaitable()).invoke("hello") assert {call["model"] for call in created_models} == { @@ -369,29 +370,32 @@ async def test_adapter_has_no_default_openai_fallback(self) -> None: fallback.assert_not_called() async def test_success_path_emits_invocation_success_and_tokens(self) -> None: - track_calls: list[str] = [] + track_calls: list[tuple[str, dict[str, Any]]] = [] mock_ld_client = MagicMock() mock_ld_client.track = MagicMock( - side_effect=lambda evt, ctx, data, val: track_calls.append(evt) + side_effect=lambda evt, ctx, data, val: track_calls.append((evt, data)) ) + + async def run( + agent: Any, _prompt: Any, hooks: Any = None, **_kwargs: Any + ) -> Any: + await hooks.on_agent_start(None, agent) + return SimpleNamespace( + final_output="done", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace( + input_tokens=4, output_tokens=2, total_tokens=6 + ) + ), + ) + 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 - ) - ), - ) - ) - ), + Runner=SimpleNamespace(run=run), handoff=MagicMock(side_effect=lambda target: target), FunctionTool=MagicMock(), RunHooks=_FakeRunHooks, @@ -413,8 +417,13 @@ async def test_success_path_emits_invocation_success_and_tokens(self) -> None: "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 + events = [name for name, _data in track_calls] + assert "$ld:ai:graph:invocation_success" in events + assert "$ld:ai:graph:total_tokens" in events + assert "$ld:ai:graph:path" not in events + nodes = [data for name, data in track_calls if name == "$ld:ai:graph:node"] + assert [data["nodeKey"] for data in nodes] == ["root"] + assert nodes[0]["index"] == 0 async def test_error_path_emits_invocation_failure_and_rethrows(self) -> None: track_calls: list[str] = [] @@ -487,6 +496,7 @@ async def test_cancellation_closes_graph_span(self) -> None: opts={"model_factory": lambda *_: object()}, ).invoke("hello") span.end.assert_called_once_with() + span.set_attribute.assert_any_call("launchdarkly.run.cancelled", True) async def test_disabled_graph_fails_before_runner_or_model_creation(self) -> None: model_factory = MagicMock() diff --git a/packages/litellm-agents/tests/test_handler.py b/packages/litellm-agents/tests/test_handler.py index e860b76d..a330005c 100644 --- a/packages/litellm-agents/tests/test_handler.py +++ b/packages/litellm-agents/tests/test_handler.py @@ -153,8 +153,40 @@ async def test_evaluated_model_constructs_agents_sdk_litellm_model(self) -> None 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 "base_url" not in litellm_model.call_args.kwargs + assert "api_key" not in litellm_model.call_args.kwargs assert FakeAgent.created[0]["model"] is litellm_model.return_value + async def test_config_cannot_redirect_the_provider_key(self) -> None: + agents = _agents_module() + litellm_model = MagicMock(return_value=object()) + config = { + **CONFIG, + "model": { + "name": "anthropic/claude-sonnet-4", + "parameters": { + "temperature": 0.2, + "api_base": "http://127.0.0.1:9", + "api_key": "provider-secret", + "base_url": "http://127.0.0.1:9", + "extra_headers": {"X-Smuggled": "1"}, + }, + }, + } + with ( + _patch_agents(agents), + patch.object(handler_mod, "LitellmModel", litellm_model), + ): + await create_litellm_agents_handler( + base_url="https://proxy.example", api_key="handler-key" + )(config, "q") + assert litellm_model.call_args.kwargs["base_url"] == "https://proxy.example" + assert litellm_model.call_args.kwargs["api_key"] == "handler-key" + settings = FakeAgent.created[0]["model_settings"] + assert settings.temperature == 0.2 + assert not settings.extra_args + assert not settings.extra_headers + async def test_model_factory_is_evaluated_per_call_and_instance_scoped( self, ) -> None: @@ -399,6 +431,7 @@ async def run( *, max_turns: int, hooks: Any, + **_kwargs: Any, ) -> Any: await hooks.on_llm_start( SimpleNamespace(), agent, agent.instructions, prompt @@ -459,6 +492,7 @@ async def run( *, max_turns: int, hooks: Any, + **_kwargs: Any, ) -> Any: await hooks.on_llm_start( SimpleNamespace(), agent, agent.instructions, prompt diff --git a/packages/litellm-messages/README.md b/packages/litellm-messages/README.md index 5211879a..5b492b1c 100644 --- a/packages/litellm-messages/README.md +++ b/packages/litellm-messages/README.md @@ -1,5 +1,7 @@ # `launchdarkly-ai-litellm-messages` +**Experimental.** This package stays on 0.x until the Python and JavaScript LiteLLM integrations match. + Provider-agnostic LaunchDarkly AI Config handler backed by LiteLLM's in-process `acompletion` API. 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 6a7faac5..55afb00a 100644 --- a/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py +++ b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py @@ -15,6 +15,7 @@ ProviderHandler, SpanUsage, config, + content_to_text, create_handler, end_span_once, image_block_to_url, @@ -39,14 +40,24 @@ def _json_schema(output_format: dict[str, Any]) -> dict[str, Any]: return schema -_OWNED_PARAMETERS = { - "model", - "messages", - "tools", - "stream", - "stream_options", - "response_format", -} +# Generation settings only. Connection and credential keys (api_base, api_key, +# base_url, extra_headers, aws_*, vertex_*, ...) stay out: LiteLLM reads them +# as call arguments, and an AI Config must not choose where the provider key goes. +_FORWARDED_PARAMETERS = frozenset( + { + "frequency_penalty", + "max_completion_tokens", + "max_tokens", + "parallel_tool_calls", + "presence_penalty", + "reasoning_effort", + "seed", + "stop", + "temperature", + "tool_choice", + "top_p", + } +) _MAX_TOOL_TURNS = 10 @@ -147,6 +158,21 @@ def _build_tools( ] +def _message_text(content: Any) -> str: + if isinstance(content, str): + return content + if isinstance(content, list): + return content_to_text(content) + return "" if content is None else str(content) + + +def _span_messages(messages: list[dict[str, Any]]) -> list[Any]: + return [ + text_message(str(message["role"]), _message_text(message.get("content"))) + for message in messages + ] + + def _request( config_value: AiConfigRep, messages: list[dict[str, Any]], @@ -156,9 +182,15 @@ def _request( ) -> 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 = ( + { + key: value + for key, value in raw_parameters.items() + if key in _FORWARDED_PARAMETERS + } + if isinstance(raw_parameters, dict) + else {} + ) parameters.update( { "model": _model_name(config_value), @@ -358,10 +390,7 @@ async def call( set_input_content_attributes( root, True, - messages=[ - text_message(str(message["role"]), str(message["content"])) - for message in messages - ], + messages=_span_messages(messages), ) for _ in range(_MAX_TOOL_TURNS + 1): model_span = _model_span(config_value, parent) @@ -371,12 +400,7 @@ async def call( set_input_content_attributes( model_span, True, - messages=[ - text_message( - str(message["role"]), str(message["content"]) - ) - for message in messages - ], + messages=_span_messages(messages), ) response = await completion( **_request(config_value, messages, tools, stream=False) @@ -494,15 +518,13 @@ async def _stream( full_output = "" ended: set[int] = set() open_model: Any = None + cancelled = False try: if capture_content: set_input_content_attributes( root, True, - messages=[ - text_message(str(message["role"]), str(message["content"])) - for message in messages - ], + messages=_span_messages(messages), ) for _ in range(_MAX_TOOL_TURNS + 1): model_span = _model_span(config_value, parent) @@ -513,10 +535,7 @@ async def _stream( set_input_content_attributes( model_span, True, - messages=[ - text_message(str(message["role"]), str(message["content"])) - for message in messages - ], + messages=_span_messages(messages), ) provider_stream = await completion( **_request(config_value, messages, tools, stream=True) @@ -607,6 +626,9 @@ async def _stream( ) ) raise RuntimeError("Tool loop exceeded the maximum number of turns") + except asyncio.CancelledError: + cancelled = True + raise except Exception as exc: if usage_reported: root.set_attribute("gen_ai.response.model", _model_name(config_value)) @@ -622,11 +644,11 @@ async def _stream( pass if not completed: if open_model is not None: - end_span_once(open_model, ended, abandoned=True) + end_span_once(open_model, ended, abandoned=True, cancelled=cancelled) 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) + end_span_once(root, ended, abandoned=True, cancelled=cancelled) def litellm_messages( diff --git a/packages/litellm-messages/tests/test_handler.py b/packages/litellm-messages/tests/test_handler.py index 0330d8cb..5676b495 100644 --- a/packages/litellm-messages/tests/test_handler.py +++ b/packages/litellm-messages/tests/test_handler.py @@ -5,6 +5,7 @@ from __future__ import annotations +import asyncio import json from collections.abc import AsyncIterator from types import SimpleNamespace @@ -187,6 +188,9 @@ async def test_evaluated_model_and_owned_parameter_precedence(self) -> None: "tools": ["wrong"], "stream": True, "response_format": {"type": "wrong"}, + "api_base": "http://127.0.0.1:9", + "api_key": "provider-secret", + "extra_headers": {"X-Smuggled": "1"}, }, }, } @@ -198,6 +202,9 @@ async def test_evaluated_model_and_owned_parameter_precedence(self) -> None: assert request.get("tools") != ["wrong"] assert request["stream"] is False assert "response_format" not in request + assert "api_base" not in request + assert "api_key" not in request + assert "extra_headers" not in request class TestPromptMapping: @@ -471,6 +478,27 @@ async def test_abandonment_closes_provider_stream(self) -> None: await gen.aclose() assert stream.closed is True + async def test_cancelled_stream_is_marked_cancelled_not_abandoned(self) -> None: + class CancellingStream(FakeStream): + async def _iterate(self) -> AsyncIterator[Any]: + yield _chunk(text="one") + raise asyncio.CancelledError + + recorder = SpanRecorder() + completion = AsyncMock(return_value=CancellingStream([])) + with ( + patch.object(handler_mod, "trace", recorder), + pytest.raises(asyncio.CancelledError), + ): + await _events( + await create_litellm_messages_handler(completion=completion).stream( + CONFIG, "q" + ) + ) + root = recorder.spans[0] + assert root.attributes.get("launchdarkly.run.cancelled") is True + assert "launchdarkly.stream.abandoned" not in root.attributes + async def test_stream_error_closes_transport_and_propagates(self) -> None: class BrokenStream(FakeStream): async def _iterate(self) -> AsyncIterator[Any]: From 94d481f63b20b9e197ab047c3d2f2ccc51e863e5 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Wed, 21 Oct 2026 10:34:35 -0800 Subject: [PATCH 7/9] fix(AIC-3410): align LiteLLM with main's usage and capture contracts, trim the agents allowlist - _version.py plus report_usage in every public helper; wrappers use the internal builders. - capture_content defaults to None and resolves at call time. - litellm_graph forwards base_url and api_key. - Drop metadata, include_usage and top_logprobs; reasoning becomes a typed Reasoning with effort/summary only. - to_litellm_agents: setup inside the try, graph-key totals, handoff target in the path. Co-authored-by: Cursor --- .../__init__.py | 5 +- .../_version.py | 4 + .../launchdarkly_ai_litellm_agents/graph.py | 19 ++- .../launchdarkly_ai_litellm_agents/handler.py | 66 +++++--- .../native_graph.py | 151 ++++++++++-------- packages/litellm-agents/tests/test_graph.py | 106 +++++++++++- packages/litellm-agents/tests/test_handler.py | 42 ++++- .../litellm-agents/tests/test_sdk_usage.py | 74 +++++++++ .../__init__.py | 5 +- .../_version.py | 4 + .../handler.py | 39 +++-- .../litellm-messages/tests/test_handler.py | 2 +- .../litellm-messages/tests/test_sdk_usage.py | 72 +++++++++ 13 files changed, 477 insertions(+), 112 deletions(-) create mode 100644 packages/litellm-agents/src/launchdarkly_ai_litellm_agents/_version.py create mode 100644 packages/litellm-agents/tests/test_sdk_usage.py create mode 100644 packages/litellm-messages/src/launchdarkly_ai_litellm_messages/_version.py create mode 100644 packages/litellm-messages/tests/test_sdk_usage.py diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/__init__.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/__init__.py index 7edf1562..e086d01e 100644 --- a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/__init__.py +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/__init__.py @@ -1,9 +1,8 @@ """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 ._version import PACKAGE_NAME, __version__ from .graph import litellm_graph from .handler import create_litellm_agents_handler, litellm_agents from .native_graph import to_litellm_agents @@ -15,4 +14,4 @@ "to_litellm_agents", ] -register_ai_sdk_package("launchdarkly-ai-litellm-agents", __version__) +register_ai_sdk_package(PACKAGE_NAME, __version__) diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/_version.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/_version.py new file mode 100644 index 00000000..322d1064 --- /dev/null +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/_version.py @@ -0,0 +1,4 @@ +"""This package's identity, as sent with ``$ld:ai:sdk:info`` and ``$ld:ai:sdk:usage``.""" + +PACKAGE_NAME = "launchdarkly-ai-litellm-agents" +__version__ = "0.1.0" # x-release-please-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 index d8fbbc70..76b82dcc 100644 --- a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/graph.py +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/graph.py @@ -4,23 +4,30 @@ from typing import Any -from launchdarkly_ai_server import graph +from launchdarkly_ai_server import _graph, report_usage -from .handler import ModelFactory, create_litellm_agents_handler +from ._version import PACKAGE_NAME, __version__ +from .handler import ModelFactory, _create_litellm_agents_handler def litellm_graph( key: str, *, model_factory: ModelFactory | None = None, - capture_content: bool = False, + capture_content: bool | None = None, + base_url: str | None = None, + api_key: str | None = None, **options: Any, ) -> Any: - return graph( + report_usage("litellm-agents.litellmGraph", PACKAGE_NAME, __version__) + return _graph( key, handlers=[ - create_litellm_agents_handler( - model_factory=model_factory, capture_content=capture_content + _create_litellm_agents_handler( + model_factory=model_factory, + capture_content=capture_content, + base_url=base_url, + api_key=api_key, ) ], **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 index 7343c93b..10ddcde4 100644 --- a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py @@ -10,6 +10,7 @@ from agents.agent_output import AgentOutputSchemaBase from agents.extensions.models.litellm_model import LitellmModel from agents.lifecycle import RunHooksBase +from openai.types.shared import Reasoning from opentelemetry import trace from opentelemetry.trace import StatusCode @@ -18,12 +19,13 @@ LDContext, ProviderHandler, SpanUsage, - config, + _config, + _create_handler, content_to_text, - create_handler, end_span_once, image_block_to_url, parse_template, + report_usage, set_input_content_attributes, set_ld_span_attributes, set_model_identity_attributes, @@ -32,6 +34,9 @@ set_usage_span_attributes, text_message, ) +from launchdarkly_ai_server.lifecycle import effective_capture_content + +from ._version import PACKAGE_NAME, __version__ ModelFactory = Callable[[str, dict[str, Any]], Any] _MODEL_SETTING_FIELDS = { @@ -44,10 +49,7 @@ "max_tokens", "reasoning", "verbosity", - "metadata", "store", - "include_usage", - "top_logprobs", } _RUN_CONFIG = RunConfig(tracing_disabled=True) @@ -120,13 +122,21 @@ def _litellm_model( def _model_settings(parameters: dict[str, Any]) -> ModelSettings: - return ModelSettings( - **{ - key: value - for key, value in parameters.items() - if key in _MODEL_SETTING_FIELDS + settings = { + key: value for key, value in parameters.items() if key in _MODEL_SETTING_FIELDS + } + # Only ``effort`` and ``summary`` reach the model, as in the TypeScript SDK; any other + # key, or a non-object value, is dropped rather than failing ModelSettings validation. + reasoning = settings.pop("reasoning", None) + if isinstance(reasoning, dict): + picked = { + k: reasoning[k] + for k in ("effort", "summary") + if reasoning.get(k) is not None } - ) + if picked: + settings["reasoning"] = Reasoning(**picked) + return ModelSettings(**settings) def _instructions(config_value: AiConfigRep, variables: dict[str, Any]) -> str | None: @@ -481,10 +491,28 @@ def _agent_and_prompt( def create_litellm_agents_handler( *, model_factory: ModelFactory | None = None, - capture_content: bool = False, + capture_content: bool | None = None, base_url: str | None = None, api_key: str | None = None, ) -> ProviderHandler: + report_usage("litellm-agents.createLiteLLMAgentsHandler", PACKAGE_NAME, __version__) + return _create_litellm_agents_handler( + model_factory=model_factory, + capture_content=capture_content, + base_url=base_url, + api_key=api_key, + ) + + +def _create_litellm_agents_handler( + *, + model_factory: ModelFactory | None = None, + capture_content: bool | None = None, + base_url: str | None = None, + api_key: str | None = None, +) -> ProviderHandler: + """Non-reporting :func:`create_litellm_agents_handler`, used by this package's wrappers.""" + content_override = capture_content if model_factory is None: def model_factory(name: str, _parameters: dict[str, Any]) -> Any: @@ -499,6 +527,7 @@ async def call( variables: dict[str, Any] | None = None, history: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: + capture_content = effective_capture_content(content_override) import importlib values = variables or {} @@ -571,14 +600,14 @@ def stream( variables or {}, history, factory, - capture_content=capture_content, + capture_content=effective_capture_content(content_override), ) - return create_handler( + return _create_handler( ("*", "agent"), call, stream, - capture_content=capture_content, + capture_content=content_override, ) @@ -675,13 +704,14 @@ def litellm_agents( **kwargs: Any, ) -> Any: variables = kwargs.pop("variables", None) - capture_content = bool(kwargs.pop("capture_content", False)) + capture_content = kwargs.pop("capture_content", None) model_factory = kwargs.pop("model_factory", None) base_url = kwargs.pop("base_url", None) api_key = kwargs.pop("api_key", None) - return config( + report_usage("litellm-agents.litellmAgents", PACKAGE_NAME, __version__) + return _config( key=config_key, - handler=create_litellm_agents_handler( + handler=_create_litellm_agents_handler( model_factory=model_factory, capture_content=capture_content, base_url=base_url, 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 261a2ef5..30c1b5b1 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 @@ -19,9 +19,12 @@ get_client, make_track_data, parse_template, + report_usage, to_ld_context, ) +from launchdarkly_ai_server.utils import make_graph_track_data +from ._version import PACKAGE_NAME, __version__ from .handler import ( ModelFactory, _litellm_model, @@ -64,6 +67,7 @@ def to_litellm_agents( definition: Any, opts: dict[str, Any] | None = None, ) -> Any: + report_usage("litellm-agents.toLiteLLMAgents", PACKAGE_NAME, __version__) options = opts or {} async def invoke( @@ -137,75 +141,58 @@ async def build(node: GraphNode) -> None: 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 - if history: - config_messages = ( - [] - if graph.root.config.get("instructions") - else [ + # One try covers setup and the run, so a setup error (a model factory or + # tool conversion that raises) is recorded and tracked as an invocation + # failure, like a run error. + if _HAS_OTEL: + span = trace.get_tracer("@launchdarkly/ai-litellm-agents").start_span( + "launchdarkly.graph" + ) + span.set_attribute("launchdarkly.graph.key", graph.key) + else: + span = None + + try: + 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": message.get("role", "user"), - "content": parse_template( - str(message.get("content", "")), values + "role": turn["role"], + "content": _map_agent_content( + str(turn["role"]), turn.get("content", "") ), } - for message in (graph.root.config.get("messages") or []) - if message.get("role") != "system" + for turn in turns ] - ) - 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 - ] - - 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: - 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, - ) - - async def on_agent_start(self, context: Any, agent: Any) -> None: - node_key = agent_name_to_key.get(agent.name) + def _enter(agent_name: str) -> None: + node_key = agent_name_to_key.get(agent_name) if not node_key or node_key in path: return index = len(path) path.append(node_key) - if not ld_context: - return - node = graph.get_node(node_key) - if node: + node = graph.get_node(node_key) if ld_context else None + if ld_context and node: get_client().track( "$ld:ai:graph:node", ld_context, @@ -217,15 +204,39 @@ async def on_agent_start(self, context: Any, agent: Any) -> None: 1, ) - if _HAS_OTEL: - span = trace.get_tracer("@launchdarkly/ai-litellm-agents").start_span( - "launchdarkly.graph" - ) - span.set_attribute("launchdarkly.graph.key", graph.key) - else: - span = None + 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: + 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: + _enter(to_agent.name) + 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, + ) + + async def on_agent_start(self, context: Any, agent: Any) -> None: + _enter(agent.name) - try: result = await agents.Runner.run( built[graph.root.key], runner_input, @@ -250,7 +261,7 @@ async def on_agent_start(self, context: Any, agent: Any) -> None: 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) + root_td = make_graph_track_data(graph.key, run_id) client = get_client() client.track( "$ld:ai:graph:duration:total", ld_context, root_td, duration @@ -279,7 +290,7 @@ async def on_agent_start(self, context: Any, agent: Any) -> None: get_client().track( "$ld:ai:graph:invocation_failure", ld_context, - make_track_data(graph.root, graph.key, run_id), + make_graph_track_data(graph.key, run_id), 1, ) raise diff --git a/packages/litellm-agents/tests/test_graph.py b/packages/litellm-agents/tests/test_graph.py index bd9aec16..77ac400c 100644 --- a/packages/litellm-agents/tests/test_graph.py +++ b/packages/litellm-agents/tests/test_graph.py @@ -94,7 +94,7 @@ async def _definition_awaitable_from( class TestGraphConvenience: def test_binds_wildcard_litellm_agent_handler(self) -> None: graph = MagicMock(return_value=object()) - with patch.object(graph_mod, "graph", graph): + with patch.object(graph_mod, "_graph", graph): result = litellm_graph( "graph-key", context={"kind": "user", "key": "u"}, @@ -108,6 +108,18 @@ def test_binds_wildcard_litellm_agent_handler(self) -> None: assert handlers[0].provides_for == ("*", "agent") assert "model_factory" not in graph.call_args.kwargs + def test_forwards_proxy_connection_options_to_the_handler(self) -> None: + create = MagicMock(return_value=MagicMock(provides_for=("*", "agent"))) + with ( + patch.object(graph_mod, "_graph", MagicMock()) as graph, + patch.object(graph_mod, "_create_litellm_agents_handler", create), + ): + litellm_graph("graph-key", base_url="http://proxy:4000", api_key="sk-proxy") + assert create.call_args.kwargs["base_url"] == "http://proxy:4000" + assert create.call_args.kwargs["api_key"] == "sk-proxy" + assert "base_url" not in graph.call_args.kwargs + assert "api_key" not in graph.call_args.kwargs + class TestNativeGraphAdapter: async def test_creates_every_node_with_its_authoritative_litellm_model( @@ -506,3 +518,95 @@ async def test_disabled_graph_fails_before_runner_or_model_creation(self) -> Non opts={"model_factory": model_factory}, ).invoke("hello") model_factory.assert_not_called() + + +class TestNativeGraphTelemetry: + def _agents(self, run: Any) -> Any: + return SimpleNamespace( + Agent=MagicMock( + side_effect=lambda **kwargs: SimpleNamespace( + name=kwargs["name"], **kwargs + ) + ), + Runner=SimpleNamespace(run=run), + handoff=MagicMock(side_effect=lambda target: target), + FunctionTool=MagicMock(), + RunHooks=_FakeRunHooks, + ) + + async def _invoke(self, agents: Any, **opts: Any) -> Any: + 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") as get_client, + patch.object(native_graph_mod, "to_ld_context", lambda _c, ctx: ctx), + ): + try: + await to_litellm_agents( + _definition_awaitable(), + opts={ + "model_factory": lambda *_: object(), + "context": {"key": "u"}, + **opts, + }, + ).invoke("hello") + except Exception as exc: + get_client.return_value.raised = exc + return get_client.return_value + + @staticmethod + def _events(client: Any) -> list[tuple[str, str]]: + return [ + (c.args[0], c.args[2]["configKey"]) for c in client.track.call_args_list + ] + + async def test_handoff_target_enters_the_path_and_totals_use_the_graph_key( + self, + ) -> None: + async def run(agent: Any, _input: Any, *, hooks: Any, run_config: Any) -> Any: + child = SimpleNamespace(name="child") + await hooks.on_agent_start(None, agent) + await hooks.on_handoff(None, agent, child) + await hooks.on_agent_start(None, child) + await hooks.on_agent_end(None, child, "done") + return SimpleNamespace( + final_output="done", + context_wrapper=SimpleNamespace( + usage=SimpleNamespace( + input_tokens=2, output_tokens=1, total_tokens=3 + ) + ), + ) + + client = await self._invoke(self._agents(run)) + assert self._events(client) == [ + ("$ld:ai:graph:node", "root"), + ("$ld:ai:graph:node", "child"), + ("$ld:ai:graph:handoff_success", "root"), + ("$ld:ai:generation:success", "child"), + ("$ld:ai:graph:duration:total", "litellm-graph"), + ("$ld:ai:graph:total_tokens", "litellm-graph"), + ("$ld:ai:graph:invocation_success", "litellm-graph"), + ] + nodes = [ + c for c in client.track.call_args_list if c.args[0] == "$ld:ai:graph:node" + ] + assert [c.args[2]["index"] for c in nodes] == [0, 1] + + async def test_setup_error_is_tracked_as_an_invocation_failure(self) -> None: + run = AsyncMock() + + def broken_factory(*_: Any) -> Any: + raise RuntimeError("no proxy") + + client = await self._invoke(self._agents(run), model_factory=broken_factory) + assert str(client.raised) == "no proxy" + assert self._events(client) == [ + ("$ld:ai:graph:invocation_failure", "litellm-graph") + ] + run.assert_not_awaited() diff --git a/packages/litellm-agents/tests/test_handler.py b/packages/litellm-agents/tests/test_handler.py index a330005c..b73f8963 100644 --- a/packages/litellm-agents/tests/test_handler.py +++ b/packages/litellm-agents/tests/test_handler.py @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from openai.types.shared import Reasoning import launchdarkly_ai_litellm_agents.handler as handler_mod from launchdarkly_ai_litellm_agents import ( @@ -291,6 +292,45 @@ async def test_runner_receives_agent_prompt_and_max_turns(self) -> None: "usage": {"input_tokens": 11, "output_tokens": 4}, } + async def test_runs_with_agents_sdk_tracing_disabled(self) -> None: + streamed = FakeStreamedResult([]) + agents = _agents_module(run_streamed=MagicMock(return_value=streamed)) + with _patch_agents(agents): + handler = create_litellm_agents_handler(model_factory=lambda *_: object()) + await handler(CONFIG, "q") + [event async for event in await handler.stream(CONFIG, "q")] + assert agents.Runner.run.call_args.kwargs["run_config"].tracing_disabled is True + assert ( + agents.Runner.run_streamed.call_args.kwargs["run_config"].tracing_disabled + is True + ) + + def test_model_settings_keep_the_shared_allowlist_and_type_reasoning( + self, + ) -> None: + settings = handler_mod._model_settings( + { + "temperature": 0.1, + "verbosity": "low", + "reasoning": {"effort": "high", "summary": "auto", "smuggled": 1}, + "metadata": {"team": "a"}, + "include_usage": True, + "top_logprobs": 2, + } + ) + assert isinstance(settings.reasoning, Reasoning) + assert settings.reasoning.effort == "high" + assert settings.reasoning.summary == "auto" + assert settings.verbosity == "low" + assert settings.temperature == 0.1 + assert settings.metadata is None + assert settings.include_usage is None + assert settings.top_logprobs is None + + @pytest.mark.parametrize("reasoning", ["high", {"smuggled": 1}, None]) + def test_unusable_reasoning_is_dropped(self, reasoning: Any) -> None: + assert handler_mod._model_settings({"reasoning": reasoning}).reasoning is None + async def test_history_uses_agents_sdk_structured_input(self) -> None: run = AsyncMock( return_value=SimpleNamespace( @@ -522,7 +562,7 @@ 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): + with patch.object(handler_mod, "_config", config): result = litellm_agents( "agent-key", "hello", diff --git a/packages/litellm-agents/tests/test_sdk_usage.py b/packages/litellm-agents/tests/test_sdk_usage.py new file mode 100644 index 00000000..776f3cba --- /dev/null +++ b/packages/litellm-agents/tests/test_sdk_usage.py @@ -0,0 +1,74 @@ +"""Tests for TESTING.md §3.27 — litellm-agents.""" + +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +import launchdarkly_ai_server.lifecycle as lifecycle_module +from launchdarkly_ai_litellm_agents import ( + create_litellm_agents_handler, + litellm_agents, + litellm_graph, + to_litellm_agents, +) +from launchdarkly_ai_litellm_agents._version import PACKAGE_NAME +from launchdarkly_ai_litellm_agents._version import __version__ as PACKAGE_VERSION +from launchdarkly_ai_server import SDK_INFO_CONTEXT, __version__, init_client, shutdown + +CONTEXT = {"kind": "user", "key": "user-1"} + + +@pytest.fixture +async def client() -> Any: + await shutdown() + installed = MagicMock() + installed.variation = AsyncMock(return_value=None) + installed.flush = AsyncMock() + installed.close = AsyncMock() + with patch.object(lifecycle_module, "_setup_telemetry", return_value=None): + await init_client(client=installed) + installed.track.reset_mock() + yield installed + await shutdown() + + +@pytest.mark.parametrize( + ("helper", "call"), + [ + ("litellm-agents.litellmAgents", lambda: litellm_agents("k", "q", CONTEXT)), + ("litellm-agents.createLiteLLMAgentsHandler", create_litellm_agents_handler), + ("litellm-agents.litellmGraph", lambda: litellm_graph("k")), + ( + "litellm-agents.litellmGraph", + lambda: litellm_graph("k").invoke("q", CONTEXT), + ), + ("litellm-agents.toLiteLLMAgents", lambda: to_litellm_agents(None)), + ], +) +async def test_helper_sends_one_usage_event( + client: MagicMock, helper: str, call: Any +) -> None: + try: + value = call() + if hasattr(value, "__await__"): + await value + elif hasattr(value, "invoke"): + await value.invoke("q") + except Exception: + pass + calls = [ + entry + for entry in client.track.call_args_list + if entry.args[0] == "$ld:ai:sdk:usage" + ] + assert [entry.args[2].get("helper") for entry in calls] == [helper] + assert calls[0].args[1] == SDK_INFO_CONTEXT + assert calls[0].args[2] == { + "aiSdkName": "launchdarkly-ai-server", + "aiSdkVersion": __version__, + "aiSdkLanguage": "python", + "helper": helper, + "helperPackageName": PACKAGE_NAME, + "helperPackageVersion": PACKAGE_VERSION, + } diff --git a/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/__init__.py b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/__init__.py index 56b796d7..1d790b6d 100644 --- a/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/__init__.py +++ b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/__init__.py @@ -1,11 +1,10 @@ """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 ._version import PACKAGE_NAME, __version__ 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__) +register_ai_sdk_package(PACKAGE_NAME, __version__) diff --git a/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/_version.py b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/_version.py new file mode 100644 index 00000000..908d6215 --- /dev/null +++ b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/_version.py @@ -0,0 +1,4 @@ +"""This package's identity, as sent with ``$ld:ai:sdk:info`` and ``$ld:ai:sdk:usage``.""" + +PACKAGE_NAME = "launchdarkly-ai-litellm-messages" +__version__ = "0.1.0" # x-release-please-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 index 55afb00a..fdfc66e7 100644 --- a/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py +++ b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py @@ -14,12 +14,13 @@ LDContext, ProviderHandler, SpanUsage, - config, + _config, + _create_handler, content_to_text, - create_handler, end_span_once, image_block_to_url, parse_template, + report_usage, set_input_content_attributes, set_ld_span_attributes, set_model_identity_attributes, @@ -29,6 +30,9 @@ text_message, to_semconv_finish_reason, ) +from launchdarkly_ai_server.lifecycle import effective_capture_content + +from ._version import PACKAGE_NAME, __version__ Completion = Callable[..., Awaitable[Any]] @@ -361,8 +365,23 @@ def _output_value(content: Any, output_format: Any) -> Any: def create_litellm_messages_handler( *, completion: Completion | None = None, - capture_content: bool = False, + capture_content: bool | None = None, +) -> ProviderHandler: + report_usage( + "litellm-messages.createLiteLLMMessagesHandler", PACKAGE_NAME, __version__ + ) + return _create_litellm_messages_handler( + completion=completion, capture_content=capture_content + ) + + +def _create_litellm_messages_handler( + *, + completion: Completion | None = None, + capture_content: bool | None = None, ) -> ProviderHandler: + """Non-reporting :func:`create_litellm_messages_handler`, used by this package's wrappers.""" + content_override = capture_content if completion is None: from litellm import acompletion @@ -375,6 +394,7 @@ async def call( variables: dict[str, Any] | None = None, history: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: + capture_content = effective_capture_content(content_override) handlers = tool_handlers or {} values = variables or {} messages = _build_messages(config_value, user_input, values, history) @@ -486,14 +506,14 @@ def stream( tool_handlers or {}, variables or {}, history, - capture_content=capture_content, + capture_content=effective_capture_content(content_override), ) - return create_handler( + return _create_handler( ("*", "messages"), call, stream, - capture_content=capture_content, + capture_content=content_override, ) @@ -658,9 +678,10 @@ def litellm_messages( **kwargs: Any, ) -> Any: variables = kwargs.pop("variables", None) - capture_content = bool(kwargs.pop("capture_content", False)) - return config( + capture_content = kwargs.pop("capture_content", None) + report_usage("litellm-messages.litellmMessages", PACKAGE_NAME, __version__) + return _config( key=config_key, - handler=create_litellm_messages_handler(capture_content=capture_content), + 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 index 5676b495..9109e0f3 100644 --- a/packages/litellm-messages/tests/test_handler.py +++ b/packages/litellm-messages/tests/test_handler.py @@ -626,7 +626,7 @@ 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): + with patch.object(handler_mod, "_config", config): result = litellm_messages( "config-key", "hello", diff --git a/packages/litellm-messages/tests/test_sdk_usage.py b/packages/litellm-messages/tests/test_sdk_usage.py new file mode 100644 index 00000000..63ab4b88 --- /dev/null +++ b/packages/litellm-messages/tests/test_sdk_usage.py @@ -0,0 +1,72 @@ +"""Tests for TESTING.md §3.27 — litellm-messages.""" + +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +import launchdarkly_ai_server.lifecycle as lifecycle_module +from launchdarkly_ai_litellm_messages import ( + create_litellm_messages_handler, + litellm_messages, +) +from launchdarkly_ai_litellm_messages._version import PACKAGE_NAME +from launchdarkly_ai_litellm_messages._version import __version__ as PACKAGE_VERSION +from launchdarkly_ai_server import SDK_INFO_CONTEXT, __version__, init_client, shutdown + +CONTEXT = {"kind": "user", "key": "user-1"} + + +@pytest.fixture +async def client() -> Any: + await shutdown() + installed = MagicMock() + installed.variation = AsyncMock(return_value=None) + installed.flush = AsyncMock() + installed.close = AsyncMock() + with patch.object(lifecycle_module, "_setup_telemetry", return_value=None): + await init_client(client=installed) + installed.track.reset_mock() + yield installed + await shutdown() + + +@pytest.mark.parametrize( + ("helper", "call"), + [ + ( + "litellm-messages.litellmMessages", + lambda: litellm_messages("k", "q", CONTEXT), + ), + ( + "litellm-messages.createLiteLLMMessagesHandler", + create_litellm_messages_handler, + ), + ], +) +async def test_helper_sends_one_usage_event( + client: MagicMock, helper: str, call: Any +) -> None: + try: + value = call() + if hasattr(value, "__await__"): + await value + elif hasattr(value, "invoke"): + await value.invoke("q") + except Exception: + pass + calls = [ + entry + for entry in client.track.call_args_list + if entry.args[0] == "$ld:ai:sdk:usage" + ] + assert [entry.args[2].get("helper") for entry in calls] == [helper] + assert calls[0].args[1] == SDK_INFO_CONTEXT + assert calls[0].args[2] == { + "aiSdkName": "launchdarkly-ai-server", + "aiSdkVersion": __version__, + "aiSdkLanguage": "python", + "helper": helper, + "helperPackageName": PACKAGE_NAME, + "helperPackageVersion": PACKAGE_VERSION, + } From d77ada4db5c4cd9dfc827d55e7201dd8098f9031 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Wed, 21 Oct 2026 10:59:40 -0800 Subject: [PATCH 8/9] fix(AIC-3410): set LaunchDarkly identity attributes on the LiteLLM graph span Co-authored-by: Cursor --- .../src/launchdarkly_ai_litellm_agents/native_graph.py | 9 +++++++++ packages/litellm-agents/tests/test_graph.py | 3 +++ 2 files changed, 12 insertions(+) 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 30c1b5b1..1066470a 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 @@ -20,6 +20,7 @@ make_track_data, parse_template, report_usage, + set_ld_span_attributes, to_ld_context, ) from launchdarkly_ai_server.utils import make_graph_track_data @@ -153,6 +154,14 @@ async def build(node: GraphNode) -> None: span = None try: + if span: + set_ld_span_attributes( + span, + { + "__ld": make_graph_track_data(graph.key, run_id), + "ldContext": raw_ld_context, + }, + ) await build(graph.root) runner_input: str | list[dict[str, Any]] = input_text if history: diff --git a/packages/litellm-agents/tests/test_graph.py b/packages/litellm-agents/tests/test_graph.py index 77ac400c..59e63794 100644 --- a/packages/litellm-agents/tests/test_graph.py +++ b/packages/litellm-agents/tests/test_graph.py @@ -509,6 +509,9 @@ async def test_cancellation_closes_graph_span(self) -> None: ).invoke("hello") span.end.assert_called_once_with() span.set_attribute.assert_any_call("launchdarkly.run.cancelled", True) + span.set_attribute.assert_any_call("launchdarkly.config.key", "litellm-graph") + attrs = [c.args[0] for c in span.set_attribute.call_args_list] + assert "launchdarkly.run.id" in attrs async def test_disabled_graph_fails_before_runner_or_model_creation(self) -> None: model_factory = MagicMock() From 6aaeffbfac9de4df1d4960b8787979bfbeb48924 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Wed, 21 Oct 2026 13:24:00 -0800 Subject: [PATCH 9/9] fix(AIC-3410): read either parameter spelling and let messages set the LiteLLM endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _model_config only matched snake_case keys, so a caller-supplied topP or maxTurns silently vanished even though spec §2.x.1 requires either spelling to be read, as JS already does. Normalize camelCase aliases to their snake_case name before filtering, with snake_case winning when both are set. create_litellm_messages_handler had no way to point at a specific endpoint or key, unlike the agents handler and the JS messages handler. It now takes the same base_url/api_key options, forwarded to acompletion as api_base/ api_key on both the blocking and streaming paths. model.parameters still cannot set them. Co-authored-by: Cursor --- .../launchdarkly_ai_litellm_agents/handler.py | 29 +++++++++--- packages/litellm-agents/tests/test_handler.py | 16 +++++++ packages/litellm-messages/README.md | 5 +++ .../handler.py | 44 +++++++++++++++++-- .../litellm-messages/tests/test_handler.py | 18 ++++++++ 5 files changed, 103 insertions(+), 9 deletions(-) diff --git a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py index 10ddcde4..0505082f 100644 --- a/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py +++ b/packages/litellm-agents/src/launchdarkly_ai_litellm_agents/handler.py @@ -97,15 +97,34 @@ def validate_json(self, json_str: str) -> Any: return json.loads(json_str) +# AI Configs are authored in snake_case, but a caller may also pass camelCase (spec §2.x.1: +# "either spelling is read"). snake_case wins when a bag sets both. +_CAMEL_TO_SNAKE_FIELDS = { + "topP": "top_p", + "maxTurns": "max_turns", + "frequencyPenalty": "frequency_penalty", + "presencePenalty": "presence_penalty", + "toolChoice": "tool_choice", + "parallelToolCalls": "parallel_tool_calls", + "maxTokens": "max_tokens", +} + + +def _normalized_parameters(raw: dict[str, Any]) -> dict[str, Any]: + result = dict(raw) + for camel, snake in _CAMEL_TO_SNAKE_FIELDS.items(): + if camel in result: + value = result.pop(camel) + result.setdefault(snake, value) + return result + + def _model_config(config_value: AiConfigRep) -> tuple[str, dict[str, Any]]: model = config_value.get("model") or {} raw = model.get("parameters") allowed = _MODEL_SETTING_FIELDS | {"max_turns"} - parameters = ( - {key: value for key, value in raw.items() if key in allowed} - if isinstance(raw, dict) - else {} - ) + normalized = _normalized_parameters(raw) if isinstance(raw, dict) else {} + parameters = {key: value for key, value in normalized.items() if key in allowed} return str(model.get("name") or ""), parameters diff --git a/packages/litellm-agents/tests/test_handler.py b/packages/litellm-agents/tests/test_handler.py index b73f8963..34a3db1f 100644 --- a/packages/litellm-agents/tests/test_handler.py +++ b/packages/litellm-agents/tests/test_handler.py @@ -331,6 +331,22 @@ def test_model_settings_keep_the_shared_allowlist_and_type_reasoning( def test_unusable_reasoning_is_dropped(self, reasoning: Any) -> None: assert handler_mod._model_settings({"reasoning": reasoning}).reasoning is None + def test_model_config_reads_either_spelling_and_snake_case_wins(self) -> None: + config = { + **CONFIG, + "model": { + **CONFIG["model"], + "parameters": { + "topP": 0.4, + "maxTurns": 5, + "top_p": 0.9, + }, + }, + } + _, parameters = handler_mod._model_config(config) + assert parameters["top_p"] == 0.9 + assert parameters["max_turns"] == 5 + async def test_history_uses_agents_sdk_structured_input(self) -> None: run = AsyncMock( return_value=SimpleNamespace( diff --git a/packages/litellm-messages/README.md b/packages/litellm-messages/README.md index 5b492b1c..1616a092 100644 --- a/packages/litellm-messages/README.md +++ b/packages/litellm-messages/README.md @@ -16,6 +16,11 @@ 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. +To route every call through a specific endpoint or key instead (for example a +LiteLLM proxy), pass `base_url` / `api_key` to `create_litellm_messages_handler`. +These come from your code, never from the AI Config's `model.parameters` — an +AI Config variation cannot redirect the request. + ## Usage ```python 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 fdfc66e7..bd9d6321 100644 --- a/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py +++ b/packages/litellm-messages/src/launchdarkly_ai_litellm_messages/handler.py @@ -183,6 +183,8 @@ def _request( tools: list[dict[str, Any]], *, stream: bool, + base_url: str | None = None, + api_key: str | None = None, ) -> dict[str, Any]: model = config_value.get("model") or {} raw_parameters = model.get("parameters") @@ -202,6 +204,11 @@ def _request( "stream": stream, } ) + # Connection settings come from the handler, never from model.parameters (see above). + if base_url is not None: + parameters["api_base"] = base_url + if api_key is not None: + parameters["api_key"] = api_key if tools: parameters["tools"] = tools if stream: @@ -366,12 +373,17 @@ def create_litellm_messages_handler( *, completion: Completion | None = None, capture_content: bool | None = None, + base_url: str | None = None, + api_key: str | None = None, ) -> ProviderHandler: report_usage( "litellm-messages.createLiteLLMMessagesHandler", PACKAGE_NAME, __version__ ) return _create_litellm_messages_handler( - completion=completion, capture_content=capture_content + completion=completion, + capture_content=capture_content, + base_url=base_url, + api_key=api_key, ) @@ -379,6 +391,8 @@ def _create_litellm_messages_handler( *, completion: Completion | None = None, capture_content: bool | None = None, + base_url: str | None = None, + api_key: str | None = None, ) -> ProviderHandler: """Non-reporting :func:`create_litellm_messages_handler`, used by this package's wrappers.""" content_override = capture_content @@ -423,7 +437,14 @@ async def call( messages=_span_messages(messages), ) response = await completion( - **_request(config_value, messages, tools, stream=False) + **_request( + config_value, + messages, + tools, + stream=False, + base_url=base_url, + api_key=api_key, + ) ) turn_usage = _usage(response) usage_reported = usage_reported or _usage_reported(response) @@ -507,6 +528,8 @@ def stream( variables or {}, history, capture_content=effective_capture_content(content_override), + base_url=base_url, + api_key=api_key, ) return _create_handler( @@ -526,6 +549,8 @@ async def _stream( history: list[dict[str, Any]] | None, *, capture_content: bool, + base_url: str | None = None, + api_key: str | None = None, ) -> AsyncGenerator[dict[str, Any], None]: messages = _build_messages(config_value, user_input, variables, history) tools = _build_tools(config_value, handlers) @@ -558,7 +583,14 @@ async def _stream( messages=_span_messages(messages), ) provider_stream = await completion( - **_request(config_value, messages, tools, stream=True) + **_request( + config_value, + messages, + tools, + stream=True, + base_url=base_url, + api_key=api_key, + ) ) fragments: dict[int, dict[str, Any]] = {} turn_text = "" @@ -679,9 +711,13 @@ def litellm_messages( ) -> Any: variables = kwargs.pop("variables", None) capture_content = kwargs.pop("capture_content", None) + base_url = kwargs.pop("base_url", None) + api_key = kwargs.pop("api_key", None) report_usage("litellm-messages.litellmMessages", PACKAGE_NAME, __version__) return _config( key=config_key, - handler=_create_litellm_messages_handler(capture_content=capture_content), + handler=_create_litellm_messages_handler( + capture_content=capture_content, base_url=base_url, api_key=api_key + ), **kwargs, ).invoke(user_input, context, variables) diff --git a/packages/litellm-messages/tests/test_handler.py b/packages/litellm-messages/tests/test_handler.py index 9109e0f3..cb8a9e76 100644 --- a/packages/litellm-messages/tests/test_handler.py +++ b/packages/litellm-messages/tests/test_handler.py @@ -206,6 +206,24 @@ async def test_evaluated_model_and_owned_parameter_precedence(self) -> None: assert "api_key" not in request assert "extra_headers" not in request + async def test_handler_options_set_the_connection_and_win_over_config(self) -> None: + completion = AsyncMock(return_value=_response()) + config = { + **CONFIG, + "model": { + **CONFIG["model"], + "parameters": {"api_base": "http://127.0.0.1:9", "api_key": "stolen"}, + }, + } + await create_litellm_messages_handler( + completion=completion, + base_url="https://proxy.example", + api_key="handler-key", + )(config, "question") + request = completion.call_args.kwargs + assert request["api_base"] == "https://proxy.example" + assert request["api_key"] == "handler-key" + class TestPromptMapping: async def test_messages_take_precedence_over_instructions(self) -> None: