diff --git a/pyproject.toml b/pyproject.toml index c228c9ff..dab818b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "greenlet>=3.2.1", "alembic>=1.13", "python-json-logger>=2.0.0", - "cashu", + "cashu>=0.20", "secp256k1", "marshmallow>=3.13,<4.0", "websockets>=12.0", @@ -21,6 +21,7 @@ dependencies = [ "mdurl==0.1.2", "pillow>=10", "openai>=1.98.0", + "litellm>=1.55.0", ] [dependency-groups] diff --git a/routstr/core/admin.py b/routstr/core/admin.py index 202203ba..923e597f 100644 --- a/routstr/core/admin.py +++ b/routstr/core/admin.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from pathlib import Path from fastapi import APIRouter, Depends, HTTPException, Query, Request -from pydantic import BaseModel +from pydantic import BaseModel, RootModel from sqlmodel import select from ..payment.models import _row_to_model, list_models @@ -142,8 +142,8 @@ async def get_settings(request: Request) -> dict: return data -class SettingsUpdate(BaseModel): - __root__: dict[str, object] +class SettingsUpdate(RootModel[dict[str, object]]): + pass class PasswordUpdate(BaseModel): @@ -154,7 +154,7 @@ class PasswordUpdate(BaseModel): @admin_router.patch("/api/settings", dependencies=[Depends(require_admin_api)]) async def update_settings(request: Request, update: SettingsUpdate) -> dict: # Remove sensitive fields from general settings update - settings_data = update.__root__.copy() + settings_data = update.root.copy() sensitive_fields = ["admin_password", "upstream_api_key", "nsec"] for field in sensitive_fields: if field in settings_data: diff --git a/routstr/core/main.py b/routstr/core/main.py index e22173df..60913447 100644 --- a/routstr/core/main.py +++ b/routstr/core/main.py @@ -23,6 +23,7 @@ from ..payment.models import models_router, update_sats_pricing from ..payment.price import update_prices_periodically from ..proxy import initialize_upstreams, proxy_router, refresh_model_maps_periodically from ..upstream.auto_topup import periodic_auto_topup +from ..upstream.litellm_routing import configure_litellm from ..wallet import periodic_payout, periodic_refund_sweep, periodic_routstr_fee_payout from .admin import admin_router from .db import create_session, init_db, run_migrations @@ -56,6 +57,10 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: routstr_fee_task = None try: + # Apply litellm-wide settings (drop_params, chat-completions URL, + # debug logging) before any upstream provider dispatches a request. + configure_litellm() + # Run database migrations on startup run_migrations() diff --git a/routstr/payment/cost_calculation.py b/routstr/payment/cost_calculation.py index 44330620..292b6d5f 100644 --- a/routstr/payment/cost_calculation.py +++ b/routstr/payment/cost_calculation.py @@ -53,10 +53,17 @@ async def calculate_cost( # todo: can be sync if "usage" not in response_data or response_data["usage"] is None: logger.warning( - "No usage data in response, using base cost only", + "No usage data in response — billing at MaxCostData with zero " + "tokens. Dashboard will show this request as `(0+0)`. Most " + "common cause: upstream stream did not include a final usage " + "chunk (OpenAI-compat backends require " + "`stream_options.include_usage=true`).", extra={ "max_cost_msats": max_cost, "model": response_data.get("model", "unknown"), + "response_keys": sorted(response_data.keys()) + if isinstance(response_data, dict) + else None, }, ) return MaxCostData( @@ -139,6 +146,22 @@ async def calculate_cost( # todo: can be sync ) if usd_cost > 0: + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "Upstream reported a USD cost but no token counts — " + "billing the USD-derived cost while the dashboard will " + "show this request as `(0+0)` tokens. Check that the " + "upstream actually emits `usage.input_tokens` and " + "`usage.output_tokens` (OpenAI-compat streams require " + "`stream_options.include_usage=true`).", + extra={ + "model": response_data.get("model", "unknown"), + "usd_cost": usd_cost, + "usage_keys": sorted(usage_data.keys()) + if isinstance(usage_data, dict) + else None, + }, + ) try: sats_per_usd = 1.0 / sats_usd_price() cost_in_sats = usd_cost * sats_per_usd @@ -239,10 +262,18 @@ async def calculate_cost( # todo: can be sync if not (MSATS_PER_1K_OUTPUT_TOKENS and MSATS_PER_1K_INPUT_TOKENS): logger.warning( - "No token pricing configured, using base cost", + "No token pricing configured — billing at flat MaxCostData. " + "Token counts %s in the upstream response but cannot be " + "priced; the request will appear in dashboards with the " + "raw counts and a fixed max-cost charge.", + "are present" + if (input_tokens > 0 or output_tokens > 0) + else "are zero", extra={ "base_cost_msats": max_cost, "model": response_data.get("model", "unknown"), + "input_tokens": input_tokens, + "output_tokens": output_tokens, }, ) return MaxCostData( diff --git a/routstr/payment/models.py b/routstr/payment/models.py index 17910749..49ea863f 100644 --- a/routstr/payment/models.py +++ b/routstr/payment/models.py @@ -4,6 +4,7 @@ import random import httpx from fastapi import APIRouter, Depends +from pydantic import BaseModel as V2BaseModel from pydantic.v1 import BaseModel from sqlmodel.ext.asyncio.session import AsyncSession @@ -411,7 +412,7 @@ async def update_sats_pricing() -> None: logger.error(f"Error updating sats pricing: {e}") -class ModelTestRequest(BaseModel): +class ModelTestRequest(V2BaseModel): model_id: str endpoint_type: str request_data: dict diff --git a/routstr/upstream/anthropic.py b/routstr/upstream/anthropic.py index 3f228e9c..5e48f058 100644 --- a/routstr/upstream/anthropic.py +++ b/routstr/upstream/anthropic.py @@ -13,6 +13,8 @@ class AnthropicUpstreamProvider(BaseUpstreamProvider): provider_type = "anthropic" default_base_url = "https://api.anthropic.com/v1" platform_url = "https://console.anthropic.com/settings/keys" + supports_anthropic_messages = True + litellm_provider_prefix = "anthropic/" def __init__(self, api_key: str, provider_fee: float = 1.01): super().__init__( diff --git a/routstr/upstream/azure.py b/routstr/upstream/azure.py index e412c466..11cee17c 100644 --- a/routstr/upstream/azure.py +++ b/routstr/upstream/azure.py @@ -13,6 +13,7 @@ class AzureUpstreamProvider(BaseUpstreamProvider): provider_type = "azure" default_base_url = None platform_url = "https://portal.azure.com/" + litellm_provider_prefix = "azure/" def __init__( self, diff --git a/routstr/upstream/base.py b/routstr/upstream/base.py index 6176c509..0640efe0 100644 --- a/routstr/upstream/base.py +++ b/routstr/upstream/base.py @@ -6,13 +6,13 @@ import json import re import traceback import uuid -from collections.abc import AsyncGenerator -from typing import Mapping +from collections.abc import AsyncGenerator, AsyncIterator +from typing import Any, Mapping, cast import httpx from fastapi import BackgroundTasks, HTTPException, Request from fastapi.responses import Response, StreamingResponse -from pydantic import BaseModel +from pydantic.v1 import BaseModel from sqlmodel import select from ..auth import adjust_payment_for_tokens @@ -41,6 +41,8 @@ from ..payment.models import ( ) from ..payment.price import sats_usd_price from ..wallet import recieve_token, send_token +from . import messages_dispatch +from .litellm_routing import detect_litellm_prefix logger = get_logger(__name__) @@ -63,6 +65,12 @@ class BaseUpstreamProvider: default_base_url: str | None = None platform_url: str | None = None + supports_anthropic_messages: bool = False + # When None, the prefix is detected from `base_url` at dispatch time + # (see `get_litellm_provider_prefix`). Subclasses set this to lock the + # provider regardless of URL. + litellm_provider_prefix: str | None = None + base_url: str api_key: str provider_fee: float = 1.05 @@ -83,6 +91,19 @@ class BaseUpstreamProvider: self._models_cache = [] self._models_by_id = {} + def get_litellm_provider_prefix(self) -> str: + """Resolve the litellm provider prefix for this provider instance. + + 1. If the subclass pinned `litellm_provider_prefix`, use it. + 2. Otherwise infer from `base_url` (e.g. ``api.fireworks.ai`` → + ``fireworks_ai/``) so custom/generic rows reach the correct + litellm backend instead of falling back to ``openai/``. + 3. Default ``openai/`` for unknown OpenAI-compatible servers. + """ + if self.__class__.litellm_provider_prefix: + return self.__class__.litellm_provider_prefix + return detect_litellm_prefix(self.base_url) + @classmethod def from_db_row( cls, provider_row: "UpstreamProviderRow" @@ -330,7 +351,10 @@ class BaseUpstreamProvider: ) -> bytes | None: """Transform request body for provider-specific requirements. - Automatically transforms model names in the request body. + Automatically transforms model names and, for streaming chat + completions, opts the upstream into emitting per-chunk ``usage`` + so cost tracking can read real token counts instead of falling + back to ``MaxCostData``. Args: body: Original request body bytes @@ -343,9 +367,25 @@ class BaseUpstreamProvider: try: data = json.loads(body) - if isinstance(data, dict) and "model" in data: - original_model = model_obj.id - transformed_model = self.transform_model_name(original_model) + except Exception as e: + logger.debug( + "Could not parse request body for transformation", + extra={ + "error": str(e), + "provider": self.provider_type or self.base_url, + }, + ) + return body + + if not isinstance(data, dict): + return body + + changed = False + + if "model" in data: + original_model = model_obj.id + transformed_model = self.transform_model_name(original_model) + if data["model"] != transformed_model: data["model"] = transformed_model logger.debug( "Transformed model name in request", @@ -355,16 +395,28 @@ class BaseUpstreamProvider: "provider": self.provider_type or self.base_url, }, ) - return json.dumps(data).encode() - except Exception as e: - logger.debug( - "Could not transform request body", - extra={ - "error": str(e), - "provider": self.provider_type or self.base_url, - }, - ) + changed = True + # OpenAI-compatible streaming responses omit ``usage`` unless the + # request sets ``stream_options.include_usage = true``. Without it + # we can't reconcile token counts at end of stream and the + # request gets billed at max-cost with zero tokens. Discriminate + # chat-completions-shaped requests by the ``messages`` field so we + # don't poke unrelated endpoints. + if ( + data.get("stream") is True + and "messages" in data + and isinstance(data.get("messages"), list) + ): + existing = data.get("stream_options") + merged = dict(existing) if isinstance(existing, dict) else {} + if merged.get("include_usage") is not True: + merged["include_usage"] = True + data["stream_options"] = merged + changed = True + + if changed: + return json.dumps(data).encode() return body def _extract_upstream_error_message( @@ -1470,6 +1522,397 @@ class BaseUpstreamProvider: except Exception: raise + # ------------------------------------------------------------------ + # Litellm /v1/messages dispatch (thin wrappers) + # + # The actual translation logic lives in ``messages_dispatch``. These + # method shims exist so subclasses and tests can keep the original + # provider-bound API. + # ------------------------------------------------------------------ + + _coerce_litellm_payload = staticmethod(messages_dispatch.coerce_litellm_payload) + _parse_sse_blocks = staticmethod(messages_dispatch.parse_sse_blocks) + _events_from_chunk = staticmethod(messages_dispatch.events_from_chunk) + + async def _aggregate_anthropic_events_to_message( + self, iterator: AsyncIterator[Any] + ) -> dict: + return await messages_dispatch.aggregate_anthropic_events_to_message( + iterator + ) + + async def _dispatch_anthropic_messages( + self, + request_body: bytes | None, + model_obj: Model, + *, + log_extra: dict[str, Any] | None = None, + ) -> tuple[bool, Any, str | None]: + return await messages_dispatch.dispatch_anthropic_messages( + request_body=request_body, + model_obj=model_obj, + base_url=self.base_url, + api_key=self.api_key, + provider_prefix=self.get_litellm_provider_prefix(), + transform_model_name=self.transform_model_name, + log_extra=log_extra, + ) + + async def _forward_messages_via_litellm( + self, + request_body: bytes | None, + key: ApiKey, + session: AsyncSession, + max_cost_for_model: int, + model_obj: Model, + ) -> Response | StreamingResponse: + """Translate /v1/messages to upstream chat/completions via litellm. + + Used when the upstream provider does not natively serve Anthropic + Messages (i.e. supports_anthropic_messages is False). Cost + tracking and metadata injection mirror the native messages path. + """ + stream, result, requested_model = await self._dispatch_anthropic_messages( + request_body, + model_obj, + log_extra={"key_hash": key.hashed_key[:8] + "..."}, + ) + + if stream: + return self._stream_litellm_messages( + cast(AsyncIterator[Any], result), + key, + max_cost_for_model, + requested_model, + ) + + response_json = messages_dispatch.coerce_litellm_payload(result) + if requested_model and "model" in response_json: + response_json["model"] = requested_model + + cost_data = await adjust_payment_for_tokens( + key, response_json, session, max_cost_for_model + ) + self.inject_cost_metadata(response_json, cost_data, key) + + return Response( + content=json.dumps(response_json).encode(), + status_code=200, + media_type="application/json", + ) + + async def _forward_x_cashu_messages_via_litellm( + self, + request_body: bytes, + amount: int, + unit: str, + max_cost_for_model: int, + model_obj: Model, + mint: str | None = None, + payment_token_hash: str | None = None, + request_id: str | None = None, + ) -> Response | StreamingResponse: + """Dispatch /v1/messages via litellm for x-cashu payments. + + Computes cost from upstream usage, refunds the unspent balance via + an X-Cashu response header, and returns the Anthropic-shaped body. + """ + stream, result, requested_model = await self._dispatch_anthropic_messages( + request_body, + model_obj, + log_extra={"payment_unit": unit, "payment_amount": amount}, + ) + + if stream: + return await self._stream_x_cashu_litellm_messages( + cast(AsyncIterator[Any], result), + amount, + unit, + max_cost_for_model, + requested_model, + mint, + payment_token_hash, + request_id, + ) + + response_json = messages_dispatch.coerce_litellm_payload(result) + if requested_model and "model" in response_json: + response_json["model"] = requested_model + + cost_data = await self.get_x_cashu_cost(response_json, max_cost_for_model) + + if cost_data and "usage" in response_json and isinstance( + response_json["usage"], dict + ): + response_json["usage"]["cost_sats"] = cost_data.total_msats // 1000 + + response_headers: dict[str, str] = {} + if cost_data: + refund_amount = messages_dispatch.compute_refund( + amount, unit, cost_data.total_msats + ) + if refund_amount > 0: + refund_token = await self.send_refund( + refund_amount, + unit, + mint, + payment_token_hash, + request_id=request_id, + ) + response_headers["X-Cashu"] = refund_token + logger.info( + "Refund processed for non-streaming /v1/messages via litellm", + extra={ + "refund_amount": refund_amount, + "unit": unit, + "model": response_json.get("model", "unknown"), + }, + ) + + return Response( + content=json.dumps(response_json).encode(), + status_code=200, + headers=response_headers, + media_type="application/json", + ) + + _compute_refund = staticmethod(messages_dispatch.compute_refund) + + def _stream_litellm_messages( + self, + iterator: AsyncIterator[Any], + key: ApiKey, + max_cost_for_model: int, + requested_model: str | None, + ) -> StreamingResponse: + """Re-emit a litellm Anthropic-event iterator as live SSE bytes + with cost reconciliation appended at end of stream.""" + + async def stream_with_cost() -> AsyncGenerator[bytes, None]: + usage_finalized = False + last_model_seen: str | None = None + input_tokens = 0 + output_tokens = 0 + + async def finalize_without_usage() -> bytes | None: + nonlocal usage_finalized + if usage_finalized: + return None + logger.warning( + "Finalizing /v1/messages stream with no usage data — " + "client will be billed at max-cost with zero tokens. " + "Likely cause: upstream omitted `usage` from the SSE " + "stream (check that the request includes " + "`stream_options.include_usage=true` and that the " + "upstream actually emits a final usage chunk).", + extra={ + "key_hash": key.hashed_key[:8] + "...", + "model": last_model_seen or "unknown", + "provider": self.provider_type or self.base_url, + "max_cost_msats": max_cost_for_model, + }, + ) + async with create_session() as new_session: + fresh_key = await new_session.get( + key.__class__, key.hashed_key + ) + if not fresh_key: + usage_finalized = True + return None + try: + fallback: dict = { + "model": last_model_seen or "unknown", + "usage": None, + } + cost_data = await adjust_payment_for_tokens( + fresh_key, + fallback, + new_session, + max_cost_for_model, + ) + usage_finalized = True + return ( + f"event: cost\ndata: " + f"{json.dumps({'cost': cost_data})}\n\n" + ).encode() + except Exception: + usage_finalized = True + return None + + try: + async for annotated in messages_dispatch.stream_annotated_events( + iterator, requested_model + ): + if annotated.model: + last_model_seen = annotated.model + # Anthropic SSE reports usage cumulatively across + # message_start + message_delta — take the max snapshot + # rather than summing, otherwise input tokens + # double-count. + input_tokens = max(input_tokens, annotated.input_tokens) + output_tokens = max(output_tokens, annotated.output_tokens) + yield annotated.sse_bytes + + if input_tokens > 0 or output_tokens > 0: + async with create_session() as new_session: + fresh_key = await new_session.get( + key.__class__, key.hashed_key + ) + if fresh_key: + try: + combined_data: dict = { + "model": last_model_seen or "unknown", + "usage": { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + }, + } + cost_data = await adjust_payment_for_tokens( + fresh_key, + combined_data, + new_session, + max_cost_for_model, + ) + self.inject_cost_metadata( + combined_data, cost_data, fresh_key + ) + usage_finalized = True + yield ( + f"event: cost\ndata: " + f"{json.dumps({'cost': cost_data})}\n\n" + ).encode() + except Exception: + pass + + if not usage_finalized: + cost_event = await finalize_without_usage() + if cost_event is not None: + yield cost_event + + except Exception: + if not usage_finalized: + await finalize_without_usage() + raise + + return StreamingResponse( + stream_with_cost(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}, + ) + + async def _stream_x_cashu_litellm_messages( + self, + iterator: AsyncIterator[Any], + amount: int, + unit: str, + max_cost_for_model: int, + requested_model: str | None, + mint: str | None, + payment_token_hash: str | None, + request_id: str | None, + ) -> StreamingResponse: + """Buffer a litellm stream end-to-end, compute cost, then replay. + + Note this is **not** true streaming — the full event sequence is + accumulated into memory before a single byte is sent to the + client. The constraint is the ``X-Cashu`` refund token, which must + be set as a response *header* and therefore has to be known before + the response begins. The bearer-key path + (:meth:`_stream_litellm_messages`) avoids this by emitting cost as + a trailing ``event: cost`` SSE message; switching x-cashu to the + same trailing-event contract would let this path stream live, at + the cost of a wire-format change for clients that read ``X-Cashu`` + from headers today. + """ + buffered: list[bytes] = [] + last_model_seen: str | None = None + input_tokens = 0 + output_tokens = 0 + + async for annotated in messages_dispatch.stream_annotated_events( + iterator, requested_model + ): + if annotated.model: + last_model_seen = annotated.model + # See _stream_litellm_messages for why this is max() not +=. + input_tokens = max(input_tokens, annotated.input_tokens) + output_tokens = max(output_tokens, annotated.output_tokens) + buffered.append(annotated.sse_bytes) + + response_headers: dict[str, str] = { + "Cache-Control": "no-cache", + "Connection": "keep-alive", + } + + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "x-cashu /v1/messages stream finished with no usage data " + "— refund cannot be computed and the client effectively " + "pays the full cashu amount. Likely cause: upstream " + "omitted `usage` from the SSE stream.", + extra={ + "model": last_model_seen or "unknown", + "provider": self.provider_type or self.base_url, + "amount": amount, + "unit": unit, + }, + ) + + if input_tokens > 0 or output_tokens > 0: + response_data: dict = { + "model": last_model_seen or "unknown", + "usage": { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + }, + } + try: + cost_data = await self.get_x_cashu_cost( + response_data, max_cost_for_model + ) + if cost_data: + refund_amount = messages_dispatch.compute_refund( + amount, unit, cost_data.total_msats + ) + if refund_amount > 0: + refund_token = await self.send_refund( + refund_amount, + unit, + mint, + payment_token_hash, + request_id=request_id, + ) + response_headers["X-Cashu"] = refund_token + logger.info( + "Refund processed for streaming /v1/messages " + "via litellm", + extra={ + "refund_amount": refund_amount, + "unit": unit, + "model": last_model_seen, + }, + ) + except Exception as exc: + logger.error( + "Error calculating cost for streaming /v1/messages", + extra={ + "error": str(exc), + "error_type": type(exc).__name__, + "amount": amount, + "unit": unit, + }, + ) + + async def replay() -> AsyncGenerator[bytes, None]: + for chunk in buffered: + yield chunk + + return StreamingResponse( + replay(), + media_type="text/event-stream", + headers=response_headers, + ) + async def forward_request( self, request: Request, @@ -1496,6 +1939,20 @@ class BaseUpstreamProvider: Response or StreamingResponse from upstream with cost tracking """ path = self.normalize_request_path(path, model_obj) + + if ( + path.endswith("messages") + and not path.endswith("count_tokens") + and not self.supports_anthropic_messages + ): + return await self._forward_messages_via_litellm( + request_body=request_body, + key=key, + session=session, + max_cost_for_model=max_cost_for_model, + model_obj=model_obj, + ) + url = self.build_request_url(path, model_obj) original_model_id = ( @@ -2679,9 +3136,26 @@ class BaseUpstreamProvider: if path.startswith("v1/"): path = path.replace("v1/", "") + request_body = await request.body() + + if ( + path.endswith("messages") + and not path.endswith("count_tokens") + and not self.supports_anthropic_messages + ): + return await self._forward_x_cashu_messages_via_litellm( + request_body=request_body, + amount=amount, + unit=unit, + max_cost_for_model=max_cost_for_model, + model_obj=model_obj, + mint=mint, + payment_token_hash=payment_token_hash, + request_id=getattr(request.state, "request_id", None), + ) + url = f"{self.base_url}/{path}" - request_body = await request.body() transformed_body = self.prepare_request_body(request_body, model_obj) logger.debug( diff --git a/routstr/upstream/clients/gemini.py b/routstr/upstream/clients/gemini.py index 893dd647..c9c750c4 100644 --- a/routstr/upstream/clients/gemini.py +++ b/routstr/upstream/clients/gemini.py @@ -26,14 +26,14 @@ class GeminiClient(BaseAPIClient): max_tokens: int | None = None, **kwargs: Any, ) -> dict[str, Any]: - from openai import NOT_GIVEN + from openai import omit response = await self.client.chat.completions.create( model=model, messages=messages, # type: ignore - temperature=temperature if temperature is not None else NOT_GIVEN, - max_tokens=max_tokens if max_tokens is not None else NOT_GIVEN, - top_p=kwargs.get("top_p", NOT_GIVEN), + temperature=temperature if temperature is not None else omit, + max_tokens=max_tokens if max_tokens is not None else omit, + top_p=kwargs.get("top_p", omit), ) return response.model_dump() @@ -45,7 +45,7 @@ class GeminiClient(BaseAPIClient): max_tokens: int | None = None, **kwargs: Any, ) -> AsyncGenerator[dict[str, Any], None]: - from openai import NOT_GIVEN + from openai import omit usage_callback = kwargs.get("usage_callback") completion_callback = kwargs.get("completion_callback") @@ -55,9 +55,9 @@ class GeminiClient(BaseAPIClient): messages=messages, # type: ignore stream=True, stream_options={"include_usage": True}, - temperature=temperature if temperature is not None else NOT_GIVEN, - max_tokens=max_tokens if max_tokens is not None else NOT_GIVEN, - top_p=kwargs.get("top_p", NOT_GIVEN), + temperature=temperature if temperature is not None else omit, + max_tokens=max_tokens if max_tokens is not None else omit, + top_p=kwargs.get("top_p", omit), ) final_usage = None diff --git a/routstr/upstream/fireworks.py b/routstr/upstream/fireworks.py index ed1ea053..e0bb1fec 100644 --- a/routstr/upstream/fireworks.py +++ b/routstr/upstream/fireworks.py @@ -12,6 +12,7 @@ class FireworksUpstreamProvider(BaseUpstreamProvider): provider_type = "fireworks" default_base_url = "https://api.fireworks.ai/inference/v1" platform_url = "https://app.fireworks.ai/settings/users/api-keys" + litellm_provider_prefix = "fireworks_ai/" def __init__(self, api_key: str, provider_fee: float = 1.01): super().__init__( diff --git a/routstr/upstream/gemini.py b/routstr/upstream/gemini.py index 8a416bfa..54bf849b 100644 --- a/routstr/upstream/gemini.py +++ b/routstr/upstream/gemini.py @@ -1,17 +1,13 @@ from __future__ import annotations -import json -from collections.abc import AsyncGenerator from typing import TYPE_CHECKING, Any -from fastapi import Request -from fastapi.responses import Response, StreamingResponse - +from . import gemini_messages from .base import BaseUpstreamProvider from .clients.gemini import GeminiClient if TYPE_CHECKING: - from ..core.db import ApiKey, AsyncSession, UpstreamProviderRow + from ..core.db import UpstreamProviderRow from ..payment.models import Model from ..core.logging import get_logger @@ -20,9 +16,19 @@ logger = get_logger(__name__) class GeminiUpstreamProvider(BaseUpstreamProvider): + """Gemini provider — proxies through Gemini's OpenAI-compat surface. + + The chat-completions, embeddings, and models paths all flow through + :meth:`BaseUpstreamProvider.forward_request`; we only override + ``get_request_base_url`` to redirect to ``{base}/openai/...`` and + ``_dispatch_anthropic_messages`` to inject thought-signatures on the + /v1/messages path (see :mod:`gemini_messages` for that rationale). + """ + provider_type = "gemini" default_base_url = "https://generativelanguage.googleapis.com/v1beta" platform_url = "https://aistudio.google.com/app/apikey" + litellm_provider_prefix = "gemini/" def __init__( self, @@ -39,7 +45,7 @@ class GeminiUpstreamProvider(BaseUpstreamProvider): @property def client(self) -> GeminiClient: - """Get or create the Gemini API client.""" + """Get or create the Gemini API client (used for the models listing).""" if self._client is None: self._client = GeminiClient(api_key=self.api_key) return self._client @@ -65,248 +71,66 @@ class GeminiUpstreamProvider(BaseUpstreamProvider): } def transform_model_name(self, model_id: str) -> str: - return model_id.removeprefix("gemini/") + """Reduce a routstr model id to the bare upstream Gemini name. - async def forward_request( + Gemini's OpenAI-compat surface expects the literal model id + (e.g. ``gemini-3.1-flash-lite-preview``) — no ``gemini/`` provider + prefix and no ``google/`` vendor sub-prefix. Take the last path + segment so we tolerate any of: + + ``gemini-2.0-flash`` + ``gemini/gemini-2.0-flash`` + ``gemini/google/gemini-3.1-flash-lite-preview`` + """ + return model_id.rsplit("/", 1)[-1] + + @property + def compat_base_url(self) -> str: + """Gemini's OpenAI-compat surface, regardless of what's stored. + + Stored ``base_url`` may be ``.../v1beta`` (the native Gemini API + root) or ``.../v1beta/openai`` (already pointed at the compat + surface). Normalize to the latter. + """ + return self.base_url.rstrip("/").removesuffix("/openai") + "/openai" + + def get_request_base_url( + self, path: str, model_obj: "Model | None" = None + ) -> str: + """Route every proxied request to the OpenAI-compat surface. + + Required because the stored ``base_url`` typically points at the + native Gemini API (``/v1beta``), but :meth:`forward_request` + forwards OpenAI-shaped paths (``/chat/completions``, + ``/embeddings``, ``/models``) which only exist under the + ``/openai`` subtree. + """ + return self.compat_base_url + + async def _dispatch_anthropic_messages( self, - request: Request, - path: str, - headers: dict, request_body: bytes | None, - key: ApiKey, - max_cost_for_model: int, - session: AsyncSession, - model_obj: Model, - ) -> Response | StreamingResponse: - # Remove provider prefix from model ID for Gemini API - if "/" in model_obj.id: - model_obj.id = model_obj.id.split("/", 1)[1] + model_obj: "Model", + *, + log_extra: dict[str, Any] | None = None, + ) -> tuple[bool, Any, str | None]: + """Dispatch /v1/messages via the gemini-specific httpx path. - if not path.startswith("chat/completions"): - return await super().forward_request( - request, - path, - headers, - request_body, - key, - max_cost_for_model, - session, - model_obj, - ) - - if not request_body: - return await super().forward_request( - request, - path, - headers, - request_body, - key, - max_cost_for_model, - session, - model_obj, - ) - - try: - openai_data = json.loads(request_body) - messages = openai_data.get("messages", []) - temperature = openai_data.get("temperature") - max_tokens = openai_data.get("max_tokens") - top_p = openai_data.get("top_p") - is_streaming = openai_data.get("stream", False) - - logger.info( - "Processing Gemini request with client abstraction", - extra={ - "model": model_obj.id, - "is_streaming": is_streaming, - "message_count": len(messages), - "key_hash": key.hashed_key[:8] + "...", - }, - ) - - if is_streaming: - final_usage_data: dict | None = None - - def usage_callback(usage_data: dict[str, Any]) -> None: - """Callback to capture usage data during streaming""" - nonlocal final_usage_data - final_usage_data = usage_data - - async def completion_callback( - model: str, usage_data: dict[str, Any] | None - ) -> None: - """Callback to handle payment when streaming completes""" - nonlocal final_usage_data - if usage_data: - final_usage_data = usage_data - - payment_data = { - "model": model, - "usage": final_usage_data, - } - - from ..auth import adjust_payment_for_tokens - from ..core.db import create_session - - async with create_session() as new_session: - fresh_key = await new_session.get(key.__class__, key.hashed_key) - if fresh_key: - try: - cost_data = await adjust_payment_for_tokens( - fresh_key, - payment_data, - new_session, - max_cost_for_model, - ) - - logger.info( - "Gemini streaming payment finalized", - extra={ - "cost_data": cost_data, - "usage_data": final_usage_data, - "key_hash": key.hashed_key[:8] + "...", - }, - ) - except Exception as cost_error: - logger.error( - "Error finalizing Gemini streaming payment", - extra={ - "error": str(cost_error), - "key_hash": key.hashed_key[:8] + "...", - }, - ) - - response_generator = self.client.generate_content_stream( - model=model_obj.id, - messages=messages, - temperature=temperature, - max_tokens=max_tokens, - top_p=top_p, - usage_callback=usage_callback, - completion_callback=completion_callback, - ) - - async def stream_with_cost() -> AsyncGenerator[bytes, None]: - payment_finalized = False - - async def finalize_payment() -> None: - nonlocal payment_finalized - if payment_finalized: - return - from ..auth import adjust_payment_for_tokens - from ..core.db import create_session - - async with create_session() as new_session: - fresh_key = await new_session.get( - key.__class__, key.hashed_key - ) - if fresh_key: - try: - await adjust_payment_for_tokens( - fresh_key, - { - "model": model_obj.id, - "usage": final_usage_data, - }, - new_session, - max_cost_for_model, - ) - payment_finalized = True - except Exception as cost_error: - logger.error( - "Error finalizing Gemini streaming payment in fallback", - extra={ - "error": str(cost_error), - "key_hash": key.hashed_key[:8] + "...", - }, - ) - - try: - async for chunk in response_generator: - sse_data = f"data: {json.dumps(chunk)}\n\n" - yield sse_data.encode() - except Exception as e: - logger.error( - "Error in Gemini streaming response", - extra={ - "error": str(e), - "error_type": type(e).__name__, - "key_hash": key.hashed_key[:8] + "...", - }, - ) - raise - finally: - if not payment_finalized: - await finalize_payment() - - return StreamingResponse( - stream_with_cost(), - media_type="text/event-stream", - headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}, - ) - - else: - openai_format_response = await self.client.generate_content( - model=model_obj.id, - messages=messages, - temperature=temperature, - max_tokens=max_tokens, - top_p=top_p, - ) - - from ..auth import adjust_payment_for_tokens - - cost_data = await adjust_payment_for_tokens( - key, openai_format_response, session, max_cost_for_model - ) - await session.refresh(key) - remaining_balance_msats = key.balance - openai_format_response["cost"] = cost_data - openai_format_response["cost"]["sats_cost"] = ( - cost_data.get("total_msats", 0) // 1000 - ) - openai_format_response["cost"]["remaining_balance_msats"] = ( - remaining_balance_msats - ) - - logger.info( - "Gemini non-streaming payment completed", - extra={ - "cost_data": cost_data, - "model": model_obj.id, - "key_hash": key.hashed_key[:8] + "...", - }, - ) - - return Response( - content=json.dumps(openai_format_response), - media_type="application/json", - headers={"Cache-Control": "no-cache"}, - ) - - except Exception as e: - logger.error( - "Error in Gemini forward_request", - extra={ - "error": str(e), - "error_type": type(e).__name__, - "path": path, - "key_hash": key.hashed_key[:8] + "...", - }, - ) - return await super().forward_request( - request, - path, - headers, - request_body, - key, - max_cost_for_model, - session, - model_obj, - ) + See :mod:`routstr.upstream.gemini_messages` for the full rationale + (thought-signature injection, why litellm + the openai SDK can't + carry the required ``extra_content`` field). + """ + return await gemini_messages.dispatch_gemini_messages( + request_body=request_body, + model_obj=model_obj, + base_url=self.compat_base_url, + api_key=self.api_key, + transform_model_name=self.transform_model_name, + log_extra=log_extra, + ) async def _fetch_provider_models(self) -> dict: - """Fetch models from Gemini API.""" + """Fetch models from Gemini API via the OpenAI-compat client.""" try: models_data = await self.client.list_models() diff --git a/routstr/upstream/gemini_messages.py b/routstr/upstream/gemini_messages.py new file mode 100644 index 00000000..11440b91 --- /dev/null +++ b/routstr/upstream/gemini_messages.py @@ -0,0 +1,480 @@ +"""Custom /v1/messages dispatcher for Gemini's OpenAI-compat endpoint. + +Why this exists +--------------- + +Gemini 2.5 / 3 thinking models reject inbound ``functionCall`` parts that +lack a ``thought_signature`` field once any prior turn in the conversation +contains a function call. Anthropic-Messages clients (Claude Code etc.) +have no concept of thought signatures, so multi-turn tool conversations +fail with:: + + Function call is missing a thought_signature in functionCall parts. + +Google's published escape hatch (https://ai.google.dev/gemini-api/docs/ +thought-signatures, FAQ #1) is the dummy signature +``"skip_thought_signature_validator"`` placed at +``tool_calls[i].extra_content.google.thought_signature`` for every tool +call in the request. The hatch is documented specifically for +"transferring a trace from a different model that does not include thought +signatures" — exactly our case. + +Why we can't reach the wire via litellm +--------------------------------------- + +``litellm.anthropic.messages.acreate`` flows through the openai SDK, whose +pydantic ``ChatCompletionMessageToolCall`` model silently drops unknown +fields like ``extra_content``. Litellm has no openai-compat translator +that emits ``extra_content.google.thought_signature``. So we bypass both +litellm and the openai SDK at the transport layer. + +Pipeline +-------- + +1. Translate Anthropic body → OpenAI body via litellm's + ``AnthropicAdapter`` (the same translator + ``litellm.anthropic.messages.acreate`` uses internally). +2. Inject ``extra_content.google.thought_signature`` on every + ``tool_calls[]`` entry. +3. Set ``reasoning_effort="none"`` to disable Gemini's thinking pass. +4. POST directly to ``{base_url}/chat/completions`` with ``stream=true`` + via ``httpx`` (preserves arbitrary fields verbatim). +5. Translate OpenAI streaming chunks → Anthropic SSE events. +""" + +from __future__ import annotations + +import json +import uuid +from collections.abc import AsyncGenerator, AsyncIterator +from typing import Any, Callable + +import httpx + +from ..core import get_logger +from ..core.exceptions import UpstreamError +from ..payment.models import Model +from .messages_dispatch import ( + ANTHROPIC_ONLY_FIELDS, + aggregate_anthropic_events_to_message, +) + +logger = get_logger(__name__) + +DUMMY_THOUGHT_SIGNATURE = "skip_thought_signature_validator" + +# Mapping: OpenAI finish_reason → Anthropic stop_reason +_FINISH_TO_STOP = { + "stop": "end_turn", + "length": "max_tokens", + "tool_calls": "tool_use", + "function_call": "tool_use", + "content_filter": "refusal", +} + + +def inject_thought_signatures(messages: list[dict]) -> None: + """Add ``extra_content.google.thought_signature`` to every tool_call. + + Mutates ``messages`` in place. Idempotent: existing signatures are not + overwritten. + """ + for msg in messages: + tool_calls = msg.get("tool_calls") + if not isinstance(tool_calls, list): + continue + for tc in tool_calls: + if not isinstance(tc, dict): + continue + extra = tc.get("extra_content") + if not isinstance(extra, dict): + extra = tc["extra_content"] = {} + google_cfg = extra.get("google") + if not isinstance(google_cfg, dict): + google_cfg = extra["google"] = {} + google_cfg.setdefault("thought_signature", DUMMY_THOUGHT_SIGNATURE) + + +def _translate_anthropic_to_openai(body: dict, model: str) -> dict: + """Use litellm's translator to convert an Anthropic /messages body to + OpenAI /chat/completions kwargs. + + Imported lazily because the litellm internal path is heavy and not + needed for any other code path in routstr. + """ + from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import ( # noqa: E501 + AnthropicAdapter, + ) + + kwargs = {"model": model, **body} + translated = AnthropicAdapter().translate_completion_input_params(kwargs) + if translated is None: + raise UpstreamError( + "Failed to translate Anthropic body to OpenAI format", + status_code=500, + ) + return dict(translated) + + +def _sse_event(event_type: str, payload: dict) -> bytes: + return f"event: {event_type}\ndata: {json.dumps(payload)}\n\n".encode() + + +async def _openai_chunks_to_anthropic_events( + line_iter: AsyncIterator[str], requested_model: str | None +) -> AsyncGenerator[bytes, None]: + """Translate an OpenAI chat-completions SSE byte stream into the + Anthropic-Messages SSE event sequence. + + Maintains per-chunk state across: + + * one optional text content block (lazy-opened on first text delta) + * any number of tool_use blocks indexed by openai's ``delta.tool_calls[].index`` + * final ``stop_reason`` / ``usage`` carried out via ``message_delta`` / + ``message_stop`` + """ + msg_id = f"msg_{uuid.uuid4().hex[:24]}" + started = False + text_block_idx: int | None = None + tool_block_indices: dict[int, int] = {} + next_block_idx = 0 + final_finish_reason: str | None = None + final_usage: dict[str, int] = {"input_tokens": 0, "output_tokens": 0} + + def open_text_block() -> bytes: + nonlocal text_block_idx, next_block_idx + text_block_idx = next_block_idx + next_block_idx += 1 + return _sse_event( + "content_block_start", + { + "type": "content_block_start", + "index": text_block_idx, + "content_block": {"type": "text", "text": ""}, + }, + ) + + def open_tool_block(delta_idx: int, tc: dict) -> bytes: + nonlocal next_block_idx + block_idx = next_block_idx + next_block_idx += 1 + tool_block_indices[delta_idx] = block_idx + fn = tc.get("function") or {} + return _sse_event( + "content_block_start", + { + "type": "content_block_start", + "index": block_idx, + "content_block": { + "type": "tool_use", + "id": tc.get("id") or f"toolu_{uuid.uuid4().hex[:24]}", + "name": fn.get("name") or "", + "input": {}, + }, + }, + ) + + def close_block(idx: int) -> bytes: + return _sse_event( + "content_block_stop", + {"type": "content_block_stop", "index": idx}, + ) + + async for raw_line in line_iter: + line = raw_line.strip() + if not line: + continue + if not line.startswith("data:"): + continue + payload = line[5:].lstrip() + if not payload or payload == "[DONE]": + continue + try: + chunk = json.loads(payload) + except json.JSONDecodeError: + continue + if not isinstance(chunk, dict): + continue + + if not started: + started = True + yield _sse_event( + "message_start", + { + "type": "message_start", + "message": { + "id": chunk.get("id") or msg_id, + "type": "message", + "role": "assistant", + "model": requested_model or chunk.get("model") or "", + "content": [], + "stop_reason": None, + "stop_sequence": None, + "usage": { + "input_tokens": 0, + "output_tokens": 0, + }, + }, + }, + ) + + usage = chunk.get("usage") + if isinstance(usage, dict): + in_tok = usage.get("prompt_tokens") or usage.get("input_tokens") or 0 + out_tok = usage.get("completion_tokens") or usage.get("output_tokens") or 0 + if in_tok: + final_usage["input_tokens"] = int(in_tok) + if out_tok: + final_usage["output_tokens"] = int(out_tok) + + choices = chunk.get("choices") or [] + if not choices: + continue + choice = choices[0] if isinstance(choices[0], dict) else {} + delta = choice.get("delta") or {} + if not isinstance(delta, dict): + delta = {} + + # Text delta + text = delta.get("content") + if isinstance(text, str) and text: + if text_block_idx is None: + yield open_text_block() + yield _sse_event( + "content_block_delta", + { + "type": "content_block_delta", + "index": text_block_idx, + "delta": {"type": "text_delta", "text": text}, + }, + ) + + # Tool call deltas + tool_calls_delta = delta.get("tool_calls") + if isinstance(tool_calls_delta, list): + for tc in tool_calls_delta: + if not isinstance(tc, dict): + continue + d_idx = int(tc.get("index") or 0) + if d_idx not in tool_block_indices: + yield open_tool_block(d_idx, tc) + block_idx = tool_block_indices[d_idx] + fn = tc.get("function") or {} + args = fn.get("arguments") + if isinstance(args, str) and args: + yield _sse_event( + "content_block_delta", + { + "type": "content_block_delta", + "index": block_idx, + "delta": { + "type": "input_json_delta", + "partial_json": args, + }, + }, + ) + + finish = choice.get("finish_reason") + if finish: + final_finish_reason = finish + + # Close any open content blocks + if text_block_idx is not None: + yield close_block(text_block_idx) + for block_idx in tool_block_indices.values(): + yield close_block(block_idx) + + # message_delta with stop_reason and usage + stop_reason = _FINISH_TO_STOP.get(final_finish_reason or "", "end_turn") + yield _sse_event( + "message_delta", + { + "type": "message_delta", + "delta": {"stop_reason": stop_reason, "stop_sequence": None}, + "usage": final_usage, + }, + ) + yield _sse_event("message_stop", {"type": "message_stop"}) + + +async def _post_and_stream( + base_url: str, + api_key: str, + payload: dict, + log_extra: dict[str, Any] | None, +) -> tuple[httpx.AsyncClient, httpx.Response]: + """POST to upstream chat-completions and return (client, response) for + streaming. Caller is responsible for closing both.""" + url = f"{base_url.rstrip('/')}/chat/completions" + client = httpx.AsyncClient(timeout=httpx.Timeout(120.0, read=120.0)) + try: + request = client.build_request( + "POST", + url, + json=payload, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "Accept": "text/event-stream", + }, + ) + response = await client.send(request, stream=True) + except Exception as exc: + await client.aclose() + logger.error( + "Gemini messages dispatch HTTP error", + extra={"error": str(exc), "url": url, **(log_extra or {})}, + ) + raise UpstreamError( + f"Failed to reach Gemini upstream: {exc}", status_code=502 + ) from exc + + if response.status_code >= 400: + try: + body_bytes = await response.aread() + finally: + await response.aclose() + await client.aclose() + body_text = body_bytes.decode("utf-8", errors="replace") + logger.error( + "Gemini messages dispatch upstream error", + extra={ + "status_code": response.status_code, + "body": body_text[:1000], + "url": url, + **(log_extra or {}), + }, + ) + raise UpstreamError( + f"Upstream error via gemini compat: {body_text}", + status_code=response.status_code, + ) + + return client, response + + +async def dispatch_gemini_messages( + *, + request_body: bytes | None, + model_obj: Model, + base_url: str, + api_key: str, + transform_model_name: Callable[[str], str], + log_extra: dict[str, Any] | None = None, +) -> tuple[bool, Any, str | None]: + """Dispatch a /v1/messages request to Gemini's OpenAI-compat endpoint + with thought-signature injection. + + Returns ``(client_stream, result, requested_model)`` where ``result`` + is either an ``AsyncIterator[bytes]`` of Anthropic-format SSE events + (for streaming clients) or an Anthropic Message dict (after the caller + aggregates). + """ + if not request_body: + raise UpstreamError( + "Missing request body for /v1/messages", status_code=400 + ) + + try: + body: dict = json.loads(request_body) + except json.JSONDecodeError as exc: + raise UpstreamError( + f"Invalid JSON in /v1/messages body: {exc}", status_code=400 + ) from exc + + body.pop("model", None) + client_stream = bool(body.pop("stream", False)) + + # Anthropic-Messages-only fields that don't translate to OpenAI + # Chat Completions. litellm's translator passes through unknown + # top-level fields verbatim and Gemini's compat surface 400s on + # unknown names like ``context_management`` / ``output_config``. + dropped: dict[str, Any] = {} + for field in ANTHROPIC_ONLY_FIELDS: + if field in body: + dropped[field] = body.pop(field) + if dropped: + logger.debug( + "Dropped anthropic-only fields before gemini compat dispatch", + extra={"dropped_keys": sorted(dropped.keys())}, + ) + + requested_model = ( + (model_obj.forwarded_model_id or model_obj.id) if model_obj else None + ) + upstream_model = transform_model_name(model_obj.id) + + openai_kwargs = _translate_anthropic_to_openai(body, upstream_model) + + messages = openai_kwargs.get("messages") or [] + if isinstance(messages, list): + inject_thought_signatures(messages) + + # Disable Gemini's thinking pass; the dummy signature already lifts + # validation, but skipping thinking entirely avoids degraded model + # output and keeps tool-calling deterministic. + openai_kwargs.setdefault("reasoning_effort", "none") + openai_kwargs["stream"] = True + openai_kwargs["model"] = upstream_model + # OpenAI-compat backends (including Gemini's) only emit a final + # ``usage`` chunk when the request opts in via this flag. Without it + # the cost-calculation pipeline can't read real token counts and + # falls back to MaxCostData billing. + existing_stream_options = openai_kwargs.get("stream_options") + merged_stream_options = ( + dict(existing_stream_options) + if isinstance(existing_stream_options, dict) + else {} + ) + merged_stream_options.setdefault("include_usage", True) + openai_kwargs["stream_options"] = merged_stream_options + + logger.info( + "Dispatching /v1/messages via gemini compat (httpx)", + extra={ + "model": upstream_model, + "client_stream": client_stream, + "messages_with_tool_calls": sum( + 1 for m in messages if isinstance(m, dict) and m.get("tool_calls") + ), + **(log_extra or {}), + }, + ) + + http_client, response = await _post_and_stream( + base_url, api_key, openai_kwargs, log_extra + ) + + async def line_iter() -> AsyncGenerator[str, None]: + try: + async for line in response.aiter_lines(): + yield line + finally: + await response.aclose() + await http_client.aclose() + + anthropic_event_iter = _openai_chunks_to_anthropic_events( + line_iter(), requested_model + ) + + if not client_stream: + # Aggregate the Anthropic SSE byte stream into a single Message dict + # so the rest of the pipeline (cost calc, metadata injection, + # response building) can treat it identically to a non-streaming + # litellm response. + try: + aggregated = await aggregate_anthropic_events_to_message( + anthropic_event_iter + ) + except Exception as exc: + logger.error( + "Failed to aggregate Gemini compat events into message", + extra={"error": str(exc), **(log_extra or {})}, + ) + raise UpstreamError( + f"Failed to aggregate upstream stream: {exc}", + status_code=502, + ) from exc + return client_stream, aggregated, requested_model + + return client_stream, anthropic_event_iter, requested_model diff --git a/routstr/upstream/groq.py b/routstr/upstream/groq.py index 11ab8ce4..4020b2df 100644 --- a/routstr/upstream/groq.py +++ b/routstr/upstream/groq.py @@ -12,6 +12,7 @@ class GroqUpstreamProvider(BaseUpstreamProvider): provider_type = "groq" default_base_url = "https://api.groq.com/openai/v1" platform_url = "https://console.groq.com/keys" + litellm_provider_prefix = "groq/" def __init__(self, api_key: str, provider_fee: float = 1.01): super().__init__( diff --git a/routstr/upstream/litellm_routing.py b/routstr/upstream/litellm_routing.py new file mode 100644 index 00000000..0b2a92a3 --- /dev/null +++ b/routstr/upstream/litellm_routing.py @@ -0,0 +1,162 @@ +"""Map an upstream `base_url` to the correct litellm provider prefix. + +Used by `BaseUpstreamProvider.get_litellm_provider_prefix` so that custom / +generic provider rows (which inherit the base class default) get routed to +the right litellm backend instead of falling back to `openai/`. + +The table is compiled from litellm 1.74's +`litellm/litellm_core_utils/get_llm_provider_logic.py` (the +`openai_compatible_endpoints` table) plus the providers documented at +https://docs.litellm.ai/docs/providers. Substring match is used so that +URLs with paths, ports, regional subdomains, etc. all resolve correctly. + +Order matters: more specific needles must appear before more generic ones +(e.g. ``openai.azure.com`` before ``api.openai.com``). +""" + +from __future__ import annotations + +import os +from urllib.parse import urlsplit + +import litellm + +DEFAULT_PREFIX = "openai/" + +LITELLM_HOST_PREFIX_MAP: tuple[tuple[str, str], ...] = ( + # Azure must win over api.openai.com because the host ends with + # `openai.azure.com` and we don't want it picked up as plain OpenAI. + ("openai.azure.com", "azure/"), + # Google + ("generativelanguage.googleapis.com", "gemini/"), + ("aiplatform.googleapis.com", "vertex_ai/"), + # First-class providers with native litellm prefixes + ("api.openai.com", "openai/"), + ("api.anthropic.com", "anthropic/"), + ("api.groq.com", "groq/"), + ("api.fireworks.ai", "fireworks_ai/"), + ("api.x.ai", "xai/"), + ("api.perplexity.ai", "perplexity/"), + ("openrouter.ai", "openrouter/"), + ("api.deepseek.com", "deepseek/"), + ("api.together.xyz", "together_ai/"), + ("codestral.mistral.ai", "codestral/"), + ("api.mistral.ai", "mistral/"), + ("api.cohere.com", "cohere_chat/"), + ("api.cohere.ai", "cohere_chat/"), + ("api.deepinfra.com", "deepinfra/"), + ("api.endpoints.anyscale.com", "anyscale/"), + ("api.cerebras.ai", "cerebras/"), + ("inference.baseten.co", "baseten/"), + ("api.sambanova.ai", "sambanova/"), + ("api.ai21.com", "ai21_chat/"), + ("api.friendli.ai", "friendliai/"), + ("api.galadriel.com", "galadriel/"), + ("api.llama.com", "meta_llama/"), + ("api.featherless.ai", "featherless_ai/"), + ("inference.api.nscale.com", "nscale/"), + ("dashscope-intl.aliyuncs.com", "dashscope/"), + ("api.moonshot.ai", "moonshot/"), + ("api.moonshot.cn", "moonshot/"), + ("api.minimax.io", "minimax/"), + ("api.minimaxi.com", "minimax/"), + ("platform.publicai.co", "publicai/"), + ("api.synthetic.new", "synthetic/"), + ("api.stima.tech", "apertis/"), + ("nano-gpt.com", "nano-gpt/"), + ("api.poe.com", "poe/"), + ("llm.chutes.ai", "chutes/"), + ("api.v0.dev", "v0/"), + ("api.lambda.ai", "lambda_ai/"), + ("api.hyperbolic.xyz", "hyperbolic/"), + ("ai-gateway.vercel.sh", "vercel_ai_gateway/"), + ("api.inference.wandb.ai", "wandb/"), + ("integrate.api.nvidia.com", "nvidia_nim/"), + ("api.studio.nebius.com", "nebius/"), + ("api.novita.ai", "novita/"), + ("ark.cn-beijing.volces.com", "volcengine/"), + ("api.voyageai.com", "voyage/"), + ("api.jina.ai", "jina_ai/"), + ("api.aimlapi.com", "aiml/"), + ("api.snowflakecomputing.com", "snowflake/"), + ("databricks.com", "databricks/"), + ("huggingface.co", "huggingface/"), +) + +# Substrings that indicate an Ollama deployment regardless of port/scheme. +OLLAMA_HOST_HINTS: tuple[str, ...] = ( + "localhost:11434", + "127.0.0.1:11434", + "ollama", +) + + +def detect_litellm_prefix( + base_url: str | None, default: str = DEFAULT_PREFIX +) -> str: + """Return the litellm provider prefix (`"/"`) for `base_url`. + + Falls back to `default` when the host doesn't match any known provider. + The default is `openai/` because every unmatched OpenAI-compatible + server is, by definition, an OpenAI-compatible server. + """ + if not base_url: + return default + + parsed = urlsplit(base_url) + host = parsed.netloc.lower() or base_url.lower() + + for needle, prefix in LITELLM_HOST_PREFIX_MAP: + if needle in host: + return prefix + + if any(hint in host for hint in OLLAMA_HOST_HINTS): + return "ollama_chat/" + + return default + + +_configured = False + + +def configure_litellm() -> None: + """Apply litellm global settings used by the messages-dispatch path. + + Idempotent: safe to call from both app startup and module-level + initializers without side effects on the second invocation. + + Settings applied: + + * ``LITELLM_DEBUG=1`` enables litellm's verbose debug logger. + * Forces the Anthropic-messages adapter to call OpenAI Chat Completions + (POST ``/chat/completions``) instead of the Responses API (POST + ``/responses``) for ``openai/``-prefixed providers. OpenAI-compatible + upstreams like Google's generativelanguage compat endpoint expose + ``/chat/completions`` but not ``/responses``, which would 404. Set + ``LITELLM_USE_RESPONSES_API_FOR_ANTHROPIC_MESSAGES=1`` to opt out. + * Silently drops Anthropic-Messages-only parameters (``thinking``, + ``cache_control``, ``context_management``, ...) when translating to + providers that don't accept them, instead of raising + ``UnsupportedParamsError``. Set ``LITELLM_STRICT_PARAMS=1`` to opt + out. + """ + global _configured + if _configured: + return + + if os.getenv("LITELLM_DEBUG") == "1": + try: + litellm._turn_on_debug() # type: ignore[no-untyped-call] + except Exception: + pass + + if os.getenv("LITELLM_USE_RESPONSES_API_FOR_ANTHROPIC_MESSAGES") != "1": + try: + litellm.use_chat_completions_url_for_anthropic_messages = True + except Exception: + pass + + if os.getenv("LITELLM_STRICT_PARAMS") != "1": + litellm.drop_params = True + + _configured = True diff --git a/routstr/upstream/messages_dispatch.py b/routstr/upstream/messages_dispatch.py new file mode 100644 index 00000000..a492e481 --- /dev/null +++ b/routstr/upstream/messages_dispatch.py @@ -0,0 +1,458 @@ +"""Pure helpers for translating ``/v1/messages`` to upstream chat completions +via litellm. + +This module owns the litellm/Anthropic-Messages translation layer: + +* SSE parsing (``parse_sse_blocks``, ``events_from_chunk``) +* Payload coercion (``coerce_litellm_payload``) +* Stream aggregation (``aggregate_anthropic_events_to_message``) — drains + a streamed Anthropic event sequence into a single Message dict +* Per-event annotation for streaming (``annotate_event``, + ``stream_annotated_events``) — handles the model-rewrite + token-tally + bookkeeping shared by the bearer-key and x-cashu streaming paths +* The dispatch entry point (``dispatch_anthropic_messages``) +* Refund math (``compute_refund``) + +Nothing in here touches ``BaseUpstreamProvider``; the thin instance methods +on the provider class forward to these functions and only retain logic that +genuinely needs ``self`` (cost adjustment, metadata injection, refund +sending). +""" + +from __future__ import annotations + +import json +from collections.abc import AsyncGenerator, AsyncIterator +from typing import Any, Callable, NamedTuple, cast + +import litellm + +from ..core import get_logger +from ..core.exceptions import UpstreamError +from ..payment.models import Model + +logger = get_logger(__name__) + +# Anthropic-Messages-only fields that don't translate to OpenAI +# Chat Completions. ``litellm.drop_params`` only filters *known* +# unsupported params; these newer/extension fields get passed through +# verbatim and the upstream rejects them with a 400. Pop them here so the +# request reaches the upstream cleanly. +ANTHROPIC_ONLY_FIELDS: tuple[str, ...] = ( + "thinking", + "cache_control", + "context_management", + "output_config", + "mcp_servers", + "service_tier", + "anthropic_version", + "anthropic_beta", +) + + +def coerce_litellm_payload(payload: object) -> dict: + """Convert a litellm event into a plain dict. + + Non-streaming responses come back as Anthropic-shaped pydantic models + or dicts. Streaming may yield raw bytes/str (SSE-encoded); those go + through ``events_from_chunk`` instead, not here. + """ + if isinstance(payload, dict): + return dict(payload) + if hasattr(payload, "model_dump"): + return cast(dict, payload.model_dump()) + raise TypeError(f"Cannot coerce {type(payload).__name__} to dict") + + +def parse_sse_blocks(buffer: bytes) -> tuple[list[dict], bytes]: + """Parse complete SSE event blocks out of a byte buffer. + + Returns (events, remaining_buffer). Events are JSON objects parsed from + one or more ``data:`` lines per block. Comments, blank lines, and + ``[DONE]`` sentinels are ignored. A trailing partial block is preserved + in remaining_buffer. + """ + events: list[dict] = [] + while True: + sep = buffer.find(b"\n\n") + if sep < 0: + sep_rn = buffer.find(b"\r\n\r\n") + if sep_rn < 0: + break + block = buffer[:sep_rn] + buffer = buffer[sep_rn + 4 :] + else: + block = buffer[:sep] + buffer = buffer[sep + 2 :] + + data_lines: list[str] = [] + for raw_line in block.replace(b"\r\n", b"\n").split(b"\n"): + line = raw_line.decode("utf-8", errors="replace") + if line.startswith(":"): + continue + if line.startswith("data:"): + data_lines.append(line[5:].lstrip()) + if not data_lines: + continue + payload = "\n".join(data_lines).strip() + if not payload or payload == "[DONE]": + continue + try: + obj = json.loads(payload) + except json.JSONDecodeError: + continue + if isinstance(obj, dict): + events.append(obj) + return events, buffer + + +def events_from_chunk( + chunk: object, sse_buffer: bytes +) -> tuple[list[dict], bytes]: + """Normalize a stream chunk into one or more event dicts. + + ``litellm.anthropic.messages.acreate(stream=True)`` yields raw SSE + bytes in practice; some adapters yield strings or typed events. Handle + all three. + """ + if isinstance(chunk, (bytes, bytearray)): + sse_buffer += bytes(chunk) + events, sse_buffer = parse_sse_blocks(sse_buffer) + return events, sse_buffer + if isinstance(chunk, str): + sse_buffer += chunk.encode("utf-8") + events, sse_buffer = parse_sse_blocks(sse_buffer) + return events, sse_buffer + return [coerce_litellm_payload(chunk)], sse_buffer + + +async def aggregate_anthropic_events_to_message( + iterator: AsyncIterator[Any], +) -> dict: + """Drain an Anthropic-Messages event iterator into a single Message dict. + + Produces the shape ``litellm.anthropic.messages.acreate(stream=False)`` + would have returned. Used to transparently stream from upstream while + still returning a non-streaming response to the client. Lets us + sidestep upstream quirks (e.g. Fireworks rejects ``max_tokens > 4096`` + unless ``stream=true``) without leaking that into client-visible + behavior. + """ + sse_buffer = b"" + message: dict = {} + blocks: list[dict] = [] + partial_json: dict[int, str] = {} + final_stop_reason: str | None = None + final_stop_sequence: str | None = None + final_usage: dict[str, Any] = {} + final_model: str | None = None + + async for chunk in iterator: + events, sse_buffer = events_from_chunk(chunk, sse_buffer) + for event in events: + etype = event.get("type") + if etype == "message_start": + raw = event.get("message") or {} + if isinstance(raw, dict): + message = dict(raw) + existing = message.get("content") + blocks = list(existing) if isinstance(existing, list) else [] + usage = message.get("usage") + if isinstance(usage, dict): + final_usage = dict(usage) + if isinstance(message.get("model"), str): + final_model = message["model"] + elif etype == "content_block_start": + idx = int(event.get("index") or 0) + cb = event.get("content_block") or {} + cb_dict = dict(cb) if isinstance(cb, dict) else {} + while len(blocks) <= idx: + blocks.append({}) + blocks[idx] = cb_dict + elif etype == "content_block_delta": + idx = int(event.get("index") or 0) + if idx >= len(blocks): + continue + delta = event.get("delta") or {} + if not isinstance(delta, dict): + continue + dtype = delta.get("type") + block = blocks[idx] + if dtype == "text_delta": + block["text"] = (block.get("text") or "") + ( + delta.get("text") or "" + ) + elif dtype == "input_json_delta": + partial_json[idx] = partial_json.get(idx, "") + ( + delta.get("partial_json") or "" + ) + elif dtype == "thinking_delta": + block["thinking"] = (block.get("thinking") or "") + ( + delta.get("thinking") or "" + ) + elif dtype == "signature_delta": + block["signature"] = (block.get("signature") or "") + ( + delta.get("signature") or "" + ) + elif etype == "content_block_stop": + idx = int(event.get("index") or 0) + raw_json = partial_json.pop(idx, None) + if raw_json is not None and idx < len(blocks): + try: + blocks[idx]["input"] = ( + json.loads(raw_json) if raw_json else {} + ) + except json.JSONDecodeError: + blocks[idx]["input"] = raw_json + elif etype == "message_delta": + delta = event.get("delta") or {} + if isinstance(delta, dict): + if "stop_reason" in delta: + final_stop_reason = delta.get("stop_reason") + if "stop_sequence" in delta: + final_stop_sequence = delta.get("stop_sequence") + usage = event.get("usage") + if isinstance(usage, dict): + final_usage.update(usage) + # message_stop: nothing to merge + + if not message: + # Upstream returned no message_start; expose what we can so the + # client at least sees the assembled content. + message = { + "id": "", + "type": "message", + "role": "assistant", + "content": [], + } + + message["content"] = blocks + if final_model and not message.get("model"): + message["model"] = final_model + if final_stop_reason is not None: + message["stop_reason"] = final_stop_reason + if final_stop_sequence is not None: + message["stop_sequence"] = final_stop_sequence + if final_usage: + existing_usage = message.get("usage") + merged = dict(existing_usage) if isinstance(existing_usage, dict) else {} + merged.update(final_usage) + message["usage"] = merged + return message + + +class AnnotatedEvent(NamedTuple): + """One Anthropic SSE event after model-rewrite + token-tally bookkeeping. + + ``sse_bytes`` is the wire-ready ``event:`` / ``data:`` block; the two + streaming paths in ``BaseUpstreamProvider`` consume ``sse_bytes`` plus + the tallies and only differ in whether they stream live or buffer + first. + """ + + event: dict + sse_bytes: bytes + input_tokens: int + output_tokens: int + model: str | None + + +def annotate_event(event: dict, requested_model: str | None) -> AnnotatedEvent: + """Rewrite ``model`` fields and extract per-event token / model info. + + Mutates ``event`` in place when ``requested_model`` is set so the + upstream's true model name doesn't leak to the client. + """ + if requested_model: + msg = event.get("message") + if isinstance(msg, dict) and "model" in msg: + msg["model"] = requested_model + if "model" in event: + event["model"] = requested_model + + in_tokens = 0 + out_tokens = 0 + model: str | None = None + + msg_for_meta = event.get("message") + if isinstance(msg_for_meta, dict): + if msg_for_meta.get("model"): + model = str(msg_for_meta["model"]) + usage = msg_for_meta.get("usage") + if isinstance(usage, dict): + in_tokens += int(usage.get("input_tokens") or 0) + out_tokens += int(usage.get("output_tokens") or 0) + + if isinstance(event.get("usage"), dict): + usage = event["usage"] + in_tokens += int(usage.get("input_tokens") or 0) + out_tokens += int(usage.get("output_tokens") or 0) + + event_type = str(event.get("type") or "") + payload = json.dumps(event) + if event_type: + sse_bytes = f"event: {event_type}\ndata: {payload}\n\n".encode() + else: + sse_bytes = f"data: {payload}\n\n".encode() + + return AnnotatedEvent(event, sse_bytes, in_tokens, out_tokens, model) + + +async def stream_annotated_events( + iterator: AsyncIterator[Any], + requested_model: str | None, +) -> AsyncGenerator[AnnotatedEvent, None]: + """Yield annotated, SSE-serialized events from a litellm stream. + + Both streaming paths in ``BaseUpstreamProvider`` consume this; the only + divergence between them — yield-as-you-go vs buffer-then-replay — stays + in the caller. + """ + sse_buffer = b"" + async for chunk in iterator: + events, sse_buffer = events_from_chunk(chunk, sse_buffer) + for event in events: + yield annotate_event(event, requested_model) + + +def compute_refund(amount: int, unit: str, cost_msats: int) -> int: + if unit == "msat": + return amount - cost_msats + if unit == "sat": + return amount - (cost_msats + 999) // 1000 + raise ValueError(f"Invalid unit: {unit}") + + +async def dispatch_anthropic_messages( + *, + request_body: bytes | None, + model_obj: Model, + base_url: str, + api_key: str, + provider_prefix: str, + transform_model_name: Callable[[str], str], + log_extra: dict[str, Any] | None = None, +) -> tuple[bool, Any, str | None]: + """Call ``litellm.anthropic.messages.acreate`` and return + ``(client_stream, result, requested_model)``. + + Shared by the bearer-key and x-cashu paths. Raises :class:`UpstreamError` + on bad input or upstream failure. + """ + if not request_body: + raise UpstreamError( + "Missing request body for /v1/messages", status_code=400 + ) + + try: + body: dict = json.loads(request_body) + except json.JSONDecodeError as exc: + raise UpstreamError( + f"Invalid JSON in /v1/messages body: {exc}", status_code=400 + ) from exc + + body.pop("model", None) + # `stream` here is what the **client** asked for. Upstream is always + # streamed (see `upstream_stream` below); when the client asked for a + # non-streaming response we drain and aggregate the events into a + # single Anthropic Message dict before returning. This sidesteps + # provider-specific non-streaming caps (e.g. Fireworks rejects + # `max_tokens > 4096` unless `stream=true`). + client_stream = bool(body.pop("stream", False)) + upstream_stream = True + + dropped: dict[str, Any] = {} + for field in ANTHROPIC_ONLY_FIELDS: + if field in body: + dropped[field] = body.pop(field) + if dropped: + logger.debug( + "Dropped anthropic-only fields before litellm dispatch", + extra={"dropped_keys": sorted(dropped.keys())}, + ) + + # Convention: `model.id` is the canonical upstream model name; + # `forwarded_model_id` is the public alias the internal API exposes + # and echoes back to the client. + requested_model = ( + (model_obj.forwarded_model_id or model_obj.id) if model_obj else None + ) + upstream_model = transform_model_name(model_obj.id) + litellm_model = f"{provider_prefix}{upstream_model}" + + kwargs: dict = { + "model": litellm_model, + "api_base": base_url, + "api_key": api_key, + "stream": upstream_stream, + **body, + } + + logger.info( + "Dispatching /v1/messages via litellm", + extra={ + "model": litellm_model, + "resolved_provider": provider_prefix.rstrip("/"), + "client_stream": client_stream, + "upstream_stream": upstream_stream, + **(log_extra or {}), + }, + ) + + try: + result = await litellm.anthropic.messages.acreate(**kwargs) + except Exception as exc: + exc_message = getattr(exc, "message", None) or str(exc) or repr(exc) + exc_status = getattr(exc, "status_code", None) + exc_response = getattr(exc, "response", None) + response_text = None + if exc_response is not None: + try: + response_text = getattr(exc_response, "text", str(exc_response)) + except Exception: + response_text = "" + logger.error( + "litellm dispatch failed", + extra={ + "error": exc_message, + "error_type": type(exc).__name__, + "status_code": exc_status, + "llm_provider": getattr(exc, "llm_provider", None), + "body": getattr(exc, "body", None), + "response_text": response_text, + "model": litellm_model, + "api_base": base_url, + }, + ) + raise UpstreamError( + f"Upstream error via litellm: {exc_message}", + status_code=exc_status if isinstance(exc_status, int) else 502, + ) from exc + + if not client_stream and hasattr(result, "__aiter__"): + # Client asked for a non-streaming response but we always stream + # from upstream — drain the events into a single Anthropic Message + # dict so the rest of the pipeline can treat it as if upstream had + # returned non-streaming. Some litellm adapters return a + # non-streaming dict even when ``stream=True``; in that case, + # leave the result as-is. + try: + aggregated: Any = await aggregate_anthropic_events_to_message( + cast(AsyncIterator[Any], result) + ) + except Exception as exc: + logger.error( + "Failed to aggregate streamed events into message", + extra={ + "error": str(exc), + "error_type": type(exc).__name__, + "model": litellm_model, + }, + ) + raise UpstreamError( + f"Failed to aggregate upstream stream: {exc}", + status_code=502, + ) from exc + return client_stream, aggregated, requested_model + + return client_stream, result, requested_model diff --git a/routstr/upstream/ollama.py b/routstr/upstream/ollama.py index eff5d5bb..b8df6f3f 100644 --- a/routstr/upstream/ollama.py +++ b/routstr/upstream/ollama.py @@ -21,6 +21,7 @@ class OllamaUpstreamProvider(BaseUpstreamProvider): provider_type = "ollama" default_base_url = "http://localhost:11434" platform_url = None + litellm_provider_prefix = "ollama_chat/" def __init__( self, diff --git a/routstr/upstream/openrouter.py b/routstr/upstream/openrouter.py index 1736f1d7..2cbc34de 100644 --- a/routstr/upstream/openrouter.py +++ b/routstr/upstream/openrouter.py @@ -15,6 +15,8 @@ class OpenRouterUpstreamProvider(BaseUpstreamProvider): provider_type = "openrouter" default_base_url = "https://openrouter.ai/api/v1" platform_url = "https://openrouter.ai/settings/keys" + supports_anthropic_messages = True + litellm_provider_prefix = "openrouter/" def __init__(self, api_key: str, provider_fee: float = 1.06): """Initialize OpenRouter provider with API key. diff --git a/routstr/upstream/perplexity.py b/routstr/upstream/perplexity.py index b73881d8..55a9116e 100644 --- a/routstr/upstream/perplexity.py +++ b/routstr/upstream/perplexity.py @@ -13,6 +13,7 @@ class PerplexityUpstreamProvider(BaseUpstreamProvider): provider_type = "perplexity" default_base_url = "https://api.perplexity.ai/" platform_url = "https://www.perplexity.ai/account/api/keys" + litellm_provider_prefix = "perplexity/" def __init__(self, api_key: str, provider_fee: float = 1.01): super().__init__( diff --git a/routstr/upstream/ppqai.py b/routstr/upstream/ppqai.py index a6890567..48ca9600 100644 --- a/routstr/upstream/ppqai.py +++ b/routstr/upstream/ppqai.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional import httpx -from pydantic import BaseModel, Field +from pydantic.v1 import BaseModel, Field from ..core.logging import get_logger from ..payment.models import Architecture, Model, Pricing, async_fetch_openrouter_models diff --git a/routstr/upstream/xai.py b/routstr/upstream/xai.py index 99e2d35a..b46676cb 100644 --- a/routstr/upstream/xai.py +++ b/routstr/upstream/xai.py @@ -13,6 +13,7 @@ class XAIUpstreamProvider(BaseUpstreamProvider): provider_type = "x-ai" default_base_url = "https://api.x.ai/v1" platform_url = "https://console.x.ai/" + litellm_provider_prefix = "xai/" def __init__(self, api_key: str, provider_fee: float = 1.01): super().__init__( diff --git a/routstr/wallet.py b/routstr/wallet.py index 360447d6..9000763b 100644 --- a/routstr/wallet.py +++ b/routstr/wallet.py @@ -1,10 +1,13 @@ import asyncio import time +import typing from typing import TypedDict from cashu.core.base import Proof, Token +from cashu.core.mint_info import MintInfo as _CashuMintInfo from cashu.wallet.helpers import deserialize_token_from_string from cashu.wallet.wallet import Wallet +from pydantic_core import PydanticUndefined from sqlmodel import col, select, update from .core import db, get_logger @@ -12,6 +15,18 @@ from .core.db import store_cashu_transaction from .core.settings import settings from .payment.lnurl import raw_send_to_lnurl +# cashu still declares Optional[X] without explicit defaults on MintInfo. +# Under pydantic v2 those are required, but real mints omit many of them. +# Default Optional fields to None at import time so balance fetches don't 422. +for _name, _field in _CashuMintInfo.model_fields.items(): + _annot = _field.annotation + _is_optional = typing.get_origin(_annot) is typing.Union and type( + None + ) in typing.get_args(_annot) + if _is_optional and _field.default is PydanticUndefined: + _field.default = None +_CashuMintInfo.model_rebuild(force=True) + logger = get_logger(__name__) diff --git a/tests/unit/test_litellm_routing.py b/tests/unit/test_litellm_routing.py new file mode 100644 index 00000000..264a142d --- /dev/null +++ b/tests/unit/test_litellm_routing.py @@ -0,0 +1,93 @@ +"""Tests for `routstr.upstream.litellm_routing.detect_litellm_prefix`.""" + +from __future__ import annotations + +import pytest + +from routstr.upstream.litellm_routing import detect_litellm_prefix + + +@pytest.mark.parametrize( + "base_url,expected", + [ + # The bug case: custom row pointing at Fireworks must NOT route to openai/. + ("https://api.fireworks.ai/inference/v1", "fireworks_ai/"), + # Other OpenAI-compatible providers commonly plugged into the custom slot. + ("https://api.groq.com/openai/v1", "groq/"), + ("https://api.x.ai/v1", "xai/"), + ("https://api.deepseek.com/v1", "deepseek/"), + ("https://api.together.xyz/v1", "together_ai/"), + ("https://api.perplexity.ai", "perplexity/"), + ("https://openrouter.ai/api/v1", "openrouter/"), + ("https://api.mistral.ai/v1", "mistral/"), + ("https://codestral.mistral.ai/v1", "codestral/"), + ("https://api.cohere.com/v1", "cohere_chat/"), + ("https://api.cohere.ai/v1", "cohere_chat/"), + ("https://api.deepinfra.com/v1/openai", "deepinfra/"), + ("https://api.cerebras.ai/v1", "cerebras/"), + ("https://api.sambanova.ai/v1", "sambanova/"), + ("https://api.moonshot.cn/v1", "moonshot/"), + ("https://api.moonshot.ai/v1", "moonshot/"), + ("https://api.studio.nebius.com/v1", "nebius/"), + ("https://api.novita.ai/v3/openai", "novita/"), + ("https://api.lambda.ai/v1", "lambda_ai/"), + ("https://api.aimlapi.com/v1", "aiml/"), + ("https://api.featherless.ai/v1", "featherless_ai/"), + ("https://integrate.api.nvidia.com/v1", "nvidia_nim/"), + ("https://inference.baseten.co/v1", "baseten/"), + ("https://ai-gateway.vercel.sh/v1", "vercel_ai_gateway/"), + ("https://api.inference.wandb.ai/v1", "wandb/"), + ("https://api.poe.com/v1", "poe/"), + ("https://llm.chutes.ai/v1/", "chutes/"), + ("https://api.v0.dev/v1", "v0/"), + ("https://api.hyperbolic.xyz/v1", "hyperbolic/"), + ("https://api.synthetic.new/openai/v1", "synthetic/"), + ("https://api.stima.tech/v1", "apertis/"), + ("https://nano-gpt.com/api/v1", "nano-gpt/"), + ("https://api.friendli.ai/serverless/v1", "friendliai/"), + ("https://api.galadriel.com/v1", "galadriel/"), + ("https://api.llama.com/compat/v1", "meta_llama/"), + ("https://api.minimax.io/v1", "minimax/"), + ("https://api.minimaxi.com/v1", "minimax/"), + ("https://platform.publicai.co/v1", "publicai/"), + ("https://inference.api.nscale.com/v1", "nscale/"), + ("https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "dashscope/"), + ("https://api.endpoints.anyscale.com/v1", "anyscale/"), + ("https://api.ai21.com/studio/v1", "ai21_chat/"), + ("https://ark.cn-beijing.volces.com/api/v3", "volcengine/"), + ("https://api.voyageai.com/v1", "voyage/"), + ("https://api.jina.ai/v1", "jina_ai/"), + ("https://api.snowflakecomputing.com", "snowflake/"), + ("https://my-workspace.databricks.com/serving-endpoints", "databricks/"), + ("https://huggingface.co/api/inference", "huggingface/"), + # First-class providers — URL detection still produces the right prefix + # so subclasses without an explicit override stay correct. + ("https://api.openai.com/v1", "openai/"), + ("https://api.anthropic.com/v1", "anthropic/"), + ("https://generativelanguage.googleapis.com/v1beta/openai", "gemini/"), + ("https://us-central1-aiplatform.googleapis.com/v1", "vertex_ai/"), + # Azure ordering: must beat api.openai.com. + ("https://my-resource.openai.azure.com/openai/deployments/foo", "azure/"), + # Ollama hints. + ("http://localhost:11434/v1", "ollama_chat/"), + ("http://127.0.0.1:11434/v1", "ollama_chat/"), + ("http://my-ollama-host:11434/v1", "ollama_chat/"), + # Casing and trailing slash normalisation. + ("HTTPS://API.FIREWORKS.AI/INFERENCE/V1/", "fireworks_ai/"), + # Unknown host falls back to openai/ (still OpenAI-compatible by convention). + ("https://example.com/v1", "openai/"), + ("", "openai/"), + ], +) +def test_detect_litellm_prefix(base_url: str, expected: str) -> None: + assert detect_litellm_prefix(base_url) == expected + + +def test_detect_litellm_prefix_none_uses_default() -> None: + assert detect_litellm_prefix(None) == "openai/" + + +def test_detect_litellm_prefix_custom_default() -> None: + assert detect_litellm_prefix("https://example.com", default="anthropic/") == ( + "anthropic/" + ) diff --git a/tests/unit/test_messages_litellm_dispatch.py b/tests/unit/test_messages_litellm_dispatch.py new file mode 100644 index 00000000..20482bb1 --- /dev/null +++ b/tests/unit/test_messages_litellm_dispatch.py @@ -0,0 +1,1301 @@ +"""Unit tests for the litellm-based /v1/messages dispatch path. + +Covers `BaseUpstreamProvider._forward_messages_via_litellm` (bearer-key), +`BaseUpstreamProvider._forward_x_cashu_messages_via_litellm` (x-cashu), +and the shortcuts wired into `forward_request` and +`forward_x_cashu_request`. +""" + +import json +import os +from typing import Any, AsyncIterator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from fastapi.responses import Response, StreamingResponse + +os.environ.setdefault("UPSTREAM_BASE_URL", "http://test") +os.environ.setdefault("UPSTREAM_API_KEY", "test") + +from routstr.core.db import ApiKey # noqa: E402 +from routstr.payment.cost_calculation import CostData # noqa: E402 +from routstr.payment.models import Architecture, Model, Pricing # noqa: E402 +from routstr.upstream.base import BaseUpstreamProvider # noqa: E402 + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +def _make_provider(supports_messages: bool = False) -> BaseUpstreamProvider: + provider = BaseUpstreamProvider(base_url="http://test", api_key="upstream-key") + if supports_messages: + provider.supports_anthropic_messages = True + return provider + + +def _make_key() -> ApiKey: + return ApiKey(hashed_key="abcdef0123" * 4, balance=1_000_000) + + +def _make_model( + model_id: str = "openai/gpt-4o-mini", + forwarded_model_id: str | None = None, +) -> Model: + return Model( + id=model_id, + name=model_id, + forwarded_model_id=forwarded_model_id if forwarded_model_id is not None else model_id, + created=0, + description="", + context_length=8192, + architecture=Architecture( + modality="text", + input_modalities=["text"], + output_modalities=["text"], + tokenizer="x", + instruct_type=None, + ), + pricing=Pricing( + prompt=0.0, + completion=0.0, + request=0.0, + image=0.0, + web_search=0.0, + internal_reasoning=0.0, + max_cost=0.0, + ), + sats_pricing=None, + per_request_limits=None, + top_provider=None, + ) + + +def _make_session() -> Any: + return MagicMock() + + +def _anthropic_request_body(*, stream: bool = False) -> bytes: + return json.dumps( + { + "model": "openai/gpt-4o-mini", + "messages": [{"role": "user", "content": "hi"}], + "max_tokens": 64, + "stream": stream, + } + ).encode() + + +def _anthropic_non_stream_response() -> dict: + return { + "id": "msg_123", + "type": "message", + "role": "assistant", + "model": "openai/gpt-4o-mini", + "content": [{"type": "text", "text": "hello!"}], + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 5, "output_tokens": 3}, + } + + +def _make_request(request_id: str | None = "req-test") -> Any: + request = MagicMock() + request.state.request_id = request_id + return request + + +# --------------------------------------------------------------------------- +# Helper / coercion +# --------------------------------------------------------------------------- + + +def test_coerce_litellm_payload_handles_dict() -> None: + out = BaseUpstreamProvider._coerce_litellm_payload({"a": 1}) + assert out == {"a": 1} + + +def test_coerce_litellm_payload_handles_pydantic_v2() -> None: + obj = MagicMock() + obj.model_dump.return_value = {"x": 42} + out = BaseUpstreamProvider._coerce_litellm_payload(obj) + assert out == {"x": 42} + + +def test_coerce_litellm_payload_rejects_unknown_types() -> None: + with pytest.raises(TypeError): + BaseUpstreamProvider._coerce_litellm_payload(object()) + + +def test_parse_sse_blocks_extracts_full_events() -> None: + buffer = ( + b"event: message_start\n" + b'data: {"type":"message_start","message":{"id":"m1"}}\n\n' + b"event: message_stop\n" + b'data: {"type":"message_stop"}\n\n' + ) + events, remaining = BaseUpstreamProvider._parse_sse_blocks(buffer) + assert remaining == b"" + assert events == [ + {"type": "message_start", "message": {"id": "m1"}}, + {"type": "message_stop"}, + ] + + +def test_parse_sse_blocks_preserves_partial_trailing_block() -> None: + buffer = b'data: {"type":"a"}\n\nevent: b\ndata: {"type":"b"}' + events, remaining = BaseUpstreamProvider._parse_sse_blocks(buffer) + assert events == [{"type": "a"}] + assert remaining == b'event: b\ndata: {"type":"b"}' + + +def test_parse_sse_blocks_skips_done_and_comments() -> None: + buffer = b': ping\n\ndata: [DONE]\n\ndata: {"type":"x"}\n\n' + events, remaining = BaseUpstreamProvider._parse_sse_blocks(buffer) + assert remaining == b"" + assert events == [{"type": "x"}] + + +def test_events_from_chunk_handles_bytes_chunks() -> None: + provider = _make_provider() + chunk = b'event: a\ndata: {"type":"a"}\n\nevent: b\ndata: {"type":"b"}' + events, buf = provider._events_from_chunk(chunk, b"") + assert events == [{"type": "a"}] + assert buf == b'event: b\ndata: {"type":"b"}' + + events, buf = provider._events_from_chunk(b"\n\n", buf) + assert events == [{"type": "b"}] + assert buf == b"" + + +def test_events_from_chunk_handles_str_chunks() -> None: + provider = _make_provider() + events, buf = provider._events_from_chunk( + 'event: a\ndata: {"type":"a"}\n\n', b"" + ) + assert events == [{"type": "a"}] + assert buf == b"" + + +def test_compute_refund_msat() -> None: + assert BaseUpstreamProvider._compute_refund(10_000, "msat", 4_000) == 6_000 + + +def test_compute_refund_sat_rounds_up_cost() -> None: + # 4001 msats → 5 sats (ceiling). 10 sats - 5 = 5 sats refund. + assert BaseUpstreamProvider._compute_refund(10, "sat", 4001) == 5 + + +def test_compute_refund_invalid_unit() -> None: + with pytest.raises(ValueError): + BaseUpstreamProvider._compute_refund(10, "btc", 100) + + +# --------------------------------------------------------------------------- +# Provider gating +# --------------------------------------------------------------------------- + + +def test_default_provider_does_not_support_anthropic_messages() -> None: + assert BaseUpstreamProvider.supports_anthropic_messages is False + # Class default is None so URL detection runs at dispatch time. + assert BaseUpstreamProvider.litellm_provider_prefix is None + + +def test_base_provider_resolves_prefix_from_base_url() -> None: + """The bug case: a custom row pointing at Fireworks must resolve to + `fireworks_ai/` instead of falling back to `openai/`.""" + fireworks = BaseUpstreamProvider( + base_url="https://api.fireworks.ai/inference/v1", api_key="sk-test" + ) + assert fireworks.get_litellm_provider_prefix() == "fireworks_ai/" + + groq = BaseUpstreamProvider( + base_url="https://api.groq.com/openai/v1", api_key="sk-test" + ) + assert groq.get_litellm_provider_prefix() == "groq/" + + xai = BaseUpstreamProvider( + base_url="https://api.x.ai/v1", api_key="sk-test" + ) + assert xai.get_litellm_provider_prefix() == "xai/" + + deepseek = BaseUpstreamProvider( + base_url="https://api.deepseek.com/v1", api_key="sk-test" + ) + assert deepseek.get_litellm_provider_prefix() == "deepseek/" + + unknown = BaseUpstreamProvider( + base_url="https://example.com/v1", api_key="sk-test" + ) + assert unknown.get_litellm_provider_prefix() == "openai/" + + +def test_subclass_prefix_wins_over_url_detection() -> None: + """A subclass override must beat URL detection. e.g. configuring an + AnthropicUpstreamProvider with a fireworks URL still produces + ``anthropic/`` (defensive: subclasses pin their backend on purpose).""" + from routstr.upstream.anthropic import AnthropicUpstreamProvider + + p = AnthropicUpstreamProvider(api_key="sk-test") + p.base_url = "https://api.fireworks.ai/inference/v1" + assert p.get_litellm_provider_prefix() == "anthropic/" + + +def test_anthropic_provider_supports_native_messages() -> None: + from routstr.upstream.anthropic import AnthropicUpstreamProvider + + assert AnthropicUpstreamProvider.supports_anthropic_messages is True + + +def test_openrouter_provider_supports_native_messages() -> None: + from routstr.upstream.openrouter import OpenRouterUpstreamProvider + + assert OpenRouterUpstreamProvider.supports_anthropic_messages is True + + +def test_provider_prefix_overrides() -> None: + from routstr.upstream.azure import AzureUpstreamProvider + from routstr.upstream.fireworks import FireworksUpstreamProvider + from routstr.upstream.gemini import GeminiUpstreamProvider + from routstr.upstream.groq import GroqUpstreamProvider + from routstr.upstream.ollama import OllamaUpstreamProvider + from routstr.upstream.perplexity import PerplexityUpstreamProvider + from routstr.upstream.xai import XAIUpstreamProvider + + assert GroqUpstreamProvider.litellm_provider_prefix == "groq/" + assert XAIUpstreamProvider.litellm_provider_prefix == "xai/" + assert FireworksUpstreamProvider.litellm_provider_prefix == "fireworks_ai/" + assert PerplexityUpstreamProvider.litellm_provider_prefix == "perplexity/" + assert GeminiUpstreamProvider.litellm_provider_prefix == "gemini/" + assert OllamaUpstreamProvider.litellm_provider_prefix == "ollama_chat/" + assert AzureUpstreamProvider.litellm_provider_prefix == "azure/" + + +# --------------------------------------------------------------------------- +# Bearer-key non-streaming +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_dispatch_strips_anthropic_only_fields_before_litellm() -> None: + """Regression: Anthropic-Messages-only fields like `output_config`, + `thinking`, `context_management`, and `cache_control` must be removed + from the request body before litellm dispatches to non-Anthropic + upstreams. Otherwise upstream returns 400 (unknown field). + """ + provider = _make_provider() + key = _make_key() + model = _make_model() + session = _make_session() + + body = json.dumps( + { + "model": "openai/gpt-4o-mini", + "messages": [{"role": "user", "content": "hi"}], + "max_tokens": 64, + "thinking": {"type": "adaptive"}, + "context_management": {"edits": []}, + "output_config": {"effort": "medium"}, + "cache_control": {"type": "ephemeral"}, + "mcp_servers": [], + "service_tier": "auto", + "anthropic_beta": "abc", + "anthropic_version": "2023-06-01", + } + ).encode() + + captured: dict[str, Any] = {} + + async def fake_acreate(**kwargs: Any) -> dict: + captured["kwargs"] = kwargs + return _anthropic_non_stream_response() + + with ( + patch( + "litellm.anthropic.messages.acreate", + new=AsyncMock(side_effect=fake_acreate), + ), + patch( + "routstr.upstream.base.adjust_payment_for_tokens", + new=AsyncMock(return_value={"total_msats": 0, "total_usd": 0.0}), + ), + ): + await provider._forward_messages_via_litellm( + request_body=body, + key=key, + session=session, + max_cost_for_model=10_000, + model_obj=model, + ) + + forwarded = captured["kwargs"] + for stripped in ( + "thinking", + "context_management", + "output_config", + "cache_control", + "mcp_servers", + "service_tier", + "anthropic_beta", + "anthropic_version", + ): + assert stripped not in forwarded, ( + f"Anthropic-only field {stripped!r} leaked through to litellm" + ) + # Core fields preserved + assert forwarded["max_tokens"] == 64 + assert forwarded["messages"] == [{"role": "user", "content": "hi"}] + + +@pytest.mark.asyncio +async def test_dispatch_uses_model_id_for_upstream_and_forwarded_for_client() -> None: + """Convention: `model.id` is the canonical upstream model name; + `forwarded_model_id` is the public alias echoed back to the client. + """ + provider = _make_provider() + key = _make_key() + # Upstream knows "gpt-4o-mini"; clients see public alias "gpt-5.4-test". + model = _make_model( + model_id="gpt-4o-mini", + forwarded_model_id="gpt-5.4-test", + ) + session = _make_session() + + captured: dict[str, Any] = {} + + async def fake_acreate(**kwargs: Any) -> dict: + captured["model"] = kwargs["model"] + resp = _anthropic_non_stream_response() + resp["model"] = "gpt-4o-mini" # upstream echoes its own id + return resp + + with ( + patch( + "litellm.anthropic.messages.acreate", + new=AsyncMock(side_effect=fake_acreate), + ), + patch( + "routstr.upstream.base.adjust_payment_for_tokens", + new=AsyncMock(return_value={"total_msats": 0, "total_usd": 0.0}), + ), + ): + result = await provider._forward_messages_via_litellm( + request_body=_anthropic_request_body(stream=False), + key=key, + session=session, + max_cost_for_model=10_000, + model_obj=model, + ) + + # Upstream call uses `model.id` with provider prefix + assert captured["model"] == "openai/gpt-4o-mini" + # Response to client echoes the public `forwarded_model_id` + assert isinstance(result, Response) + body = json.loads(result.body) + assert body["model"] == "gpt-5.4-test" + + +@pytest.mark.asyncio +async def test_non_streaming_dispatches_via_litellm_and_returns_anthropic_response() -> ( + None +): + provider = _make_provider() + key = _make_key() + model = _make_model() + session = _make_session() + body = _anthropic_request_body(stream=False) + + upstream_response = _anthropic_non_stream_response() + + async def fake_acreate(**kwargs: Any) -> dict: + assert kwargs["model"] == "openai/openai/gpt-4o-mini" + assert kwargs["api_base"] == "http://test" + assert kwargs["api_key"] == "upstream-key" + # The dispatcher always streams from upstream and aggregates back + # when the client wants a non-streaming response (sidesteps + # provider-specific non-streaming caps like Fireworks's max_tokens + # > 4096). When the mock returns a plain dict, the dispatcher + # detects the lack of __aiter__ and skips aggregation, so the + # client still gets the same Response shape. + assert kwargs["stream"] is True + assert kwargs["messages"] == [{"role": "user", "content": "hi"}] + assert kwargs["max_tokens"] == 64 + return upstream_response + + fake_cost = {"total_msats": 1234, "total_usd": 0.0001} + + with ( + patch( + "litellm.anthropic.messages.acreate", + new=AsyncMock(side_effect=fake_acreate), + ), + patch( + "routstr.upstream.base.adjust_payment_for_tokens", + new=AsyncMock(return_value=fake_cost), + ), + ): + result = await provider._forward_messages_via_litellm( + request_body=body, + key=key, + session=session, + max_cost_for_model=10_000, + model_obj=model, + ) + + assert isinstance(result, Response) + payload = json.loads(result.body) + assert payload["model"] == "openai/gpt-4o-mini" # mapped back to requested + assert payload["usage"]["input_tokens"] == 5 + assert payload["usage"]["output_tokens"] == 3 + assert payload["usage"]["cost"] == 0.0001 + assert payload["usage"]["cost_sats"] == 1 + + +# --------------------------------------------------------------------------- +# Bearer-key streaming +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_streaming_emits_sse_and_reconciles_cost_at_end() -> None: + provider = _make_provider() + key = _make_key() + model = _make_model() + session = _make_session() + body = _anthropic_request_body(stream=True) + + async def fake_chunks() -> AsyncIterator[dict]: + yield { + "type": "message_start", + "message": { + "id": "msg_1", + "type": "message", + "role": "assistant", + "model": "openai/gpt-4o-mini", + "content": [], + "usage": {"input_tokens": 5, "output_tokens": 0}, + }, + } + yield { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": ""}, + } + yield { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": "hi"}, + } + yield {"type": "content_block_stop", "index": 0} + yield { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"output_tokens": 7}, + } + yield {"type": "message_stop"} + + fake_cost = {"total_msats": 4321, "total_usd": 0.00015} + + captured_cost_call: dict[str, Any] = {} + + async def fake_adjust( + fresh_key: Any, combined_data: Any, sess: Any, max_cost: int + ) -> dict: + captured_cost_call["combined_data"] = combined_data + captured_cost_call["max_cost"] = max_cost + return fake_cost + + fake_session = MagicMock() + + class FakeSessionCtx: + async def __aenter__(self) -> Any: + return fake_session + + async def __aexit__(self, *args: Any) -> None: + return None + + fake_session.get = AsyncMock(return_value=key) + + with ( + patch( + "litellm.anthropic.messages.acreate", + new=AsyncMock(return_value=fake_chunks()), + ), + patch( + "routstr.upstream.base.adjust_payment_for_tokens", + new=AsyncMock(side_effect=fake_adjust), + ), + patch( + "routstr.upstream.base.create_session", + new=lambda: FakeSessionCtx(), + ), + ): + result = await provider._forward_messages_via_litellm( + request_body=body, + key=key, + session=session, + max_cost_for_model=10_000, + model_obj=model, + ) + + assert isinstance(result, StreamingResponse) + emitted: list[bytes] = [] + async for chunk in result.body_iterator: + if isinstance(chunk, bytes): + emitted.append(chunk) + elif isinstance(chunk, memoryview): + emitted.append(bytes(chunk)) + else: + emitted.append(chunk.encode()) + + joined = b"".join(emitted).decode() + assert "event: message_start" in joined + assert "event: content_block_delta" in joined + assert "event: message_delta" in joined + assert "event: cost" in joined + + combined = captured_cost_call["combined_data"] + assert combined["usage"]["input_tokens"] == 5 + assert combined["usage"]["output_tokens"] == 7 + assert combined["model"] == "openai/gpt-4o-mini" + + +@pytest.mark.asyncio +async def test_streaming_handles_iterator_yielding_raw_sse_bytes() -> None: + """Regression: litellm.anthropic.messages.acreate(stream=True) yields + already-SSE-encoded bytes in production. The stream loop must parse + SSE blocks (even split across chunks) and still reconcile cost. + """ + provider = _make_provider() + key = _make_key() + model = _make_model() + session = _make_session() + body = _anthropic_request_body(stream=True) + + async def fake_byte_chunks() -> AsyncIterator[bytes]: + yield ( + b"event: message_start\n" + b'data: {"type":"message_start","message":{"id":"m1",' + b'"model":"openai/gpt-4o-mini",' + b'"usage":{"input_tokens":3,"output_tokens":0}}}\n\n' + ) + yield b'event: message_delta\ndata: {"type":"message_delta",' + yield b'"delta":{},"usage":{"output_tokens":4}}\n\n' + yield b": keepalive\n\ndata: [DONE]\n\n" + yield b'event: message_stop\ndata: {"type":"message_stop"}\n\n' + + fake_cost = {"total_msats": 999, "total_usd": 0.0001} + captured: dict[str, Any] = {} + + async def fake_adjust( + fresh_key: Any, combined_data: Any, sess: Any, max_cost: int + ) -> dict: + captured["combined_data"] = combined_data + return fake_cost + + fake_session = MagicMock() + fake_session.get = AsyncMock(return_value=key) + + class FakeSessionCtx: + async def __aenter__(self) -> Any: + return fake_session + + async def __aexit__(self, *args: Any) -> None: + return None + + with ( + patch( + "litellm.anthropic.messages.acreate", + new=AsyncMock(return_value=fake_byte_chunks()), + ), + patch( + "routstr.upstream.base.adjust_payment_for_tokens", + new=AsyncMock(side_effect=fake_adjust), + ), + patch( + "routstr.upstream.base.create_session", + new=lambda: FakeSessionCtx(), + ), + ): + result = await provider._forward_messages_via_litellm( + request_body=body, + key=key, + session=session, + max_cost_for_model=10_000, + model_obj=model, + ) + + assert isinstance(result, StreamingResponse) + emitted: list[bytes] = [] + async for chunk in result.body_iterator: + if isinstance(chunk, bytes): + emitted.append(chunk) + elif isinstance(chunk, memoryview): + emitted.append(bytes(chunk)) + else: + emitted.append(chunk.encode()) + + joined = b"".join(emitted).decode() + assert "event: message_start" in joined + assert "event: message_delta" in joined + assert "event: message_stop" in joined + assert "event: cost" in joined + assert "[DONE]" not in joined + + combined = captured["combined_data"] + assert combined["usage"]["input_tokens"] == 3 + assert combined["usage"]["output_tokens"] == 4 + assert combined["model"] == "openai/gpt-4o-mini" + + +# --------------------------------------------------------------------------- +# x-cashu non-streaming +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_x_cashu_non_streaming_dispatches_and_refunds_overpaid_amount() -> None: + provider = _make_provider() + model = _make_model() + body = _anthropic_request_body(stream=False) + upstream_response = _anthropic_non_stream_response() + + cost = CostData( + base_msats=0, + input_msats=1_000_000, + output_msats=2_000_000, + total_msats=3_000_000, + input_tokens=5, + output_tokens=3, + ) + + with ( + patch( + "litellm.anthropic.messages.acreate", + new=AsyncMock(return_value=upstream_response), + ), + patch.object( + provider, + "get_x_cashu_cost", + new=AsyncMock(return_value=cost), + ), + patch.object( + provider, + "send_refund", + new=AsyncMock(return_value="cashuREFUND"), + ) as mock_refund, + ): + result = await provider._forward_x_cashu_messages_via_litellm( + request_body=body, + amount=10_000, # sats + unit="sat", + max_cost_for_model=10_000, + model_obj=model, + mint="https://mint.example", + payment_token_hash="hash123", + request_id="req-1", + ) + + assert isinstance(result, Response) + assert result.headers.get("X-Cashu") == "cashuREFUND" + payload = json.loads(result.body) + # 3_000_000 msats → 3000 sats. Refund = 10_000 - 3000 = 7000. + mock_refund.assert_awaited_once() + refund_call = mock_refund.await_args + assert refund_call is not None + assert refund_call.args[0] == 7_000 + assert refund_call.args[1] == "sat" + # cost_sats injected into usage + assert payload["usage"]["cost_sats"] == 3_000 + + +@pytest.mark.asyncio +async def test_x_cashu_non_streaming_no_refund_when_fully_consumed() -> None: + provider = _make_provider() + model = _make_model() + body = _anthropic_request_body(stream=False) + upstream_response = _anthropic_non_stream_response() + + cost = CostData( + base_msats=0, + input_msats=10_000_000, + output_msats=0, + total_msats=10_000_000, + input_tokens=5, + output_tokens=3, + ) + + with ( + patch( + "litellm.anthropic.messages.acreate", + new=AsyncMock(return_value=upstream_response), + ), + patch.object( + provider, + "get_x_cashu_cost", + new=AsyncMock(return_value=cost), + ), + patch.object( + provider, + "send_refund", + new=AsyncMock(return_value="cashuNOPE"), + ) as mock_refund, + ): + result = await provider._forward_x_cashu_messages_via_litellm( + request_body=body, + amount=10_000, + unit="sat", + max_cost_for_model=10_000, + model_obj=model, + ) + + assert isinstance(result, Response) + assert "X-Cashu" not in result.headers + mock_refund.assert_not_awaited() + + +# --------------------------------------------------------------------------- +# x-cashu streaming +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_x_cashu_streaming_replays_events_and_sets_refund_header() -> None: + provider = _make_provider() + model = _make_model() + body = _anthropic_request_body(stream=True) + + async def fake_chunks() -> AsyncIterator[dict]: + yield { + "type": "message_start", + "message": { + "id": "msg_1", + "model": "openai/gpt-4o-mini", + "usage": {"input_tokens": 5, "output_tokens": 0}, + }, + } + yield { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": "hi"}, + } + yield { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"output_tokens": 7}, + } + yield {"type": "message_stop"} + + cost = CostData( + base_msats=0, + input_msats=1_000_000, + output_msats=500_000, + total_msats=1_500_000, + input_tokens=5, + output_tokens=7, + ) + + with ( + patch( + "litellm.anthropic.messages.acreate", + new=AsyncMock(return_value=fake_chunks()), + ), + patch.object( + provider, + "get_x_cashu_cost", + new=AsyncMock(return_value=cost), + ), + patch.object( + provider, + "send_refund", + new=AsyncMock(return_value="cashuSTREAM"), + ) as mock_refund, + ): + result = await provider._forward_x_cashu_messages_via_litellm( + request_body=body, + amount=5_000, + unit="sat", + max_cost_for_model=10_000, + model_obj=model, + ) + + assert isinstance(result, StreamingResponse) + assert result.headers.get("X-Cashu") == "cashuSTREAM" + # 1_500_000 msats → 1500 sats. Refund = 5000 - 1500 = 3500. + mock_refund.assert_awaited_once() + refund_call = mock_refund.await_args + assert refund_call is not None + assert refund_call.args[0] == 3_500 + + emitted: list[bytes] = [] + async for chunk in result.body_iterator: + if isinstance(chunk, bytes): + emitted.append(chunk) + elif isinstance(chunk, memoryview): + emitted.append(bytes(chunk)) + else: + emitted.append(chunk.encode()) + joined = b"".join(emitted).decode() + assert "event: message_start" in joined + assert "event: message_delta" in joined + assert "event: message_stop" in joined + + +# --------------------------------------------------------------------------- +# forward_request gating +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_forward_request_routes_messages_via_litellm() -> None: + provider = _make_provider() + key = _make_key() + model = _make_model() + session = _make_session() + request = _make_request() + + sentinel = Response(content=b'{"ok": true}', media_type="application/json") + with patch.object( + provider, + "_forward_messages_via_litellm", + new=AsyncMock(return_value=sentinel), + ) as mock_helper: + result = await provider.forward_request( + request=request, + path="messages", + headers={}, + request_body=_anthropic_request_body(), + key=key, + max_cost_for_model=10_000, + session=session, + model_obj=model, + ) + + mock_helper.assert_awaited_once() + assert result is sentinel + + +@pytest.mark.asyncio +async def test_forward_request_skips_litellm_when_provider_supports_messages() -> None: + provider = _make_provider(supports_messages=True) + key = _make_key() + model = _make_model() + session = _make_session() + request = _make_request() + + with patch.object( + provider, + "_forward_messages_via_litellm", + new=AsyncMock(side_effect=AssertionError("should not be called")), + ): + with patch.object( + provider, + "prepare_request_body", + side_effect=RuntimeError("stop here"), + ): + with pytest.raises(RuntimeError, match="stop here"): + await provider.forward_request( + request=request, + path="messages", + headers={}, + request_body=_anthropic_request_body(), + key=key, + max_cost_for_model=10_000, + session=session, + model_obj=model, + ) + + +@pytest.mark.asyncio +async def test_forward_request_skips_litellm_for_count_tokens() -> None: + provider = _make_provider() + key = _make_key() + model = _make_model() + session = _make_session() + request = _make_request() + + with patch.object( + provider, + "_forward_messages_via_litellm", + new=AsyncMock(side_effect=AssertionError("should not be called")), + ): + with patch.object( + provider, + "prepare_request_body", + side_effect=RuntimeError("stop here"), + ): + with pytest.raises(RuntimeError, match="stop here"): + await provider.forward_request( + request=request, + path="messages/count_tokens", + headers={}, + request_body=_anthropic_request_body(), + key=key, + max_cost_for_model=10_000, + session=session, + model_obj=model, + ) + + +# --------------------------------------------------------------------------- +# forward_x_cashu_request gating +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_forward_x_cashu_request_routes_messages_via_litellm() -> None: + provider = _make_provider() + model = _make_model() + request = _make_request() + request.body = AsyncMock(return_value=_anthropic_request_body()) + + sentinel = Response(content=b'{"ok": true}', media_type="application/json") + + with patch.object( + provider, + "_forward_x_cashu_messages_via_litellm", + new=AsyncMock(return_value=sentinel), + ) as mock_helper: + result = await provider.forward_x_cashu_request( + request=request, + path="v1/messages", + headers={}, + amount=5_000, + unit="sat", + max_cost_for_model=10_000, + model_obj=model, + mint="https://mint", + payment_token_hash="h", + ) + + mock_helper.assert_awaited_once() + helper_call = mock_helper.await_args + assert helper_call is not None + kwargs = helper_call.kwargs + assert kwargs["amount"] == 5_000 + assert kwargs["unit"] == "sat" + assert kwargs["mint"] == "https://mint" + assert kwargs["request_id"] == "req-test" + assert result is sentinel + + +@pytest.mark.asyncio +async def test_forward_x_cashu_request_skips_litellm_when_native_messages() -> None: + provider = _make_provider(supports_messages=True) + model = _make_model() + request = _make_request() + request.body = AsyncMock(return_value=_anthropic_request_body()) + + with patch.object( + provider, + "_forward_x_cashu_messages_via_litellm", + new=AsyncMock(side_effect=AssertionError("should not be called")), + ): + with patch.object( + provider, + "prepare_request_body", + side_effect=RuntimeError("stop here"), + ): + with pytest.raises(RuntimeError, match="stop here"): + await provider.forward_x_cashu_request( + request=request, + path="v1/messages", + headers={}, + amount=5_000, + unit="sat", + max_cost_for_model=10_000, + model_obj=model, + ) + + +@pytest.mark.asyncio +async def test_forward_x_cashu_request_skips_litellm_for_count_tokens() -> None: + provider = _make_provider() + model = _make_model() + request = _make_request() + request.body = AsyncMock(return_value=_anthropic_request_body()) + + with patch.object( + provider, + "_forward_x_cashu_messages_via_litellm", + new=AsyncMock(side_effect=AssertionError("should not be called")), + ): + with patch.object( + provider, + "prepare_request_body", + side_effect=RuntimeError("stop here"), + ): + with pytest.raises(RuntimeError, match="stop here"): + await provider.forward_x_cashu_request( + request=request, + path="v1/messages/count_tokens", + headers={}, + amount=5_000, + unit="sat", + max_cost_for_model=10_000, + model_obj=model, + ) + + +# --------------------------------------------------------------------------- +# Upstream-always-streams + aggregate-on-non-streaming +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_aggregator_assembles_text_message_from_events() -> None: + """When the client wants a non-streaming response, the dispatcher + drains the upstream stream and assembles a single Anthropic Message + dict. This is what makes Fireworks (which rejects max_tokens > 4096 + unless stream=true) work transparently for non-streaming clients.""" + provider = _make_provider() + + events: list[dict] = [ + { + "type": "message_start", + "message": { + "id": "msg_aggregated", + "type": "message", + "role": "assistant", + "model": "accounts/fireworks/models/glm-5", + "content": [], + "stop_reason": None, + "stop_sequence": None, + "usage": {"input_tokens": 10, "output_tokens": 0}, + }, + }, + { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": ""}, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": "Hello"}, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": ", world!"}, + }, + {"type": "content_block_stop", "index": 0}, + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn", "stop_sequence": None}, + "usage": {"output_tokens": 42}, + }, + {"type": "message_stop"}, + ] + + async def event_iter() -> AsyncIterator[dict]: + for event in events: + yield event + + message = await provider._aggregate_anthropic_events_to_message(event_iter()) + + assert message["id"] == "msg_aggregated" + assert message["role"] == "assistant" + assert message["stop_reason"] == "end_turn" + assert message["stop_sequence"] is None + assert message["content"] == [{"type": "text", "text": "Hello, world!"}] + assert message["usage"]["input_tokens"] == 10 + assert message["usage"]["output_tokens"] == 42 + + +@pytest.mark.asyncio +async def test_aggregator_parses_tool_use_input_json_delta() -> None: + """tool_use blocks split their `input` across multiple + `input_json_delta` chunks. The aggregator must concatenate and parse + them into a single JSON object.""" + provider = _make_provider() + + events: list[dict] = [ + { + "type": "message_start", + "message": { + "id": "msg_tool", + "type": "message", + "role": "assistant", + "model": "test-model", + "content": [], + "usage": {"input_tokens": 5, "output_tokens": 0}, + }, + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "tool_use", + "id": "toolu_1", + "name": "calc", + "input": {}, + }, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "input_json_delta", "partial_json": '{"x":'}, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "input_json_delta", "partial_json": ' 7}'}, + }, + {"type": "content_block_stop", "index": 0}, + { + "type": "message_delta", + "delta": {"stop_reason": "tool_use"}, + "usage": {"output_tokens": 3}, + }, + {"type": "message_stop"}, + ] + + async def event_iter() -> AsyncIterator[dict]: + for event in events: + yield event + + message = await provider._aggregate_anthropic_events_to_message(event_iter()) + assert message["stop_reason"] == "tool_use" + assert message["content"][0]["type"] == "tool_use" + assert message["content"][0]["input"] == {"x": 7} + + +@pytest.mark.asyncio +async def test_aggregator_parses_sse_byte_chunks() -> None: + """litellm's anthropic adapter often yields raw SSE byte chunks. The + aggregator must parse the SSE wire format, not just typed dicts.""" + provider = _make_provider() + + sse = ( + b'event: message_start\n' + b'data: {"type":"message_start","message":{"id":"m1","type":"message",' + b'"role":"assistant","model":"x","content":[],"usage":{"input_tokens":1,"output_tokens":0}}}\n\n' + b'event: content_block_start\n' + b'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n' + b'event: content_block_delta\n' + b'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}\n\n' + b'event: content_block_stop\n' + b'data: {"type":"content_block_stop","index":0}\n\n' + b'event: message_delta\n' + b'data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":2}}\n\n' + b'event: message_stop\n' + b'data: {"type":"message_stop"}\n\n' + ) + + async def chunk_iter() -> AsyncIterator[bytes]: + # Split mid-event to exercise the partial-buffer path. + yield sse[:100] + yield sse[100:] + + message = await provider._aggregate_anthropic_events_to_message(chunk_iter()) + assert message["content"] == [{"type": "text", "text": "hi"}] + assert message["stop_reason"] == "end_turn" + assert message["usage"]["output_tokens"] == 2 + + +@pytest.mark.asyncio +async def test_dispatch_always_streams_upstream_and_aggregates_for_non_streaming_client( # noqa: E501 +) -> None: + """End-to-end: client says stream=false; dispatcher upstream-streams + and returns an aggregated Anthropic Message dict.""" + provider = _make_provider() + model = _make_model() + body = _anthropic_request_body(stream=False) + + events: list[dict] = [ + { + "type": "message_start", + "message": { + "id": "msg_e2e", + "type": "message", + "role": "assistant", + "model": "openai/gpt-4o-mini", + "content": [], + "usage": {"input_tokens": 4, "output_tokens": 0}, + }, + }, + { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": ""}, + }, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": "ok"}, + }, + {"type": "content_block_stop", "index": 0}, + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn"}, + "usage": {"output_tokens": 1}, + }, + {"type": "message_stop"}, + ] + + async def event_iter() -> AsyncIterator[dict]: + for event in events: + yield event + + captured_kwargs: dict[str, Any] = {} + + async def fake_acreate(**kwargs: Any) -> AsyncIterator[dict]: + captured_kwargs.update(kwargs) + return event_iter() + + with patch( + "litellm.anthropic.messages.acreate", + new=AsyncMock(side_effect=fake_acreate), + ): + client_stream, result, requested_model = ( + await provider._dispatch_anthropic_messages( + request_body=body, + model_obj=model, + ) + ) + + # Upstream was streamed regardless of client preference. + assert captured_kwargs["stream"] is True + # Client wanted non-streaming → aggregator ran. + assert client_stream is False + assert isinstance(result, dict) + assert result["content"] == [{"type": "text", "text": "ok"}] + assert result["stop_reason"] == "end_turn" + assert requested_model == "openai/gpt-4o-mini" + + +@pytest.mark.asyncio +async def test_dispatch_uses_url_detected_prefix_for_fireworks_custom_row() -> None: + """The original bug: a custom-typed row pointing at Fireworks must + dispatch with `fireworks_ai/`, not `openai/`.""" + provider = BaseUpstreamProvider( + base_url="https://api.fireworks.ai/inference/v1", + api_key="fw-key", + ) + model = _make_model(model_id="accounts/fireworks/models/glm-5") + body = _anthropic_request_body(stream=True) + + captured_kwargs: dict[str, Any] = {} + + async def empty_iter() -> AsyncIterator[dict]: + if False: + yield {} + + async def fake_acreate(**kwargs: Any) -> AsyncIterator[dict]: + captured_kwargs.update(kwargs) + return empty_iter() + + with patch( + "litellm.anthropic.messages.acreate", + new=AsyncMock(side_effect=fake_acreate), + ): + await provider._dispatch_anthropic_messages( + request_body=body, model_obj=model + ) + + assert captured_kwargs["model"] == ( + "fireworks_ai/accounts/fireworks/models/glm-5" + ) + assert captured_kwargs["api_base"] == "https://api.fireworks.ai/inference/v1" diff --git a/tests/unit/test_upstream_gemini.py b/tests/unit/test_upstream_gemini.py new file mode 100644 index 00000000..13683887 --- /dev/null +++ b/tests/unit/test_upstream_gemini.py @@ -0,0 +1,313 @@ +"""Unit tests for the Gemini /v1/messages dispatch path. + +The Gemini upstream needs special handling because its OpenAI-compat +surface rejects inbound ``functionCall`` parts that lack a +``thought_signature``. We bypass litellm + openai SDK at the wire layer +(see ``routstr/upstream/gemini_messages.py``) so we can inject Google's +documented dummy signature (``"skip_thought_signature_validator"``). + +These tests cover the two pure helpers that drive the dispatcher: + +* ``inject_thought_signatures`` — request-side injection +* ``_openai_chunks_to_anthropic_events`` — response-side translator +""" + +from __future__ import annotations + +import json +from collections.abc import AsyncGenerator +from typing import Any + +import pytest + +from routstr.upstream.gemini_messages import ( + DUMMY_THOUGHT_SIGNATURE, + _openai_chunks_to_anthropic_events, + inject_thought_signatures, +) + +# --------------------------------------------------------------------------- +# inject_thought_signatures +# --------------------------------------------------------------------------- + + +def test_inject_thought_signatures_adds_dummy_to_each_tool_call() -> None: + messages: list[dict[str, Any]] = [ + {"role": "user", "content": "do thing"}, + { + "role": "assistant", + "tool_calls": [ + { + "id": "toolu_1", + "type": "function", + "function": {"name": "Bash", "arguments": "{}"}, + }, + { + "id": "toolu_2", + "type": "function", + "function": {"name": "Read", "arguments": "{}"}, + }, + ], + }, + ] + + inject_thought_signatures(messages) + + for tc in messages[1]["tool_calls"]: + assert ( + tc["extra_content"]["google"]["thought_signature"] + == DUMMY_THOUGHT_SIGNATURE + ) + + +def test_inject_thought_signatures_preserves_existing_signature() -> None: + """Don't clobber a real signature that came back from a prior turn.""" + messages: list[dict[str, Any]] = [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "tc1", + "type": "function", + "function": {"name": "fn", "arguments": "{}"}, + "extra_content": { + "google": {"thought_signature": "real-signature"} + }, + } + ], + } + ] + + inject_thought_signatures(messages) + + assert ( + messages[0]["tool_calls"][0]["extra_content"]["google"][ + "thought_signature" + ] + == "real-signature" + ) + + +def test_inject_thought_signatures_skips_messages_without_tool_calls() -> None: + messages: list[dict[str, Any]] = [ + {"role": "user", "content": "hi"}, + {"role": "assistant", "content": "hello"}, + ] + + inject_thought_signatures(messages) + + for m in messages: + assert "extra_content" not in m + + +def test_inject_thought_signatures_handles_malformed_extra_content() -> None: + """If a caller already set ``extra_content`` to a non-dict (defensive), + we replace it instead of crashing.""" + messages: list[dict[str, Any]] = [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "tc", + "type": "function", + "function": {"name": "fn", "arguments": "{}"}, + "extra_content": "garbage", + } + ], + } + ] + + inject_thought_signatures(messages) + + extra = messages[0]["tool_calls"][0]["extra_content"] + assert isinstance(extra, dict) + assert extra["google"]["thought_signature"] == DUMMY_THOUGHT_SIGNATURE + + +# --------------------------------------------------------------------------- +# _openai_chunks_to_anthropic_events +# --------------------------------------------------------------------------- + + +async def _lines(*chunks: dict | str) -> AsyncGenerator[str, None]: + """Helper to wrap chunk dicts as SSE-style ``data:`` lines.""" + for c in chunks: + if isinstance(c, dict): + yield f"data: {json.dumps(c)}" + else: + yield c + + +def _parse_anthropic_sse(blocks: list[bytes]) -> list[dict]: + """Flatten a list of Anthropic SSE byte chunks into event dicts.""" + events: list[dict] = [] + for blob in blocks: + text = blob.decode() + for entry in text.split("\n\n"): + for line in entry.splitlines(): + if line.startswith("data:"): + events.append(json.loads(line[5:].lstrip())) + return events + + +@pytest.mark.asyncio +async def test_translator_emits_text_only_response() -> None: + """Plain text response: message_start → content_block_* (text) → + message_delta(end_turn) → message_stop.""" + chunks: list[dict] = [ + { + "id": "chatcmpl-1", + "model": "gemini-2.5-flash", + "choices": [{"index": 0, "delta": {"role": "assistant"}}], + }, + {"choices": [{"delta": {"content": "Hello"}}]}, + {"choices": [{"delta": {"content": ", world"}}]}, + { + "choices": [{"delta": {}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 5, "completion_tokens": 7}, + }, + ] + + out = [] + async for event_bytes in _openai_chunks_to_anthropic_events( + _lines(*chunks), requested_model="gemini-2.5-flash" + ): + out.append(event_bytes) + + events = _parse_anthropic_sse(out) + types = [e["type"] for e in events] + assert types == [ + "message_start", + "content_block_start", + "content_block_delta", + "content_block_delta", + "content_block_stop", + "message_delta", + "message_stop", + ] + # Text deltas concatenate to "Hello, world". + text_deltas = [ + e["delta"]["text"] + for e in events + if e["type"] == "content_block_delta" + ] + assert "".join(text_deltas) == "Hello, world" + # Stop reason was mapped from openai's "stop". + msg_delta = next(e for e in events if e["type"] == "message_delta") + assert msg_delta["delta"]["stop_reason"] == "end_turn" + assert msg_delta["usage"]["input_tokens"] == 5 + assert msg_delta["usage"]["output_tokens"] == 7 + + +@pytest.mark.asyncio +async def test_translator_emits_tool_use_block() -> None: + """tool_calls split across deltas → tool_use content block with + accumulated input_json_delta and stop_reason='tool_use'.""" + chunks: list[dict] = [ + { + "id": "chatcmpl-2", + "model": "gemini-2.5-flash", + "choices": [{"delta": {"role": "assistant"}}], + }, + { + "choices": [ + { + "delta": { + "tool_calls": [ + { + "index": 0, + "id": "call-abc", + "type": "function", + "function": { + "name": "Bash", + "arguments": '{"cmd":', + }, + } + ] + } + } + ] + }, + { + "choices": [ + { + "delta": { + "tool_calls": [ + { + "index": 0, + "function": {"arguments": ' "ls"}'}, + } + ] + } + } + ] + }, + {"choices": [{"delta": {}, "finish_reason": "tool_calls"}]}, + ] + + out = [] + async for event_bytes in _openai_chunks_to_anthropic_events( + _lines(*chunks), requested_model="gemini-2.5-flash" + ): + out.append(event_bytes) + + events = _parse_anthropic_sse(out) + types = [e["type"] for e in events] + assert types == [ + "message_start", + "content_block_start", + "content_block_delta", + "content_block_delta", + "content_block_stop", + "message_delta", + "message_stop", + ] + # Tool use block was opened with the right name. + cb_start = next(e for e in events if e["type"] == "content_block_start") + assert cb_start["content_block"]["type"] == "tool_use" + assert cb_start["content_block"]["name"] == "Bash" + assert cb_start["content_block"]["id"] == "call-abc" + # Argument deltas were forwarded as input_json_delta partials. + deltas = [e for e in events if e["type"] == "content_block_delta"] + assert all(d["delta"]["type"] == "input_json_delta" for d in deltas) + assert "".join(d["delta"]["partial_json"] for d in deltas) == ( + '{"cmd": "ls"}' + ) + # tool_calls finish_reason → tool_use stop_reason. + msg_delta = next(e for e in events if e["type"] == "message_delta") + assert msg_delta["delta"]["stop_reason"] == "tool_use" + + +@pytest.mark.asyncio +async def test_translator_handles_done_sentinel_and_blank_lines() -> None: + """Spec edge cases from openai SSE: ``data: [DONE]``, blank lines, + invalid JSON. Translator should skip them gracefully.""" + chunks: list[dict | str] = [ + { + "id": "x", + "model": "m", + "choices": [{"delta": {"role": "assistant"}}], + }, + {"choices": [{"delta": {"content": "ok"}}]}, + "", + ": comment", + "data: not-json", + "data: [DONE]", + {"choices": [{"delta": {}, "finish_reason": "stop"}]}, + ] + + out = [] + async for event_bytes in _openai_chunks_to_anthropic_events( + _lines(*chunks), requested_model=None + ): + out.append(event_bytes) + + events = _parse_anthropic_sse(out) + assert events[0]["type"] == "message_start" + assert events[-1]["type"] == "message_stop" + text = "".join( + e["delta"]["text"] + for e in events + if e["type"] == "content_block_delta" + ) + assert text == "ok" diff --git a/uv.lock b/uv.lock index 2be12f91..900f9cfe 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] [[package]] name = "aiohappyeyeballs" @@ -118,6 +122,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/62/96b5217b742805236614f05904541000f55422a6060a90d7fd4ce26c172d/alembic-1.16.4-py3-none-any.whl", hash = "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d", size = 247026, upload-time = "2025-07-10T16:17:21.845Z" }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "anyio" version = "4.9.0" @@ -302,7 +315,7 @@ wheels = [ [[package]] name = "cashu" -version = "0.17.0" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiosqlite" }, @@ -319,6 +332,7 @@ dependencies = [ { name = "environs" }, { name = "fastapi" }, { name = "googleapis-common-protos" }, + { name = "greenlet" }, { name = "grpcio" }, { name = "grpcio-tools" }, { name = "h11" }, @@ -330,9 +344,9 @@ dependencies = [ { name = "mypy-protobuf" }, { name = "pycryptodomex" }, { name = "pydantic" }, + { name = "pydantic-settings" }, { name = "pyjwt" }, { name = "redis" }, - { name = "secp256k1" }, { name = "setuptools" }, { name = "slowapi" }, { name = "sqlalchemy", extra = ["asyncio"] }, @@ -343,9 +357,9 @@ dependencies = [ { name = "wheel" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/84/b60d5a007b48d8b3bc795f3bbe6f47a56cd42e7af828c0082e94eb9ce949/cashu-0.17.0.tar.gz", hash = "sha256:a37ce0630b5e1a2b938ae10e4a252f4ed089224d7a186fe2f4548d5f2be17831", size = 6992332, upload-time = "2025-05-19T10:47:59.209Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/d0/31f3319b36707f754b254a46e39b055b08b9d0c7bdd49f6232295fcd786f/cashu-0.20.0.tar.gz", hash = "sha256:259b061dc1786129ce02d88401ca38a91311b08c119c769b4f64653d892b204a", size = 7004615, upload-time = "2026-03-31T16:44:28.493Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/07/f4564f42b4b579d4a476b4280f082f3e861f684945842630b73ee0aa0309/cashu-0.17.0-py3-none-any.whl", hash = "sha256:23d64de4d076dc5cbab149d11b3e662b2a7adb7aaf8416e4c445e7acf216b37d", size = 7062992, upload-time = "2025-05-19T10:47:56.753Z" }, + { url = "https://files.pythonhosted.org/packages/26/0c/3f736d3a6f2f88631d6f1cc42976e5ad7af3b97b7ff29b142f6d2829ae56/cashu-0.20.0-py3-none-any.whl", hash = "sha256:7148b13fcb8da63502775c5c627b56f74480dcaff2321cab65c1cfd2cd387dc2", size = 7087085, upload-time = "2026-03-31T16:44:30.544Z" }, ] [[package]] @@ -432,6 +446,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + [[package]] name = "click" version = "8.2.1" @@ -705,6 +808,67 @@ standard = [ { name = "uvicorn", extra = ["standard"] }, ] +[[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/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, + { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, + { 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.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + [[package]] name = "frozenlist" version = "1.7.0" @@ -782,6 +946,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] +[[package]] +name = "fsspec" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.70.0" @@ -934,6 +1107,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" }, ] +[[package]] +name = "hf-xet" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload-time = "2026-03-31T22:39:49.618Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" }, + { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, +] + [[package]] name = "httpcore" version = "1.0.8" @@ -997,6 +1202,26 @@ socks = [ { name = "socksio" }, ] +[[package]] +name = "huggingface-hub" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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 = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/52/1b54cb569509c725a32c1315261ac9fd0e6b91bbbf74d86fca10d3376164/huggingface_hub-1.12.0.tar.gz", hash = "sha256:7c3fe85e24b652334e5d456d7a812cd9a071e75630fac4365d9165ab5e4a34b6", size = 763091, upload-time = "2026-04-24T13:32:08.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/2b/ef03ddb96bd1123503c2bd6932001020292deea649e9bf4caa2cb65a85bf/huggingface_hub-1.12.0-py3-none-any.whl", hash = "sha256:d74939969585ee35748bd66de09baf84099d461bda7287cd9043bfb99b0e424d", size = 646806, upload-time = "2026-04-24T13:32:06.717Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -1099,6 +1324,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "limits" version = "5.5.0" @@ -1113,6 +1365,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/68/ee314018c28da75ece5a639898b4745bd0687c0487fc465811f0c4b9cd44/limits-5.5.0-py3-none-any.whl", hash = "sha256:57217d01ffa5114f7e233d1f5e5bdc6fe60c9b24ade387bf4d5e83c5cf929bae", size = 60948, upload-time = "2025-08-05T18:23:53.335Z" }, ] +[[package]] +name = "litellm" +version = "1.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/92/6ce9737554994ca8e536e5f4f6a87cc7c4774b656c9eb9add071caf7d54b/litellm-1.83.0.tar.gz", hash = "sha256:860bebc76c4bb27b4cf90b4a77acd66dba25aced37e3db98750de8a1766bfb7a", size = 17333062, upload-time = "2026-03-31T05:08:25.331Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/2c/a670cc050fcd6f45c6199eb99e259c73aea92edba8d5c2fc1b3686d36217/litellm-1.83.0-py3-none-any.whl", hash = "sha256:88c536d339248f3987571493015784671ba3f193a328e1ea6780dbebaa2094a8", size = 15610306, upload-time = "2026-03-31T05:08:21.987Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -1387,7 +1662,7 @@ wheels = [ [[package]] name = "openai" -version = "1.98.0" +version = "2.32.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1399,9 +1674,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/9d/52eadb15c92802711d6b6cf00df3a6d0d18b588f4c5ba5ff210c6419fc03/openai-1.98.0.tar.gz", hash = "sha256:3ee0fcc50ae95267fd22bd1ad095ba5402098f3df2162592e68109999f685427", size = 496695, upload-time = "2025-07-30T12:48:03.701Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/59/bdcc6b759b8c42dd73afaf5bf8f902c04b37987a5514dbc1c64dba390fef/openai-2.32.0.tar.gz", hash = "sha256:c54b27a9e4cb8d51f0dd94972ffd1a04437efeb259a9e60d8922b8bd26fe55e0", size = 693286, upload-time = "2026-04-15T22:28:19.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/fe/f64631075b3d63a613c0d8ab761d5941631a470f6fa87eaaee1aa2b4ec0c/openai-1.98.0-py3-none-any.whl", hash = "sha256:b99b794ef92196829120e2df37647722104772d2a74d08305df9ced5f26eae34", size = 767713, upload-time = "2025-07-30T12:48:01.264Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c1/d6e64ccd0536bf616556f0cad2b6d94a8125f508d25cfd814b1d2db4e2f1/openai-2.32.0-py3-none-any.whl", hash = "sha256:4dcc9badeb4bf54ad0d187453742f290226d30150890b7890711bda4f32f192f", size = 1162570, upload-time = "2026-04-15T22:28:17.714Z" }, ] [[package]] @@ -1670,35 +1945,133 @@ wheels = [ [[package]] name = "pydantic" -version = "1.10.22" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/57/5996c63f0deec09e9e901a2b838247c97c6844999562eac4e435bcb83938/pydantic-1.10.22.tar.gz", hash = "sha256:ee1006cebd43a8e7158fb7190bb8f4e2da9649719bff65d0c287282ec38dec6d", size = 356771, upload-time = "2025-04-24T13:38:43.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/03/e435ed85a9abda29e3fbdb49c572fe4131a68c6daf3855a01eebda9e1b27/pydantic-1.10.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e530a8da353f791ad89e701c35787418605d35085f4bdda51b416946070e938", size = 2845682, upload-time = "2025-04-24T13:37:10.142Z" }, - { url = "https://files.pythonhosted.org/packages/72/ea/4a625035672f6c06d3f1c7e33aa0af6bf1929991e27017e98b9c2064ae0b/pydantic-1.10.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:654322b85642e9439d7de4c83cb4084ddd513df7ff8706005dada43b34544946", size = 2553286, upload-time = "2025-04-24T13:37:11.946Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f0/424ad837746e69e9f061ba9be68c2a97aef7376d1911692904d8efbcd322/pydantic-1.10.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8bece75bd1b9fc1c32b57a32831517943b1159ba18b4ba32c0d431d76a120ae", size = 3141232, upload-time = "2025-04-24T13:37:14.394Z" }, - { url = "https://files.pythonhosted.org/packages/14/67/4979c19e8cfd092085a292485e0b42d74e4eeefbb8cd726aa8ba38d06294/pydantic-1.10.22-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eccb58767f13c6963dcf96d02cb8723ebb98b16692030803ac075d2439c07b0f", size = 3214272, upload-time = "2025-04-24T13:37:16.201Z" }, - { url = "https://files.pythonhosted.org/packages/1a/04/32339ce43e97519d19e7759902515c750edbf4832a13063a4ab157f83f42/pydantic-1.10.22-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7778e6200ff8ed5f7052c1516617423d22517ad36cc7a3aedd51428168e3e5e8", size = 3321646, upload-time = "2025-04-24T13:37:19.086Z" }, - { url = "https://files.pythonhosted.org/packages/92/35/dffc1b29cb7198aadab68d75447191e59bdbc1f1d2d51826c9a4460d372f/pydantic-1.10.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffe02767d27c39af9ca7dc7cd479c00dda6346bb62ffc89e306f665108317a2", size = 3244258, upload-time = "2025-04-24T13:37:20.929Z" }, - { url = "https://files.pythonhosted.org/packages/11/c5/c4ce6ebe7f528a879441eabd2c6dd9e2e4c54f320a8c9344ba93b3aa8701/pydantic-1.10.22-cp311-cp311-win_amd64.whl", hash = "sha256:23bc19c55427091b8e589bc08f635ab90005f2dc99518f1233386f46462c550a", size = 2309702, upload-time = "2025-04-24T13:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/f6/a3/ec66239ed7c9e90edfb85b23b6b18eb290ed7aa05f54837cdcb6a14faa98/pydantic-1.10.22-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:92d0f97828a075a71d9efc65cf75db5f149b4d79a38c89648a63d2932894d8c9", size = 2794865, upload-time = "2025-04-24T13:37:25.087Z" }, - { url = "https://files.pythonhosted.org/packages/49/6a/99cf3fee612d93210c85f45a161e98c1c5b45b6dcadb21c9f1f838fa9e28/pydantic-1.10.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af5a2811b6b95b58b829aeac5996d465a5f0c7ed84bd871d603cf8646edf6ff", size = 2534212, upload-time = "2025-04-24T13:37:26.848Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e6/0f8882775cd9a60b221103ee7d6a89e10eb5a892d877c398df0da7140704/pydantic-1.10.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cf06d8d40993e79af0ab2102ef5da77b9ddba51248e4cb27f9f3f591fbb096e", size = 2994027, upload-time = "2025-04-24T13:37:28.683Z" }, - { url = "https://files.pythonhosted.org/packages/e7/a3/f20fdecbaa2a2721a6a8ee9e4f344d1f72bd7d56e679371c3f2be15eb8c8/pydantic-1.10.22-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:184b7865b171a6057ad97f4a17fbac81cec29bd103e996e7add3d16b0d95f609", size = 3036716, upload-time = "2025-04-24T13:37:30.547Z" }, - { url = "https://files.pythonhosted.org/packages/1f/83/dab34436d830c38706685acc77219fc2a209fea2a2301a1b05a2865b28bf/pydantic-1.10.22-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:923ad861677ab09d89be35d36111156063a7ebb44322cdb7b49266e1adaba4bb", size = 3171801, upload-time = "2025-04-24T13:37:32.474Z" }, - { url = "https://files.pythonhosted.org/packages/1e/6e/b64deccb8a7304d584088972437ea3091e9d99d27a8e7bf2bd08e29ae84e/pydantic-1.10.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:82d9a3da1686443fb854c8d2ab9a473251f8f4cdd11b125522efb4d7c646e7bc", size = 3123560, upload-time = "2025-04-24T13:37:34.855Z" }, - { url = "https://files.pythonhosted.org/packages/08/9a/90d1ab704329a7ae8666354be84b5327d655764003974364767c9d307d3a/pydantic-1.10.22-cp312-cp312-win_amd64.whl", hash = "sha256:1612604929af4c602694a7f3338b18039d402eb5ddfbf0db44f1ebfaf07f93e7", size = 2191378, upload-time = "2025-04-24T13:37:36.649Z" }, - { url = "https://files.pythonhosted.org/packages/47/8f/67befe3607b342dd6eb80237134ebcc6e8db42138609306eaf2b30e1f273/pydantic-1.10.22-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b259dc89c9abcd24bf42f31951fb46c62e904ccf4316393f317abeeecda39978", size = 2797042, upload-time = "2025-04-24T13:37:38.753Z" }, - { url = "https://files.pythonhosted.org/packages/aa/91/bfde7d301f8e1c4cff949b3f1eb2c9b27bdd4b2368da0fe88e7350bbe4bc/pydantic-1.10.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9238aa0964d80c0908d2f385e981add58faead4412ca80ef0fa352094c24e46d", size = 2538572, upload-time = "2025-04-24T13:37:41.653Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/1b0097ece420354df77d2f01c72278fb43770c8ed732d6b7a303c0c70875/pydantic-1.10.22-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8029f05b04080e3f1a550575a1bca747c0ea4be48e2d551473d47fd768fc1b", size = 2986271, upload-time = "2025-04-24T13:37:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/eb/4c/e257edfd5a0025a428aee7a2835e21b51c76a6b1c8994bcccb14d5721eea/pydantic-1.10.22-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c06918894f119e0431a36c9393bc7cceeb34d1feeb66670ef9b9ca48c073937", size = 3015617, upload-time = "2025-04-24T13:37:45.466Z" }, - { url = "https://files.pythonhosted.org/packages/00/17/ecf46ff31fd62d382424a07ed60540d4479094204bebeebb6dea597e88c3/pydantic-1.10.22-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e205311649622ee8fc1ec9089bd2076823797f5cd2c1e3182dc0e12aab835b35", size = 3164222, upload-time = "2025-04-24T13:37:47.35Z" }, - { url = "https://files.pythonhosted.org/packages/1a/47/2d55ec452c9a87347234bbbc70df268e1f081154b1851f0db89638558a1c/pydantic-1.10.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:815f0a73d5688d6dd0796a7edb9eca7071bfef961a7b33f91e618822ae7345b7", size = 3117572, upload-time = "2025-04-24T13:37:49.339Z" }, - { url = "https://files.pythonhosted.org/packages/03/2f/30359a36245b029bec7e442dd780fc242c66e66ad7dd5b50af2dcfd41ff3/pydantic-1.10.22-cp313-cp313-win_amd64.whl", hash = "sha256:9dfce71d42a5cde10e78a469e3d986f656afc245ab1b97c7106036f088dd91f8", size = 2174666, upload-time = "2025-04-24T13:37:51.114Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e0/1ed151a56869be1588ad2d8cda9f8c1d95b16f74f09a7cea879ca9b63a8b/pydantic-1.10.22-py3-none-any.whl", hash = "sha256:343037d608bcbd34df937ac259708bfc83664dadf88afe8516c4f282d7d471a9", size = 166503, upload-time = "2025-04-24T13:38:41.374Z" }, + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, ] [[package]] @@ -1849,6 +2222,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/26/5c5fa0e83c3621db835cfc1f1d789b37e7fa99ed54423b5f519beb931aa7/redis-5.3.1-py3-none-any.whl", hash = "sha256:dc1909bd24669cc31b5f67a039700b16ec30571096c5f1f0d9d2324bff31af97", size = 272833, upload-time = "2025-07-25T08:06:26.317Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/7a/617356cbecdb452812a5d42f720d6d5096b360d4a4c1073af700ea140ad2/regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6", size = 489415, upload-time = "2026-04-03T20:53:11.645Z" }, + { url = "https://files.pythonhosted.org/packages/20/e6/bf057227144d02e3ba758b66649e87531d744dda5f3254f48660f18ae9d8/regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87", size = 291205, upload-time = "2026-04-03T20:53:13.289Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3b/637181b787dd1a820ba1c712cee2b4144cd84a32dc776ca067b12b2d70c8/regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8", size = 289225, upload-time = "2026-04-03T20:53:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/05/21/bac05d806ed02cd4b39d9c8e5b5f9a2998c94c3a351b7792e80671fa5315/regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada", size = 792434, upload-time = "2026-04-03T20:53:17.414Z" }, + { url = "https://files.pythonhosted.org/packages/d9/17/c65d1d8ae90b772d5758eb4014e1e011bb2db353fc4455432e6cc9100df7/regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d", size = 861730, upload-time = "2026-04-03T20:53:18.903Z" }, + { url = "https://files.pythonhosted.org/packages/ad/64/933321aa082a2c6ee2785f22776143ba89840189c20d3b6b1d12b6aae16b/regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87", size = 906495, upload-time = "2026-04-03T20:53:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/01/ea/4c8d306e9c36ac22417336b1e02e7b358152c34dc379673f2d331143725f/regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4", size = 799810, upload-time = "2026-04-03T20:53:22.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/7605048f00e1379eba89d610c7d644d8f695dc9b26d3b6ecfa3132b872ff/regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86", size = 774242, upload-time = "2026-04-03T20:53:25.015Z" }, + { url = "https://files.pythonhosted.org/packages/e9/77/283e0d5023fde22cd9e86190d6d9beb21590a452b195ffe00274de470691/regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59", size = 781257, upload-time = "2026-04-03T20:53:26.918Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fb/7f3b772be101373c8626ed34c5d727dcbb8abd42a7b1219bc25fd9a3cc04/regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453", size = 854490, upload-time = "2026-04-03T20:53:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/85/30/56547b80f34f4dd2986e1cdd63b1712932f63b6c4ce2f79c50a6cd79d1c2/regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80", size = 763544, upload-time = "2026-04-03T20:53:30.917Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2f/ce060fdfea8eff34a8997603532e44cdb7d1f35e3bc253612a8707a90538/regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b", size = 844442, upload-time = "2026-04-03T20:53:32.463Z" }, + { url = "https://files.pythonhosted.org/packages/e5/44/810cb113096a1dacbe82789fbfab2823f79d19b7f1271acecb7009ba9b88/regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f", size = 789162, upload-time = "2026-04-03T20:53:34.039Z" }, + { url = "https://files.pythonhosted.org/packages/20/96/9647dd7f2ecf6d9ce1fb04dfdb66910d094e10d8fe53e9c15096d8aa0bd2/regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351", size = 266227, upload-time = "2026-04-03T20:53:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/33/80/74e13262460530c3097ff343a17de9a34d040a5dc4de9cf3a8241faab51c/regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735", size = 278399, upload-time = "2026-04-03T20:53:37.021Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/39f19f47f19dcefa3403f09d13562ca1c0fd07ab54db2bc03148f3f6b46a/regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54", size = 270473, upload-time = "2026-04-03T20:53:38.633Z" }, + { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, + { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, + { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, + { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, + { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, + { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, + { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, + { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, + { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, + { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, + { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, + { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, + { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, + { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, + { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, + { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + [[package]] name = "rich" version = "14.1.0" @@ -1887,6 +2393,7 @@ dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "greenlet" }, { name = "httpx", extra = ["socks"] }, + { name = "litellm" }, { name = "marshmallow" }, { name = "mdurl" }, { name = "nostr" }, @@ -1917,10 +2424,11 @@ dev = [ requires-dist = [ { name = "aiosqlite", specifier = ">=0.20" }, { name = "alembic", specifier = ">=1.13" }, - { name = "cashu" }, + { name = "cashu", specifier = ">=0.20" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115" }, { name = "greenlet", specifier = ">=3.2.1" }, { name = "httpx", extras = ["socks"], specifier = ">=0.25.2" }, + { name = "litellm", specifier = ">=1.55.0" }, { name = "marshmallow", specifier = ">=3.13,<4.0" }, { name = "mdurl", specifier = "==0.1.2" }, { name = "nostr", specifier = ">=0.0.2" }, @@ -1947,6 +2455,114 @@ dev = [ { name = "ruff", specifier = ">=0.11.6" }, ] +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + [[package]] name = "ruff" version = "0.12.7" @@ -2104,6 +2720,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, ] +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + [[package]] name = "tomli" version = "2.2.1" @@ -2188,6 +2884,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + [[package]] name = "uvicorn" version = "0.31.1"