mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-09-14 02:35:05 +00:00
Merge branch 'main' into x-routstr-model-path
This commit is contained in:
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11", "3.12"]
|
||||
python-version: ["3.11", "3.12", "3.14"]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -25,22 +25,22 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync --dev
|
||||
uv sync --python ${{ matrix.python-version }} --dev
|
||||
|
||||
- name: Run linting with ruff
|
||||
run: |
|
||||
uv run ruff check .
|
||||
uv run --python ${{ matrix.python-version }} ruff check .
|
||||
|
||||
- name: Run type checking with mypy
|
||||
run: |
|
||||
uv run mypy .
|
||||
uv run --python ${{ matrix.python-version }} mypy .
|
||||
|
||||
- name: Run tests with pytest
|
||||
env:
|
||||
UPSTREAM_BASE_URL: "http://test"
|
||||
UPSTREAM_API_KEY: "test"
|
||||
run: |
|
||||
uv run pytest --verbose --tb=short
|
||||
uv run --python ${{ matrix.python-version }} pytest --verbose --tb=short
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.11
|
||||
3.14
|
||||
|
||||
+3
-1
@@ -1,10 +1,12 @@
|
||||
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim
|
||||
ARG PYTHON_VERSION=3.14
|
||||
FROM ghcr.io/astral-sh/uv:python${PYTHON_VERSION}-bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libffi-dev \
|
||||
libsecp256k1-dev \
|
||||
autoconf \
|
||||
automake \
|
||||
|
||||
+4
-1
@@ -1,4 +1,6 @@
|
||||
# Multi-stage Dockerfile for Routstr (includes UI build)
|
||||
ARG PYTHON_VERSION=3.14
|
||||
|
||||
# Stage 1: Build the UI
|
||||
FROM node:23-alpine AS ui-builder
|
||||
WORKDIR /app/ui
|
||||
@@ -16,13 +18,14 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN pnpm run build
|
||||
|
||||
# Stage 2: Build the Routstr Node
|
||||
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim AS runner
|
||||
FROM ghcr.io/astral-sh/uv:python${PYTHON_VERSION}-bookworm-slim AS runner
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libffi-dev \
|
||||
libsecp256k1-dev \
|
||||
autoconf \
|
||||
automake \
|
||||
|
||||
@@ -300,7 +300,7 @@ Project metadata and dependencies:
|
||||
name = "routstr"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.115",
|
||||
"fastapi[standard-no-fastapi-cloud-cli]>=0.141",
|
||||
"sqlmodel>=0.0.24",
|
||||
"cashu",
|
||||
# ...
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ from openai import OpenAI
|
||||
client = OpenAI(
|
||||
api_key=os.environ.get("TOKEN"),
|
||||
base_url=os.environ.get("ONION_URL", "http://roustrjfsdgfiueghsklchg.onion/v1"),
|
||||
http_client=httpx.Client(proxies="socks5://localhost:9050"),
|
||||
http_client=httpx.Client(proxy="socks5://localhost:9050"),
|
||||
)
|
||||
|
||||
print(
|
||||
|
||||
+35
-6
@@ -6,22 +6,22 @@ readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.115",
|
||||
"fastapi[standard-no-fastapi-cloud-cli]>=0.141",
|
||||
"aiosqlite>=0.20",
|
||||
"sqlmodel>=0.0.24",
|
||||
"httpx[socks]>=0.25.2",
|
||||
"h11>=0.14",
|
||||
"sqlmodel>=0.0.42", # Python 3.14 deferred-annotation support
|
||||
"httpx[socks]>=0.28.1",
|
||||
"h11>=0.16",
|
||||
"greenlet>=3.2.1",
|
||||
"alembic>=1.13",
|
||||
"python-json-logger>=2.0.0",
|
||||
"cashu>=0.20",
|
||||
"marshmallow>=3.13,<4.0",
|
||||
"websockets>=12.0",
|
||||
"nostr>=0.0.2",
|
||||
"nostr-sdk>=0.45.1,<0.46",
|
||||
"mdurl==0.1.2",
|
||||
"pillow>=10",
|
||||
"openai>=1.98.0",
|
||||
"litellm>=1.55.0",
|
||||
"litellm>=1.93.0,<1.94", # 1.93 is the first line supporting Python 3.14
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
@@ -87,3 +87,32 @@ disallow_untyped_decorators = true
|
||||
|
||||
[tool.uv.sources]
|
||||
routstr = { workspace = true }
|
||||
|
||||
# Security floors. cashu 0.20.x caps httpx, h11, fastapi, cryptography,
|
||||
# setuptools and wheel below their patched versions. routstr uses only cashu's
|
||||
# wallet modules, not its server paths, so the caps are safe to lift here.
|
||||
[tool.uv]
|
||||
# coincurve 20's build config uses cmake.verbose, removed in
|
||||
# scikit-build-core 0.10. This keeps its source build working on Python 3.14.
|
||||
build-constraint-dependencies = ["scikit-build-core<0.10"]
|
||||
override-dependencies = [
|
||||
# httpx 0.28 (required by litellm 1.93) removed the `proxies` kwarg cashu
|
||||
# passes on every mint call; routstr/cashu_compat.py restores it for cashu.
|
||||
"httpx[socks]>=0.28.1,<1.0",
|
||||
"importlib-metadata>=8.0.0,<9.0",
|
||||
"h11>=0.16.0",
|
||||
"fastapi[standard-no-fastapi-cloud-cli]>=0.141",
|
||||
"cryptography>=49.0.0",
|
||||
"setuptools>=83.0.0",
|
||||
"wheel>=0.46.2",
|
||||
]
|
||||
|
||||
# Transitive deps whose dependents allow the patched version but don't require
|
||||
# it. Constraints raise the floor without bypassing any upstream pin.
|
||||
constraint-dependencies = [
|
||||
"starlette>=1.3.1",
|
||||
"httpcore>=1.0.9", # 1.0.8 caps h11<0.15
|
||||
# 1.76 is the first grpcio-tools release with CPython 3.14 wheels.
|
||||
"grpcio>=1.76.0,<2.0.0",
|
||||
"grpcio-tools>=1.76.0,<2.0.0",
|
||||
]
|
||||
|
||||
+21
-29
@@ -1191,13 +1191,27 @@ async def adjust_payment_for_tokens(
|
||||
calculated_cost = await calculate_cost(
|
||||
response_data, deducted_max_cost, model_obj, provider_fee
|
||||
)
|
||||
if not isinstance(calculated_cost, CostDataError):
|
||||
if not await _claim_reservation_for_charge(reservation, session):
|
||||
# A prior charge or release already owns this reservation. Returning
|
||||
# the calculated metadata is safe; the aggregate balances must not
|
||||
# be modified a second time.
|
||||
calculated_cost.charged_msats = 0
|
||||
return calculated_cost.dict()
|
||||
if isinstance(calculated_cost, CostDataError):
|
||||
# Content was already served, so release instead of raising a 400.
|
||||
logger.error(
|
||||
"Cost calculation error during payment adjustment, releasing reservation",
|
||||
extra={
|
||||
"key_hash": key_log_hash,
|
||||
"model": model,
|
||||
"error_message": calculated_cost.message,
|
||||
"error_code": calculated_cost.code,
|
||||
},
|
||||
)
|
||||
calculated_cost = MaxCostData(
|
||||
base_msats=0, input_msats=0, output_msats=0, total_msats=0
|
||||
)
|
||||
|
||||
if not await _claim_reservation_for_charge(reservation, session):
|
||||
# A prior charge or release already owns this reservation. Returning
|
||||
# the calculated metadata is safe; the aggregate balances must not
|
||||
# be modified a second time.
|
||||
calculated_cost.charged_msats = 0
|
||||
return calculated_cost.dict()
|
||||
|
||||
match calculated_cost:
|
||||
case MaxCostData() as cost:
|
||||
@@ -1522,28 +1536,6 @@ async def adjust_payment_for_tokens(
|
||||
|
||||
return cost.dict()
|
||||
|
||||
case CostDataError() as error:
|
||||
logger.error(
|
||||
"Cost calculation error during payment adjustment - releasing reservation",
|
||||
extra={
|
||||
"key_hash": key.hashed_key[:8] + "...",
|
||||
"model": model,
|
||||
"error_message": error.message,
|
||||
"error_code": error.code,
|
||||
},
|
||||
)
|
||||
await release_reservation_only()
|
||||
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": {
|
||||
"message": error.message,
|
||||
"type": "invalid_request_error",
|
||||
"code": error.code,
|
||||
}
|
||||
},
|
||||
)
|
||||
# All calculate_cost variants are handled above.
|
||||
raise AssertionError("Unreachable: unhandled calculate_cost result")
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Compatibility shim that keeps cashu 0.20.x working on httpx>=0.28.
|
||||
|
||||
cashu's ``async_set_httpx_client`` decorator builds the client for *every* mint
|
||||
call as ``httpx.AsyncClient(proxies=proxies_dict, ...)``. httpx deprecated
|
||||
``proxies`` in 0.26 and removed it in 0.28, so on httpx>=0.28 every wallet
|
||||
operation routstr performs -- ``load_mint_keysets``, ``mint_quote``,
|
||||
``melt_quote``, token redeem -- raises::
|
||||
|
||||
TypeError: AsyncClient.__init__() got an unexpected keyword argument 'proxies'
|
||||
|
||||
Upstream cashu (0.20.3, the latest release) still caps ``httpx<0.26`` and has no
|
||||
release that fixes this, while litellm>=1.84 requires ``httpx>=0.28``. We bridge
|
||||
the gap by translating the keyword *inside cashu's module namespace only* --
|
||||
global httpx behaviour is untouched, and the shim is a no-op on httpx<0.28.
|
||||
|
||||
Delete this module once cashu ships a release that passes ``proxy=``.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
__all__ = ["install_cashu_httpx_shim"]
|
||||
|
||||
_ALL_SCHEMES = "all://"
|
||||
|
||||
|
||||
def _single_proxy(proxies: Any) -> str | None:
|
||||
"""Collapse an httpx<0.28 ``proxies`` mapping into a single ``proxy`` URL.
|
||||
|
||||
cashu only ever builds ``{}`` or ``{"all://": url}``, so a mapping with one
|
||||
distinct URL is all we need to support. Anything richer is unrepresentable
|
||||
as httpx 0.28's scalar ``proxy=``; we fail closed and raise rather than
|
||||
return ``None``, because dropping the entry would silently send mint
|
||||
traffic direct instead of through the configured Tor/SOCKS proxy.
|
||||
"""
|
||||
if not proxies:
|
||||
return None
|
||||
if isinstance(proxies, str):
|
||||
return proxies
|
||||
if isinstance(proxies, dict):
|
||||
if _ALL_SCHEMES in proxies:
|
||||
value = proxies[_ALL_SCHEMES]
|
||||
return str(value) if value is not None else None
|
||||
distinct = {str(v) for v in proxies.values() if v is not None}
|
||||
if not distinct:
|
||||
return None
|
||||
if len(distinct) == 1:
|
||||
return distinct.pop()
|
||||
raise ValueError(
|
||||
f"cannot represent proxies={proxies!r} as httpx 0.28 proxy=; "
|
||||
"refusing to send proxied traffic direct"
|
||||
)
|
||||
|
||||
|
||||
class _ProxiesCompatAsyncClient(httpx.AsyncClient):
|
||||
"""``httpx.AsyncClient`` that still accepts the removed ``proxies`` kwarg."""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
if "proxies" in kwargs:
|
||||
proxies = kwargs.pop("proxies")
|
||||
proxy = _single_proxy(proxies)
|
||||
if proxy is not None and kwargs.get("proxy") is None:
|
||||
kwargs["proxy"] = proxy
|
||||
elif isinstance(proxies, dict) and not proxies:
|
||||
# In httpx<0.28, proxies={} disabled environment proxy discovery.
|
||||
kwargs.setdefault("trust_env", False)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class _HttpxNamespace:
|
||||
"""Stand-in for the ``httpx`` module inside cashu's ``v1_api``.
|
||||
|
||||
Every attribute resolves against the real module except ``AsyncClient``,
|
||||
so cashu keeps using genuine httpx types everywhere else.
|
||||
"""
|
||||
|
||||
AsyncClient = _ProxiesCompatAsyncClient
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(httpx, name)
|
||||
|
||||
|
||||
def _httpx_accepts_proxies() -> bool:
|
||||
import inspect
|
||||
|
||||
try:
|
||||
return "proxies" in inspect.signature(httpx.AsyncClient.__init__).parameters
|
||||
except (TypeError, ValueError): # pragma: no cover - defensive
|
||||
return False
|
||||
|
||||
|
||||
def install_cashu_httpx_shim() -> bool:
|
||||
"""Patch cashu's mint client to survive httpx>=0.28.
|
||||
|
||||
Returns True when the shim was installed, False when it wasn't needed.
|
||||
Safe to call repeatedly.
|
||||
"""
|
||||
if _httpx_accepts_proxies():
|
||||
return False
|
||||
|
||||
from cashu.wallet import v1_api
|
||||
|
||||
if isinstance(getattr(v1_api, "httpx", None), _HttpxNamespace):
|
||||
return True
|
||||
|
||||
v1_api.httpx = _HttpxNamespace() # type: ignore[assignment]
|
||||
return True
|
||||
@@ -34,6 +34,26 @@ class UpstreamError(Exception):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class EhbpTimeoutError(UpstreamError):
|
||||
"""Raised when an EHBP upstream times out waiting for a response.
|
||||
|
||||
Distinct from a generic :class:`UpstreamError` so callers can map the
|
||||
failure to a ``504 Gateway Timeout`` with a stable ``UPSTREAM_TIMEOUT``
|
||||
code instead of a misleading ``500`` internal server error.
|
||||
|
||||
``details`` carries optional structured, redaction-safe context and is
|
||||
forwarded to the client by ``create_upstream_error_response``.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict[str, object] | None = None):
|
||||
super().__init__(
|
||||
message,
|
||||
status_code=504,
|
||||
code="UPSTREAM_TIMEOUT",
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
async def http_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""Handle HTTP exceptions and include request ID in response."""
|
||||
request_id = getattr(request.state, "request_id", "unknown")
|
||||
|
||||
+22
-4
@@ -184,6 +184,18 @@ class RequestIdFilter(logging.Filter):
|
||||
return True
|
||||
|
||||
|
||||
class ClientAppFilter(logging.Filter):
|
||||
"""Attach request-local app attribution to log records."""
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
# Import here to avoid circular imports
|
||||
from .middleware import UNKNOWN_CLIENT_APP, client_app_context
|
||||
|
||||
client_app = client_app_context.get(None)
|
||||
record.client_app = client_app if client_app else UNKNOWN_CLIENT_APP
|
||||
return True
|
||||
|
||||
|
||||
# Standard ``LogRecord`` attributes that are never user-supplied ``extra``
|
||||
# fields; skipped when redacting structured extras (``msg``/``message`` are
|
||||
# handled separately above).
|
||||
@@ -323,7 +335,7 @@ def setup_logging() -> None:
|
||||
"rich_tracebacks": True,
|
||||
"markup": True,
|
||||
"console": _console,
|
||||
"filters": ["request_id_filter", "security_filter"],
|
||||
"filters": ["request_id_filter", "client_app_filter", "security_filter"],
|
||||
}
|
||||
else:
|
||||
console_handler = {
|
||||
@@ -331,7 +343,7 @@ def setup_logging() -> None:
|
||||
"level": log_level,
|
||||
"formatter": "plain",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["request_id_filter", "security_filter"],
|
||||
"filters": ["request_id_filter", "client_app_filter", "security_filter"],
|
||||
}
|
||||
|
||||
LOGGING_CONFIG = {
|
||||
@@ -340,7 +352,7 @@ def setup_logging() -> None:
|
||||
"formatters": {
|
||||
"json": {
|
||||
"()": jsonlogger.JsonFormatter,
|
||||
"format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d %(version)s %(request_id)s",
|
||||
"format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d %(version)s %(request_id)s %(client_app)s",
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
"plain": {
|
||||
@@ -351,6 +363,7 @@ def setup_logging() -> None:
|
||||
"filters": {
|
||||
"version_filter": {"()": VersionFilter},
|
||||
"request_id_filter": {"()": RequestIdFilter},
|
||||
"client_app_filter": {"()": ClientAppFilter},
|
||||
"security_filter": {"()": SecurityFilter},
|
||||
},
|
||||
"handlers": {
|
||||
@@ -364,7 +377,12 @@ def setup_logging() -> None:
|
||||
"interval": 1, # Every 1 day
|
||||
"backupCount": 30, # Keep 30 days of logs
|
||||
"atTime": None, # Rotate at midnight (00:00)
|
||||
"filters": ["version_filter", "request_id_filter", "security_filter"],
|
||||
"filters": [
|
||||
"version_filter",
|
||||
"request_id_filter",
|
||||
"client_app_filter",
|
||||
"security_filter",
|
||||
],
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
|
||||
@@ -17,6 +17,7 @@ from ..auth import (
|
||||
periodic_stale_reservation_sweep,
|
||||
)
|
||||
from ..balance import balance_router, deprecated_wallet_router
|
||||
from ..cashu_compat import install_cashu_httpx_shim
|
||||
from ..lightning import lightning_router, periodic_invoice_watcher
|
||||
from ..nostr import (
|
||||
announce_provider,
|
||||
@@ -71,6 +72,12 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
||||
invoice_watcher_task = None
|
||||
|
||||
try:
|
||||
# cashu 0.20.x passes the `proxies` kwarg httpx removed in 0.28.
|
||||
# routstr.wallet and routstr.payment.lnurl also install this at import;
|
||||
# repeating it here keeps startup correct for any future module that
|
||||
# reaches cashu's mint client without going through those two.
|
||||
install_cashu_httpx_shim()
|
||||
|
||||
# Apply litellm-wide settings (drop_params, chat-completions URL,
|
||||
# debug logging) before any upstream provider dispatches a request.
|
||||
configure_litellm()
|
||||
|
||||
@@ -2,8 +2,10 @@ import time
|
||||
import uuid
|
||||
from contextvars import ContextVar
|
||||
from typing import Callable
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from fastapi import Request, Response
|
||||
from starlette.datastructures import Headers
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from .logging import get_logger
|
||||
@@ -13,6 +15,41 @@ logger = get_logger(__name__)
|
||||
# Context variable to store request ID across async context
|
||||
request_id_context: ContextVar[str | None] = ContextVar("request_id")
|
||||
|
||||
client_app_context: ContextVar[str | None] = ContextVar("client_app")
|
||||
|
||||
UNKNOWN_CLIENT_APP = "unknown"
|
||||
|
||||
# Prefer OpenRouter app headers, then browser and SDK fallbacks.
|
||||
_CLIENT_APP_HEADERS: tuple[str, ...] = (
|
||||
"x-title",
|
||||
"http-referer",
|
||||
"referer",
|
||||
"user-agent",
|
||||
)
|
||||
|
||||
# Limit untrusted header data repeated in every log record.
|
||||
_CLIENT_APP_MAX_LENGTH = 120
|
||||
|
||||
|
||||
def client_app_from_headers(headers: Headers) -> str:
|
||||
for header in _CLIENT_APP_HEADERS:
|
||||
raw = headers.get(header)
|
||||
if raw is None:
|
||||
continue
|
||||
cleaned = "".join(ch for ch in raw if ch.isprintable()).strip()
|
||||
if header in ("http-referer", "referer"):
|
||||
try:
|
||||
url = urlsplit(cleaned)
|
||||
if url.scheme not in ("http", "https") or not url.hostname:
|
||||
continue
|
||||
except ValueError:
|
||||
continue
|
||||
# Attribution needs the origin, not credentials or private page URLs.
|
||||
cleaned = f"{url.scheme}://{url.netloc.rsplit('@', 1)[-1]}"
|
||||
if cleaned:
|
||||
return cleaned[:_CLIENT_APP_MAX_LENGTH]
|
||||
return UNKNOWN_CLIENT_APP
|
||||
|
||||
|
||||
# Methods that are never logged: HEAD requests are health probes from
|
||||
# monitoring/load balancers, OPTIONS are CORS preflights — both are framework
|
||||
@@ -71,6 +108,10 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
||||
# Set request ID in context for logging
|
||||
token = request_id_context.set(request_id)
|
||||
|
||||
client_app_token = client_app_context.set(
|
||||
client_app_from_headers(request.headers)
|
||||
)
|
||||
|
||||
path = request.url.path
|
||||
should_log = _should_log(request.method, path)
|
||||
|
||||
@@ -130,6 +171,12 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
||||
finally:
|
||||
# Reset context
|
||||
request_id_context.reset(token)
|
||||
client_app_context.reset(client_app_token)
|
||||
|
||||
|
||||
__all__ = ["LoggingMiddleware", "request_id_context"]
|
||||
__all__ = [
|
||||
"LoggingMiddleware",
|
||||
"UNKNOWN_CLIENT_APP",
|
||||
"client_app_context",
|
||||
"request_id_context",
|
||||
]
|
||||
|
||||
@@ -262,7 +262,7 @@ def derive_npub_from_nsec(nsec: str) -> str | None:
|
||||
boot.
|
||||
"""
|
||||
try:
|
||||
from nostr.key import PublicKey # type: ignore
|
||||
from nostr_sdk import PublicKey
|
||||
|
||||
from ..nostr.listing import nsec_to_keypair
|
||||
except ImportError:
|
||||
@@ -274,7 +274,7 @@ def derive_npub_from_nsec(nsec: str) -> str | None:
|
||||
_privkey_hex, pubkey_hex = keypair
|
||||
|
||||
try:
|
||||
return PublicKey(bytes.fromhex(pubkey_hex)).bech32()
|
||||
return PublicKey.parse(pubkey_hex).to_bech32()
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
@@ -12,13 +12,11 @@ import json
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from nostr.event import Event
|
||||
from nostr.key import PrivateKey
|
||||
|
||||
from ..core import get_logger
|
||||
from ..core.log_manager import log_manager
|
||||
from ..core.settings import settings
|
||||
from .listing import nsec_to_keypair, publish_to_relay
|
||||
from .sdk import create_signed_event
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -44,18 +42,6 @@ WINDOW_DEFINITIONS: tuple[tuple[str, int, int], ...] = (
|
||||
)
|
||||
|
||||
|
||||
def _event_to_dict(ev: Event) -> dict[str, Any]:
|
||||
return {
|
||||
"id": ev.id,
|
||||
"pubkey": ev.public_key,
|
||||
"created_at": ev.created_at,
|
||||
"kind": int(ev.kind) if not isinstance(ev.kind, int) else ev.kind,
|
||||
"tags": ev.tags,
|
||||
"content": ev.content,
|
||||
"sig": ev.signature,
|
||||
}
|
||||
|
||||
|
||||
def _resolve_provider_id(public_key_hex: str) -> str:
|
||||
explicit_provider_id = (settings.provider_id or "").strip()
|
||||
if explicit_provider_id:
|
||||
@@ -293,21 +279,18 @@ def create_stats_snapshot_event(
|
||||
*,
|
||||
d_tag: str,
|
||||
) -> dict[str, Any]:
|
||||
private_key = PrivateKey(bytes.fromhex(private_key_hex))
|
||||
tags = [
|
||||
["d", d_tag],
|
||||
["provider", provider_id],
|
||||
["schema", ANALYTICS_SCHEMA],
|
||||
]
|
||||
|
||||
event = Event(
|
||||
public_key=private_key.public_key.hex(),
|
||||
content=payload_json,
|
||||
return create_signed_event(
|
||||
private_key_hex,
|
||||
kind=ANALYTICS_KIND,
|
||||
content=payload_json,
|
||||
tags=tags,
|
||||
)
|
||||
private_key.sign_event(event)
|
||||
return _event_to_dict(event)
|
||||
|
||||
|
||||
def _fingerprint_payload(payload: dict[str, Any]) -> str:
|
||||
|
||||
@@ -320,18 +320,18 @@ async def fetch_provider_health(endpoint_url: str) -> dict[str, Any]:
|
||||
is_onion = ".onion" in endpoint_url
|
||||
|
||||
# Set up client arguments conditionally
|
||||
proxies = None
|
||||
proxy: str | None = None
|
||||
if is_onion:
|
||||
try:
|
||||
tor_proxy = settings.tor_proxy_url
|
||||
except Exception:
|
||||
tor_proxy = "socks5://127.0.0.1:9050"
|
||||
proxies = {"http://": tor_proxy, "https://": tor_proxy} # type: ignore[assignment]
|
||||
proxy = tor_proxy
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(30.0),
|
||||
follow_redirects=True,
|
||||
proxies=proxies, # type: ignore[arg-type]
|
||||
proxy=proxy,
|
||||
) as client:
|
||||
# Prefer provider's /v1/info for full details
|
||||
info_url = f"{endpoint_url.rstrip('/')}/v1/info"
|
||||
|
||||
+36
-123
@@ -8,18 +8,12 @@ import asyncio
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import ssl
|
||||
import time
|
||||
from typing import Any, cast
|
||||
|
||||
from nostr.event import Event
|
||||
from nostr.filter import Filter, Filters
|
||||
from nostr.key import PrivateKey
|
||||
from nostr.message_type import ClientMessageType
|
||||
from nostr.relay_manager import RelayManager
|
||||
|
||||
from ..core import get_logger
|
||||
from ..core.settings import settings
|
||||
from .sdk import create_signed_event, fetch_events, parse_keypair, send_event
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -33,18 +27,6 @@ def get_app_version() -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _event_to_dict(ev: Event) -> dict[str, Any]:
|
||||
return {
|
||||
"id": ev.id,
|
||||
"pubkey": ev.public_key,
|
||||
"created_at": ev.created_at,
|
||||
"kind": int(ev.kind) if not isinstance(ev.kind, int) else ev.kind,
|
||||
"tags": ev.tags,
|
||||
"content": ev.content,
|
||||
"sig": ev.signature,
|
||||
}
|
||||
|
||||
|
||||
def nsec_to_keypair(nsec: str) -> tuple[str, str] | None:
|
||||
"""
|
||||
Convert a Nostr private key (nsec) to a keypair (privkey_hex, pubkey_hex).
|
||||
@@ -56,16 +38,10 @@ def nsec_to_keypair(nsec: str) -> tuple[str, str] | None:
|
||||
Tuple of (private_key_hex, public_key_hex) or None if invalid
|
||||
"""
|
||||
try:
|
||||
if nsec.startswith("nsec"):
|
||||
pk = PrivateKey.from_nsec(nsec)
|
||||
return (pk.hex(), pk.public_key.hex())
|
||||
|
||||
if len(nsec) == 64:
|
||||
pk = PrivateKey(bytes.fromhex(nsec))
|
||||
return (pk.hex(), pk.public_key.hex())
|
||||
|
||||
logger.error(f"Invalid private key format/length: {len(nsec)}")
|
||||
return None
|
||||
if not (nsec.startswith("nsec") or len(nsec) == 64):
|
||||
logger.error(f"Invalid private key format/length: {len(nsec)}")
|
||||
return None
|
||||
return parse_keypair(nsec)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to convert nsec to keypair: {e}")
|
||||
return None
|
||||
@@ -93,8 +69,6 @@ def create_listing_event(
|
||||
Returns:
|
||||
Complete signed nostr event as a dict ready for publishing
|
||||
"""
|
||||
pk = PrivateKey(bytes.fromhex(private_key_hex))
|
||||
|
||||
tags = [["d", provider_id]]
|
||||
for url in endpoint_urls:
|
||||
tags.append(["u", url])
|
||||
@@ -107,9 +81,12 @@ def create_listing_event(
|
||||
|
||||
content = json.dumps(metadata, separators=(",", ":")) if metadata else ""
|
||||
|
||||
ev = Event(pk.public_key.hex(), content, kind=38421, tags=tags)
|
||||
pk.sign_event(ev)
|
||||
return _event_to_dict(ev)
|
||||
return create_signed_event(
|
||||
private_key_hex,
|
||||
kind=38421,
|
||||
content=content,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
|
||||
def _get_tag_values(event: dict[str, Any], key: str) -> list[str]:
|
||||
@@ -177,75 +154,25 @@ async def query_listing_events(
|
||||
succeeded without transport-level errors.
|
||||
"""
|
||||
|
||||
def _sync_query() -> tuple[list[dict[str, Any]], bool]:
|
||||
rm = RelayManager()
|
||||
rm.add_relay(relay_url)
|
||||
events_out: list[dict[str, Any]] = []
|
||||
ok = True
|
||||
try:
|
||||
rm.open_connections({"cert_reqs": ssl.CERT_NONE})
|
||||
time.sleep(1.0)
|
||||
try:
|
||||
events_out = await fetch_events(
|
||||
relay_url,
|
||||
kind=38421,
|
||||
author=pubkey,
|
||||
limit=10,
|
||||
timeout=timeout,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to query relay {relay_url}: {type(e).__name__}")
|
||||
return [], False
|
||||
|
||||
flt = Filter(kinds=[38421], authors=[pubkey], limit=10)
|
||||
filters = Filters([flt])
|
||||
sub_id = f"routstr_listing_{int(time.time())}"
|
||||
rm.add_subscription(sub_id, filters)
|
||||
req: list[Any] = [ClientMessageType.REQUEST, sub_id]
|
||||
req.extend(filters.to_json_array())
|
||||
rm.publish_message(json.dumps(req))
|
||||
|
||||
start = time.time()
|
||||
last_event_ts = start
|
||||
while time.time() - start < timeout:
|
||||
drained = False
|
||||
while rm.message_pool.has_events():
|
||||
drained = True
|
||||
ev_msg = rm.message_pool.get_event()
|
||||
ev = ev_msg.event
|
||||
ev_dict = _event_to_dict(ev)
|
||||
if provider_id is not None:
|
||||
tags = ev_dict.get("tags", [])
|
||||
if not any(
|
||||
isinstance(t, list)
|
||||
and len(t) >= 2
|
||||
and t[0] == "d"
|
||||
and t[1] == provider_id
|
||||
for t in tags
|
||||
):
|
||||
continue
|
||||
events_out.append(ev_dict)
|
||||
logger.debug(
|
||||
f"Found listing event: {ev_dict.get('id', '')[:6]}...{ev_dict.get('id', '')[-6:]}"
|
||||
)
|
||||
if drained:
|
||||
last_event_ts = time.time()
|
||||
|
||||
while rm.message_pool.has_notices():
|
||||
notice = rm.message_pool.get_notice()
|
||||
try:
|
||||
content = getattr(notice, "content", notice)
|
||||
s = str(content)
|
||||
if len(s) > 200:
|
||||
s = s[:200] + "..."
|
||||
logger.debug(f"Relay notice: {s}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if time.time() - last_event_ts > 2.5:
|
||||
break
|
||||
|
||||
time.sleep(0.1)
|
||||
except Exception as e:
|
||||
ok = False
|
||||
logger.debug(f"Failed to query relay {relay_url}: {type(e).__name__}")
|
||||
finally:
|
||||
try:
|
||||
rm.close_connections()
|
||||
except Exception:
|
||||
pass
|
||||
return events_out, ok
|
||||
|
||||
return await asyncio.to_thread(_sync_query)
|
||||
if provider_id is not None:
|
||||
events_out = [
|
||||
event
|
||||
for event in events_out
|
||||
if _get_single_tag_value(event, "d") == provider_id
|
||||
]
|
||||
return events_out, True
|
||||
|
||||
|
||||
def discover_onion_url_from_tor(base_dir: str = "/var/lib/tor") -> str | None:
|
||||
@@ -333,27 +260,13 @@ async def publish_to_relay(
|
||||
Publish a listing event to a nostr relay via nostr library.
|
||||
"""
|
||||
|
||||
def _sync_publish() -> bool:
|
||||
rm = RelayManager()
|
||||
rm.add_relay(relay_url)
|
||||
try:
|
||||
rm.open_connections({"cert_reqs": ssl.CERT_NONE})
|
||||
time.sleep(1.0)
|
||||
# Publish the event as-is via publish_message to preserve signature
|
||||
rm.publish_message(json.dumps(["EVENT", event]))
|
||||
logger.debug(f"Sent listing event {event.get('id', '')} to {relay_url}")
|
||||
time.sleep(1.0)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to publish to {relay_url}: {type(e).__name__}")
|
||||
return False
|
||||
finally:
|
||||
try:
|
||||
rm.close_connections()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return await asyncio.to_thread(_sync_publish)
|
||||
try:
|
||||
await send_event(relay_url, event, timeout=timeout)
|
||||
logger.debug(f"Sent listing event {event.get('id', '')} to {relay_url}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to publish to {relay_url}: {type(e).__name__}")
|
||||
return False
|
||||
|
||||
|
||||
async def announce_provider() -> None:
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Small adapter around the maintained ``nostr-sdk`` package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
from nostr_sdk import (
|
||||
AckPolicy,
|
||||
Client,
|
||||
Event,
|
||||
EventBuilder,
|
||||
Filter,
|
||||
Keys,
|
||||
Kind,
|
||||
PublicKey,
|
||||
RelayUrl,
|
||||
ReqExitPolicy,
|
||||
ReqTarget,
|
||||
SendEventTarget,
|
||||
Tag,
|
||||
)
|
||||
|
||||
|
||||
def parse_keypair(secret_key: str) -> tuple[str, str]:
|
||||
keys = Keys.parse(secret_key)
|
||||
return keys.secret_key().to_hex(), keys.public_key().to_hex()
|
||||
|
||||
|
||||
def create_signed_event(
|
||||
secret_key_hex: str,
|
||||
*,
|
||||
kind: int,
|
||||
content: str,
|
||||
tags: list[list[str]],
|
||||
) -> dict[str, Any]:
|
||||
keys = Keys.parse(secret_key_hex)
|
||||
event = (
|
||||
EventBuilder(Kind(kind), content)
|
||||
.tags([Tag.parse(tag) for tag in tags])
|
||||
.finalize(keys)
|
||||
)
|
||||
return cast(dict[str, Any], json.loads(event.as_json()))
|
||||
|
||||
|
||||
async def fetch_events(
|
||||
relay_url: str,
|
||||
*,
|
||||
kind: int,
|
||||
author: str,
|
||||
limit: int,
|
||||
timeout: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
relay = RelayUrl.parse(relay_url)
|
||||
client = Client()
|
||||
await client.add_relay(relay)
|
||||
try:
|
||||
await client.connect()
|
||||
event_filter = (
|
||||
Filter().kinds([Kind(kind)]).authors([PublicKey.parse(author)]).limit(limit)
|
||||
)
|
||||
events = await client.fetch_events(
|
||||
ReqTarget.single(relay, [event_filter]),
|
||||
timeout=timedelta(seconds=timeout),
|
||||
policy=ReqExitPolicy.WAIT_DURATION_AFTER_EOSE(timedelta(seconds=2.5)),
|
||||
max_events=limit,
|
||||
)
|
||||
return [cast(dict[str, Any], json.loads(event.as_json())) for event in events]
|
||||
finally:
|
||||
await client.shutdown()
|
||||
|
||||
|
||||
async def send_event(relay_url: str, event: dict[str, Any], *, timeout: int) -> None:
|
||||
relay = RelayUrl.parse(relay_url)
|
||||
client = Client()
|
||||
await client.add_relay(relay)
|
||||
try:
|
||||
await client.connect()
|
||||
output = await client.send_event(
|
||||
Event.from_json(json.dumps(event)),
|
||||
target=SendEventTarget.to([relay]),
|
||||
ack_policy=AckPolicy.all(),
|
||||
ok_timeout=timedelta(seconds=timeout),
|
||||
)
|
||||
if output.failed or relay not in output.success:
|
||||
reasons = ", ".join(
|
||||
f"{url}: {reason}" for url, reason in output.failed.items()
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"Relay did not accept event: {reasons or 'no OK from relay'}"
|
||||
)
|
||||
finally:
|
||||
await client.shutdown()
|
||||
+146
-62
@@ -24,6 +24,12 @@ from ..wallet import (
|
||||
deserialize_token_from_string,
|
||||
is_trusted_source_mint,
|
||||
)
|
||||
from .responses_input import (
|
||||
FILE_ID_URL_PREFIX,
|
||||
count_input_images,
|
||||
input_image_part_to_image_url,
|
||||
responses_input_to_messages,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -179,7 +185,12 @@ async def calculate_discounted_max_cost(
|
||||
body: dict,
|
||||
model_obj: Any | None = None,
|
||||
) -> int:
|
||||
"""Calculate the discounted max cost for a request using model pricing when available."""
|
||||
"""Calculate the discounted max cost for a request using model pricing when available.
|
||||
|
||||
Completion discounts are trimmed from the largest declared cap among
|
||||
``max_tokens`` and ``max_completion_tokens`` (chat/completions) or
|
||||
``max_output_tokens`` (responses).
|
||||
"""
|
||||
if settings.fixed_pricing:
|
||||
return max_cost_for_model
|
||||
|
||||
@@ -225,17 +236,28 @@ async def calculate_discounted_max_cost(
|
||||
# for work the reservation never covered.
|
||||
prompt_tokens = estimate_prompt_tokens(body)
|
||||
|
||||
# Images are billed as tokens by the upstream but carry no text for
|
||||
# ``estimate_prompt_tokens`` to count, so they are estimated separately and
|
||||
# added on both the chat (``messages``) and Responses (``input``) paths.
|
||||
image_tokens = 0
|
||||
if isinstance(messages, list):
|
||||
image_tokens = await estimate_image_tokens_in_messages(messages)
|
||||
if image_tokens > 0:
|
||||
logger.debug(
|
||||
"Found images in request",
|
||||
extra={
|
||||
"model": model,
|
||||
"image_tokens": image_tokens,
|
||||
},
|
||||
)
|
||||
prompt_tokens += image_tokens
|
||||
image_tokens += await estimate_image_tokens_in_messages(messages)
|
||||
input_data = body.get("input")
|
||||
if isinstance(input_data, list):
|
||||
converted = responses_input_to_messages(input_data)
|
||||
if converted is None:
|
||||
image_tokens += count_input_images(input_data) * _MAX_ORIGINAL_IMAGE_TOKENS
|
||||
else:
|
||||
image_tokens += await estimate_image_tokens_in_messages(converted)
|
||||
if image_tokens > 0:
|
||||
logger.debug(
|
||||
"Found images in request",
|
||||
extra={
|
||||
"model": model,
|
||||
"image_tokens": image_tokens,
|
||||
},
|
||||
)
|
||||
prompt_tokens += image_tokens
|
||||
|
||||
if prompt_tokens > 0:
|
||||
estimated_prompt_delta_sats = (
|
||||
@@ -244,21 +266,39 @@ async def calculate_discounted_max_cost(
|
||||
if estimated_prompt_delta_sats > 0:
|
||||
adjusted = adjusted - math.floor(estimated_prompt_delta_sats * 1000)
|
||||
|
||||
max_tokens_raw = body.get("max_tokens", None)
|
||||
if max_tokens_raw is not None:
|
||||
# Completion caps arrive under several names: ``max_tokens`` (legacy
|
||||
# chat), ``max_completion_tokens`` (modern chat) and ``max_output_tokens``
|
||||
# (Responses API). When a request declares more than one, reserve against
|
||||
# the largest: upstream precedence between the fields varies by provider,
|
||||
# so the smaller cap may not be honored and the reservation must never
|
||||
# under-cover what the upstream could bill.
|
||||
max_tokens_int: int | None = None
|
||||
for cap_field in ("max_tokens", "max_completion_tokens", "max_output_tokens"):
|
||||
cap_raw = body.get(cap_field)
|
||||
if cap_raw is None:
|
||||
continue
|
||||
try:
|
||||
max_tokens_int = int(max_tokens_raw)
|
||||
cap_int = int(cap_raw)
|
||||
except (TypeError, ValueError):
|
||||
logger.warning(
|
||||
"Invalid max_tokens; ignoring in cost adjustment",
|
||||
extra={"max_tokens": str(max_tokens_raw)[:64], "model": model},
|
||||
"Invalid completion token cap; ignoring in cost adjustment",
|
||||
extra={
|
||||
"field": cap_field,
|
||||
"value": str(cap_raw)[:64],
|
||||
"model": model,
|
||||
},
|
||||
)
|
||||
else:
|
||||
estimated_completion_delta_sats = (
|
||||
max_completion_allowed_sats - max_tokens_int * model_pricing.completion
|
||||
)
|
||||
if estimated_completion_delta_sats > 0:
|
||||
adjusted = adjusted - math.floor(estimated_completion_delta_sats * 1000)
|
||||
continue
|
||||
max_tokens_int = (
|
||||
cap_int if max_tokens_int is None else max(max_tokens_int, cap_int)
|
||||
)
|
||||
|
||||
if max_tokens_int is not None:
|
||||
estimated_completion_delta_sats = (
|
||||
max_completion_allowed_sats - max_tokens_int * model_pricing.completion
|
||||
)
|
||||
if estimated_completion_delta_sats > 0:
|
||||
adjusted = adjusted - math.floor(estimated_completion_delta_sats * 1000)
|
||||
|
||||
logger.debug(
|
||||
"Discounted max cost computed",
|
||||
@@ -431,15 +471,46 @@ async def _fetch_image_from_url(url: str) -> bytes | None:
|
||||
return None
|
||||
|
||||
|
||||
# Patch-based image pricing (OpenAI ``detail: "original"``): the image is
|
||||
# covered with 32x32px patches and billed as ceil(patches * multiplier)
|
||||
# tokens, with no 512px-tile downscaling. The API rejects images above
|
||||
# 30,000 patches, so at the 1.2x multiplier documented for the
|
||||
# original-capable model families (gpt-5.4/5.5/5.6) the worst case a
|
||||
# single image can bill is 36,000 tokens.
|
||||
_IMAGE_PATCH_PX = 32
|
||||
_MAX_IMAGE_PATCHES = 30_000
|
||||
_MAX_ORIGINAL_IMAGE_TOKENS = (_MAX_IMAGE_PATCHES * 6 + 4) // 5 # 36,000
|
||||
|
||||
|
||||
def _calculate_original_image_tokens(width: int, height: int) -> int:
|
||||
"""Estimate tokens for an image billed at ``detail: "original"``.
|
||||
|
||||
Patch-based models cover the image with 32x32px patches and bill
|
||||
``ceil(patches * 1.2)`` tokens. The estimate is bounded by the
|
||||
30,000-patch rejection limit, which is more conservative than the
|
||||
per-model resizing patch budgets (e.g. 10,000 patches on gpt-5.4/5.5)
|
||||
so it never under-reserves.
|
||||
"""
|
||||
patches = ((width + _IMAGE_PATCH_PX - 1) // _IMAGE_PATCH_PX) * (
|
||||
(height + _IMAGE_PATCH_PX - 1) // _IMAGE_PATCH_PX
|
||||
)
|
||||
bounded = min(patches, _MAX_IMAGE_PATCHES)
|
||||
return (bounded * 6 + 4) // 5 # ceil(bounded * 1.2) in exact integer math
|
||||
|
||||
|
||||
def _calculate_image_tokens(width: int, height: int, detail: str = "auto") -> int:
|
||||
"""Calculate image tokens based on OpenAI's vision pricing.
|
||||
|
||||
For low detail: 85 tokens
|
||||
For high detail/auto: 85 base tokens + 170 tokens per 512px tile
|
||||
For original detail: patch-based pricing at the original resolution
|
||||
"""
|
||||
if detail == "low":
|
||||
return 85
|
||||
|
||||
if detail == "original":
|
||||
return _calculate_original_image_tokens(width, height)
|
||||
|
||||
if width > 2048 or height > 2048:
|
||||
aspect_ratio = width / height
|
||||
if width > height:
|
||||
@@ -492,7 +563,9 @@ async def estimate_image_tokens_in_messages(messages: list) -> int:
|
||||
continue
|
||||
|
||||
content_type = content_item.get("type")
|
||||
if content_type not in ("image_url", "input_image"):
|
||||
if content_type == "input_image":
|
||||
content_item = input_image_part_to_image_url(content_item)
|
||||
elif content_type != "image_url":
|
||||
continue
|
||||
|
||||
image_url_data = content_item.get("image_url")
|
||||
@@ -504,7 +577,7 @@ async def estimate_image_tokens_in_messages(messages: list) -> int:
|
||||
detail = "auto"
|
||||
elif isinstance(image_url_data, dict):
|
||||
url = image_url_data.get("url", "")
|
||||
detail = image_url_data.get("detail", "auto")
|
||||
detail = image_url_data.get("detail") or "auto"
|
||||
else:
|
||||
continue
|
||||
|
||||
@@ -512,56 +585,67 @@ async def estimate_image_tokens_in_messages(messages: list) -> int:
|
||||
continue
|
||||
|
||||
if url.startswith("data:image/"):
|
||||
try:
|
||||
header, base64_data = url.split(",", 1)
|
||||
image_bytes = base64.b64decode(base64_data)
|
||||
width, height = _get_image_dimensions(image_bytes)
|
||||
tokens = _calculate_image_tokens(width, height, detail)
|
||||
total_image_tokens += tokens
|
||||
logger.debug(
|
||||
"Calculated tokens for base64 image",
|
||||
extra={
|
||||
"width": width,
|
||||
"height": height,
|
||||
"detail": detail,
|
||||
"tokens": tokens,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to process base64 image",
|
||||
extra={"error": str(e)},
|
||||
)
|
||||
total_image_tokens += 85
|
||||
total_image_tokens += _data_url_image_tokens(url, detail)
|
||||
elif url.startswith(FILE_ID_URL_PREFIX):
|
||||
total_image_tokens += _worst_case_image_tokens(detail)
|
||||
elif fetches >= IMAGE_FETCH_MAX_PER_REQUEST:
|
||||
logger.warning(
|
||||
"Skipping image URL fetch above per-request limit",
|
||||
extra={"url": url[:100], "limit": IMAGE_FETCH_MAX_PER_REQUEST},
|
||||
)
|
||||
total_image_tokens += 85
|
||||
total_image_tokens += _worst_case_image_tokens(detail)
|
||||
else:
|
||||
fetches += 1
|
||||
image_bytes_or_none = await _fetch_image_from_url(url)
|
||||
if image_bytes_or_none:
|
||||
width, height = _get_image_dimensions(image_bytes_or_none)
|
||||
tokens = _calculate_image_tokens(width, height, detail)
|
||||
total_image_tokens += tokens
|
||||
logger.debug(
|
||||
"Calculated tokens for URL image",
|
||||
extra={
|
||||
"url": url[:100],
|
||||
"width": width,
|
||||
"height": height,
|
||||
"detail": detail,
|
||||
"tokens": tokens,
|
||||
},
|
||||
)
|
||||
else:
|
||||
total_image_tokens += 85
|
||||
total_image_tokens += _image_bytes_tokens(
|
||||
image_bytes_or_none, detail, source=url[:100]
|
||||
)
|
||||
|
||||
return total_image_tokens
|
||||
|
||||
|
||||
def _worst_case_image_tokens(detail: str) -> int:
|
||||
"""Dimensions unknown: reserve the most ``detail`` can bill."""
|
||||
if detail == "original":
|
||||
return _MAX_ORIGINAL_IMAGE_TOKENS
|
||||
return _calculate_image_tokens(2048, 2048, detail)
|
||||
|
||||
|
||||
def _data_url_image_tokens(url: str, detail: str) -> int:
|
||||
try:
|
||||
_, base64_data = url.split(",", 1)
|
||||
image_bytes = base64.b64decode(base64_data, validate=True)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to decode base64 image", extra={"error": str(e)})
|
||||
return _worst_case_image_tokens(detail)
|
||||
return _image_bytes_tokens(image_bytes, detail, source="data-url")
|
||||
|
||||
|
||||
def _image_bytes_tokens(image_bytes: bytes | None, detail: str, source: str) -> int:
|
||||
if not image_bytes:
|
||||
return _worst_case_image_tokens(detail)
|
||||
try:
|
||||
width, height = Image.open(BytesIO(image_bytes)).size
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to read image dimensions",
|
||||
extra={"error": str(e), "source": source},
|
||||
)
|
||||
return _worst_case_image_tokens(detail)
|
||||
tokens = _calculate_image_tokens(width, height, detail)
|
||||
logger.debug(
|
||||
"Calculated image tokens",
|
||||
extra={
|
||||
"source": source,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"detail": detail,
|
||||
"tokens": tokens,
|
||||
},
|
||||
)
|
||||
return tokens
|
||||
|
||||
|
||||
def create_error_response(
|
||||
error_type: str,
|
||||
message: str,
|
||||
|
||||
@@ -8,12 +8,17 @@ import httpx
|
||||
from cashu.core.base import MeltQuoteState
|
||||
from cashu.wallet.wallet import Proof, Wallet
|
||||
|
||||
from ..cashu_compat import install_cashu_httpx_shim
|
||||
from ..mint import (
|
||||
is_mint_rate_limited,
|
||||
is_mint_transport_error,
|
||||
run_mint_operation,
|
||||
)
|
||||
|
||||
# cashu 0.20.x passes the `proxies` kwarg httpx removed in 0.28; see the module
|
||||
# docstring. Installed at import so no mint call can run before the patch.
|
||||
install_cashu_httpx_shim()
|
||||
|
||||
try:
|
||||
from bech32 import bech32_decode, convertbits # type: ignore
|
||||
except ModuleNotFoundError: # pragma: no cover – allow runtime miss
|
||||
|
||||
+59
-18
@@ -5,7 +5,7 @@ import random
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel as V2BaseModel
|
||||
from pydantic.v1 import BaseModel
|
||||
from pydantic.v1 import BaseModel, validator
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from ..core.db import ModelRow, UpstreamProviderRow, get_session
|
||||
@@ -85,6 +85,30 @@ class TopProvider(BaseModel):
|
||||
is_moderated: bool | None = None
|
||||
|
||||
|
||||
class Reasoning(BaseModel):
|
||||
"""Per-model reasoning-effort metadata, matching OpenRouter's shape."""
|
||||
|
||||
mandatory: bool | None = None
|
||||
default_enabled: bool | None = None
|
||||
supported_efforts: list[str] | None = None
|
||||
default_effort: str | None = None
|
||||
supports_max_tokens: bool | None = None
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
return not any(
|
||||
(
|
||||
self.mandatory is not None,
|
||||
self.default_enabled is not None,
|
||||
self.supported_efforts,
|
||||
self.default_effort,
|
||||
self.supports_max_tokens is not None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Model(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
@@ -101,10 +125,43 @@ class Model(BaseModel):
|
||||
canonical_slug: str | None = None
|
||||
alias_ids: list[str] | None = None
|
||||
forwarded_model_id: str | None = None
|
||||
reasoning: Reasoning | None = None
|
||||
|
||||
class Config:
|
||||
extra = "ignore"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
|
||||
@validator("reasoning", pre=True)
|
||||
def _coerce_reasoning(cls, value: object) -> object:
|
||||
if value is None or value is False:
|
||||
return None
|
||||
if isinstance(value, Reasoning):
|
||||
return None if value.is_empty() else value
|
||||
if not isinstance(value, dict) or not value:
|
||||
return None
|
||||
try:
|
||||
parsed = Reasoning.parse_obj(value)
|
||||
except Exception:
|
||||
return None
|
||||
return None if parsed.is_empty() else parsed
|
||||
|
||||
def dict(self, **kwargs: object) -> dict:
|
||||
# Non-reasoning models omit the field entirely so the catalog stays
|
||||
# additive: existing clients never see a new null key.
|
||||
data = super().dict(**kwargs) # type: ignore[arg-type]
|
||||
reasoning = data.get("reasoning")
|
||||
if not reasoning:
|
||||
data.pop("reasoning", None)
|
||||
elif isinstance(reasoning, dict):
|
||||
cleaned = {k: v for k, v in reasoning.items() if v is not None}
|
||||
if cleaned:
|
||||
data["reasoning"] = cleaned
|
||||
else:
|
||||
data.pop("reasoning", None)
|
||||
return data
|
||||
|
||||
|
||||
def litellm_cost_entry(model_id: str) -> dict | None:
|
||||
"""Look up ``model_id`` in litellm's bundled cost map.
|
||||
@@ -474,23 +531,7 @@ def _update_model_sats_pricing(model: Model, sats_to_usd: float) -> Model:
|
||||
if (sats.max_cost or 0.0) < min_req_sats:
|
||||
sats.max_cost = min_req_sats
|
||||
|
||||
return Model(
|
||||
id=model.id,
|
||||
name=model.name,
|
||||
created=model.created,
|
||||
description=model.description,
|
||||
context_length=model.context_length,
|
||||
architecture=model.architecture,
|
||||
pricing=model.pricing,
|
||||
sats_pricing=sats,
|
||||
per_request_limits=model.per_request_limits,
|
||||
top_provider=model.top_provider,
|
||||
enabled=model.enabled,
|
||||
upstream_provider_id=model.upstream_provider_id,
|
||||
canonical_slug=model.canonical_slug,
|
||||
alias_ids=model.alias_ids,
|
||||
forwarded_model_id=model.forwarded_model_id,
|
||||
)
|
||||
return model.copy(update={"sats_pricing": sats})
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to update sats pricing for model",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Convert a Responses API ``input`` into chat ``messages`` via litellm.
|
||||
|
||||
litellm drops ``file_id`` (emits ``url: ""``) and nests a dict-form ``image_url``
|
||||
as-is, so ``input_image`` parts are flattened to ``{image_url: str, detail}`` first.
|
||||
``file_id`` becomes a sentinel URL the image walker treats as unfetchable.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from litellm.responses.litellm_completion_transformation.transformation import (
|
||||
LiteLLMCompletionResponsesConfig,
|
||||
)
|
||||
|
||||
from ..core import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
FILE_ID_URL_PREFIX = "file-id:"
|
||||
|
||||
|
||||
def _flatten_input_image(part: dict[str, Any]) -> tuple[str, str]:
|
||||
raw = part.get("image_url")
|
||||
url = raw.get("url", "") if isinstance(raw, dict) else raw
|
||||
detail = part.get("detail") or (
|
||||
raw.get("detail") if isinstance(raw, dict) else None
|
||||
)
|
||||
if not url and part.get("file_id"):
|
||||
url = f"{FILE_ID_URL_PREFIX}{part['file_id']}"
|
||||
return (url if isinstance(url, str) else ""), (detail or "auto")
|
||||
|
||||
|
||||
def _normalize_item(item: Any) -> Any:
|
||||
if not isinstance(item, dict):
|
||||
return item
|
||||
if item.get("type") == "input_image":
|
||||
url, detail = _flatten_input_image(item)
|
||||
return {**item, "image_url": url, "detail": detail}
|
||||
content = item.get("content")
|
||||
if isinstance(content, list):
|
||||
return {**item, "content": [_normalize_item(part) for part in content]}
|
||||
return item
|
||||
|
||||
|
||||
def input_image_part_to_image_url(part: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Reshape an ``input_image`` part found inside chat ``messages``."""
|
||||
url, detail = _flatten_input_image(part)
|
||||
return {"type": "image_url", "image_url": {"url": url, "detail": detail}}
|
||||
|
||||
|
||||
def count_input_images(input_data: Any) -> int:
|
||||
if isinstance(input_data, dict):
|
||||
own = 1 if input_data.get("type") == "input_image" else 0
|
||||
return own + count_input_images(input_data.get("content"))
|
||||
if isinstance(input_data, list):
|
||||
return sum(count_input_images(item) for item in input_data)
|
||||
return 0
|
||||
|
||||
|
||||
def responses_input_to_messages(input_data: Any) -> list[dict[str, Any]] | None:
|
||||
"""Returns ``None`` when the transform fails so the caller can worst-case."""
|
||||
if isinstance(input_data, str):
|
||||
return [{"role": "user", "content": input_data}]
|
||||
if not isinstance(input_data, list):
|
||||
return []
|
||||
try:
|
||||
normalized = [_normalize_item(item) for item in input_data]
|
||||
converted = (
|
||||
LiteLLMCompletionResponsesConfig.transform_responses_api_input_to_messages(
|
||||
input=normalized, # type: ignore[arg-type]
|
||||
responses_api_request={},
|
||||
)
|
||||
)
|
||||
return [dict(message) for message in converted]
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Responses input transform failed; using conservative image fallback",
|
||||
extra={"error": str(e)},
|
||||
)
|
||||
return None
|
||||
@@ -262,14 +262,6 @@ _ALLOWED_ENDPOINTS: dict[str, frozenset[str]] = {
|
||||
"responses": frozenset({"POST"}),
|
||||
"messages": frozenset({"POST"}),
|
||||
"embeddings": frozenset({"POST"}),
|
||||
"moderations": frozenset({"POST"}),
|
||||
"rerank": frozenset({"POST"}),
|
||||
"audio/speech": frozenset({"POST"}),
|
||||
"audio/transcriptions": frozenset({"POST"}),
|
||||
"audio/translations": frozenset({"POST"}),
|
||||
"images/generations": frozenset({"POST"}),
|
||||
"images/edits": frozenset({"POST"}),
|
||||
"images/variations": frozenset({"POST"}),
|
||||
"models": frozenset({"GET"}),
|
||||
"attestation": frozenset({"GET"}),
|
||||
"tee/attestation": frozenset({"GET"}),
|
||||
|
||||
+220
-131
@@ -64,6 +64,7 @@ from .cache_breakpoints import (
|
||||
from .count_tokens import MissingUsageEstimator, count_tokens_locally
|
||||
from .litellm_routing import detect_litellm_prefix
|
||||
from .rate_limit import UPSTREAM_RATE_LIMIT, classify_rate_limit
|
||||
from .reasoning_effort import apply_reasoning_effort
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .ehbp import ConfidentialInferenceProfile, EHBPForwardingTarget
|
||||
@@ -154,6 +155,35 @@ def _inject_cost_response_headers(
|
||||
headers["X-Routstr-Cost-Usd"] = str(total_usd)
|
||||
|
||||
|
||||
def _apply_estimated_usage(
|
||||
response_json: dict[str, Any],
|
||||
request_body: bytes | None,
|
||||
model_obj: Model | None,
|
||||
amount: int,
|
||||
unit: str,
|
||||
api: str,
|
||||
) -> None:
|
||||
"""Bill a buffered response from a local estimate when usage is missing."""
|
||||
if response_json.get("usage"):
|
||||
return
|
||||
estimator = MissingUsageEstimator(request_body, model_obj)
|
||||
estimator.observe(response_json)
|
||||
estimated = estimator.estimated_usage(response_json.get("model"))
|
||||
if not estimated:
|
||||
return
|
||||
logger.warning(
|
||||
"No usage in non-streaming response, billing from local token estimate",
|
||||
extra={
|
||||
"api": api,
|
||||
"model": response_json.get("model", "unknown"),
|
||||
"amount": amount,
|
||||
"unit": unit,
|
||||
"estimated_usage": estimated,
|
||||
},
|
||||
)
|
||||
response_json["usage"] = estimated
|
||||
|
||||
|
||||
def _parse_sse_events(content: str) -> list[tuple[list[str], str]]:
|
||||
"""Split a buffered SSE body into ``(field_lines, data)`` pairs.
|
||||
|
||||
@@ -463,6 +493,34 @@ class BaseUpstreamProvider:
|
||||
return
|
||||
response_json["provider"] = f"{provider_type}:{existing_str}"
|
||||
|
||||
def _log_full_refund(
|
||||
self,
|
||||
*,
|
||||
route: str,
|
||||
model: str | None,
|
||||
content_str: str,
|
||||
amount: int,
|
||||
unit: str,
|
||||
) -> None:
|
||||
"""Record a settlement that serves content but charges nothing.
|
||||
|
||||
The client keeps both the response and the whole prepayment, so the
|
||||
model, the serving upstream and a redacted body preview are logged to
|
||||
keep the unbilled request auditable.
|
||||
"""
|
||||
logger.warning(
|
||||
"Zero-cost settlement, refunding the full prepayment",
|
||||
extra={
|
||||
"route": route,
|
||||
"model": model or "unknown",
|
||||
"provider_type": self.provider_type,
|
||||
"upstream_base_url": self.base_url,
|
||||
"refund_amount": amount,
|
||||
"unit": unit,
|
||||
"response_body_preview": redact_org_ids(content_str.strip()[:500]),
|
||||
},
|
||||
)
|
||||
|
||||
def inject_cost_metadata(
|
||||
self,
|
||||
response_json: dict,
|
||||
@@ -654,8 +712,7 @@ class BaseUpstreamProvider:
|
||||
transformed_model = self.transform_model_name(original_model)
|
||||
data["input"]["model"] = transformed_model
|
||||
|
||||
# Ensure proper Responses API structure
|
||||
# Add any Responses-specific transformations here
|
||||
apply_reasoning_effort(data, model_obj)
|
||||
|
||||
return json.dumps(data).encode()
|
||||
except Exception as e:
|
||||
@@ -769,6 +826,9 @@ class BaseUpstreamProvider:
|
||||
if inject_anthropic_cache_breakpoints(data):
|
||||
changed = True
|
||||
|
||||
if apply_reasoning_effort(data, model_obj):
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
return json.dumps(data).encode()
|
||||
return body
|
||||
@@ -3729,23 +3789,17 @@ class BaseUpstreamProvider:
|
||||
)
|
||||
return cost
|
||||
case CostDataError() as error:
|
||||
# Content was already served, so refund instead of raising.
|
||||
logger.error(
|
||||
"Cost calculation error",
|
||||
"Cost calculation error, refunding the prepayment",
|
||||
extra={
|
||||
"model": model,
|
||||
"error_message": error.message,
|
||||
"error_code": error.code,
|
||||
},
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": {
|
||||
"message": error.message,
|
||||
"type": "invalid_request_error",
|
||||
"code": error.code,
|
||||
}
|
||||
},
|
||||
return MaxCostData(
|
||||
base_msats=0, input_msats=0, output_msats=0, total_msats=0
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -3828,6 +3882,7 @@ class BaseUpstreamProvider:
|
||||
mint: str | None = None,
|
||||
request_id: str | None = None,
|
||||
model_obj: Model | None = None,
|
||||
request_body: bytes | None = None,
|
||||
) -> StreamingResponse:
|
||||
"""Handle streaming response for X-Cashu payment, calculating refund if needed.
|
||||
|
||||
@@ -3859,6 +3914,7 @@ class BaseUpstreamProvider:
|
||||
usage_data = None
|
||||
model = None
|
||||
cost_data: CostData | MaxCostData | None = None
|
||||
usage_estimator = MissingUsageEstimator(request_body, model_obj)
|
||||
|
||||
lines = content_str.strip().split("\n")
|
||||
for line in lines:
|
||||
@@ -3886,87 +3942,108 @@ class BaseUpstreamProvider:
|
||||
usage_data = merged
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
usage_estimator.observe(data_json)
|
||||
|
||||
if usage_data and model:
|
||||
logger.debug(
|
||||
"Found usage data in streaming response",
|
||||
if not usage_data:
|
||||
usage_data = usage_estimator.estimated_usage(model)
|
||||
if usage_data:
|
||||
logger.warning(
|
||||
"No usage in streaming response, billing from local token estimate",
|
||||
extra={
|
||||
"model": model,
|
||||
"amount": amount,
|
||||
"unit": unit,
|
||||
"estimated_usage": usage_data,
|
||||
},
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"Calculating cost for streaming response",
|
||||
extra={
|
||||
"model": model,
|
||||
"usage_data": usage_data,
|
||||
"amount": amount,
|
||||
"unit": unit,
|
||||
},
|
||||
)
|
||||
|
||||
response_data = {"usage": usage_data, "model": model or "unknown"}
|
||||
try:
|
||||
cost_data = await self.get_x_cashu_cost(
|
||||
response_data, max_cost_for_model, model_obj
|
||||
)
|
||||
if cost_data is not None and cost_data.total_msats == 0:
|
||||
self._log_full_refund(
|
||||
route="chat.streaming",
|
||||
model=model,
|
||||
content_str=content_str,
|
||||
amount=amount,
|
||||
unit=unit,
|
||||
)
|
||||
if cost_data:
|
||||
if unit == "msat":
|
||||
refund_amount = amount - cost_data.total_msats
|
||||
elif unit == "sat":
|
||||
refund_amount = amount - (cost_data.total_msats + 999) // 1000
|
||||
else:
|
||||
raise ValueError(f"Invalid unit: {unit}")
|
||||
|
||||
if refund_amount > 0:
|
||||
logger.debug(
|
||||
"Processing refund for streaming response",
|
||||
extra={
|
||||
"original_amount": amount,
|
||||
"cost_msats": cost_data.total_msats,
|
||||
"refund_amount": refund_amount,
|
||||
"unit": unit,
|
||||
"model": model,
|
||||
},
|
||||
)
|
||||
|
||||
refund_token = await self.send_refund(
|
||||
refund_amount,
|
||||
unit,
|
||||
mint,
|
||||
request_id=request_id,
|
||||
)
|
||||
response_headers["X-Cashu"] = refund_token
|
||||
|
||||
logger.info(
|
||||
"Refund processed for streaming response",
|
||||
extra={
|
||||
"refund_amount": refund_amount,
|
||||
"unit": unit,
|
||||
"refund_token_preview": refund_token[:20] + "..."
|
||||
if len(refund_token) > 20
|
||||
else refund_token,
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"No refund needed for streaming response",
|
||||
extra={
|
||||
"amount": amount,
|
||||
"cost_msats": cost_data.total_msats,
|
||||
"model": model,
|
||||
},
|
||||
)
|
||||
|
||||
# Inject cost breakdown headers so the SDK's
|
||||
# extractUsageFromResponseHeaders can populate
|
||||
# inputMsats/outputMsats/totalMsats for x-cashu requests.
|
||||
_inject_cost_response_headers(response_headers, cost_data)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error calculating cost for streaming response",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
"model": model,
|
||||
"usage_data": usage_data,
|
||||
"amount": amount,
|
||||
"unit": unit,
|
||||
},
|
||||
)
|
||||
|
||||
response_data = {"usage": usage_data, "model": model}
|
||||
try:
|
||||
cost_data = await self.get_x_cashu_cost(
|
||||
response_data, max_cost_for_model, model_obj
|
||||
)
|
||||
if cost_data:
|
||||
if unit == "msat":
|
||||
refund_amount = amount - cost_data.total_msats
|
||||
elif unit == "sat":
|
||||
refund_amount = amount - (cost_data.total_msats + 999) // 1000
|
||||
else:
|
||||
raise ValueError(f"Invalid unit: {unit}")
|
||||
|
||||
if refund_amount > 0:
|
||||
logger.debug(
|
||||
"Processing refund for streaming response",
|
||||
extra={
|
||||
"original_amount": amount,
|
||||
"cost_msats": cost_data.total_msats,
|
||||
"refund_amount": refund_amount,
|
||||
"unit": unit,
|
||||
"model": model,
|
||||
},
|
||||
)
|
||||
|
||||
refund_token = await self.send_refund(
|
||||
refund_amount,
|
||||
unit,
|
||||
mint,
|
||||
request_id=request_id,
|
||||
)
|
||||
response_headers["X-Cashu"] = refund_token
|
||||
|
||||
logger.info(
|
||||
"Refund processed for streaming response",
|
||||
extra={
|
||||
"refund_amount": refund_amount,
|
||||
"unit": unit,
|
||||
"refund_token_preview": refund_token[:20] + "..."
|
||||
if len(refund_token) > 20
|
||||
else refund_token,
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"No refund needed for streaming response",
|
||||
extra={
|
||||
"amount": amount,
|
||||
"cost_msats": cost_data.total_msats,
|
||||
"model": model,
|
||||
},
|
||||
)
|
||||
|
||||
# Inject cost breakdown headers so the SDK's
|
||||
# extractUsageFromResponseHeaders can populate
|
||||
# inputMsats/outputMsats/totalMsats for x-cashu requests.
|
||||
_inject_cost_response_headers(response_headers, cost_data)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error calculating cost for streaming response",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
"model": model,
|
||||
"amount": amount,
|
||||
"unit": unit,
|
||||
},
|
||||
)
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("data: "):
|
||||
try:
|
||||
@@ -4005,6 +4082,7 @@ class BaseUpstreamProvider:
|
||||
mint: str | None = None,
|
||||
request_id: str | None = None,
|
||||
model_obj: Model | None = None,
|
||||
request_body: bytes | None = None,
|
||||
) -> Response:
|
||||
"""Handle non-streaming response for X-Cashu payment, calculating refund if needed.
|
||||
|
||||
@@ -4026,9 +4104,20 @@ class BaseUpstreamProvider:
|
||||
try:
|
||||
response_json = json.loads(content_str)
|
||||
self._apply_provider_field(response_json)
|
||||
_apply_estimated_usage(
|
||||
response_json, request_body, model_obj, amount, unit, "chat"
|
||||
)
|
||||
cost_data = await self.get_x_cashu_cost(
|
||||
response_json, max_cost_for_model, model_obj
|
||||
)
|
||||
if cost_data is not None and cost_data.total_msats == 0:
|
||||
self._log_full_refund(
|
||||
route="chat",
|
||||
model=response_json.get("model"),
|
||||
content_str=content_str,
|
||||
amount=amount,
|
||||
unit=unit,
|
||||
)
|
||||
|
||||
if cost_data and "usage" in response_json:
|
||||
# Inject cost breakdown into both the response body (so the
|
||||
@@ -4160,6 +4249,7 @@ class BaseUpstreamProvider:
|
||||
mint: str | None = None,
|
||||
request_id: str | None = None,
|
||||
model_obj: Model | None = None,
|
||||
request_body: bytes | None = None,
|
||||
) -> StreamingResponse | Response:
|
||||
"""Handle chat completion response for X-Cashu payment, detecting streaming vs non-streaming.
|
||||
|
||||
@@ -4206,6 +4296,7 @@ class BaseUpstreamProvider:
|
||||
mint,
|
||||
request_id=request_id,
|
||||
model_obj=model_obj,
|
||||
request_body=request_body,
|
||||
)
|
||||
else:
|
||||
return await self.handle_x_cashu_non_streaming_response(
|
||||
@@ -4217,6 +4308,7 @@ class BaseUpstreamProvider:
|
||||
mint,
|
||||
request_id=request_id,
|
||||
model_obj=model_obj,
|
||||
request_body=request_body,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -4414,6 +4506,7 @@ class BaseUpstreamProvider:
|
||||
mint,
|
||||
request_id=getattr(request.state, "request_id", None),
|
||||
model_obj=model_obj,
|
||||
request_body=request_body,
|
||||
)
|
||||
background_tasks = BackgroundTasks()
|
||||
background_tasks.add_task(response.aclose)
|
||||
@@ -4710,6 +4803,7 @@ class BaseUpstreamProvider:
|
||||
mint,
|
||||
request_id=getattr(request.state, "request_id", None),
|
||||
model_obj=model_obj,
|
||||
request_body=request_body,
|
||||
)
|
||||
background_tasks = BackgroundTasks()
|
||||
background_tasks.add_task(response.aclose)
|
||||
@@ -4761,6 +4855,7 @@ class BaseUpstreamProvider:
|
||||
mint: str | None = None,
|
||||
request_id: str | None = None,
|
||||
model_obj: Model | None = None,
|
||||
request_body: bytes | None = None,
|
||||
) -> StreamingResponse | Response:
|
||||
"""Handle Responses API completion response for X-Cashu payment.
|
||||
|
||||
@@ -4808,6 +4903,7 @@ class BaseUpstreamProvider:
|
||||
mint,
|
||||
request_id=request_id,
|
||||
model_obj=model_obj,
|
||||
request_body=request_body,
|
||||
)
|
||||
else:
|
||||
return await self.handle_x_cashu_non_streaming_responses_response(
|
||||
@@ -4819,6 +4915,7 @@ class BaseUpstreamProvider:
|
||||
mint,
|
||||
request_id=request_id,
|
||||
model_obj=model_obj,
|
||||
request_body=request_body,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -4847,6 +4944,7 @@ class BaseUpstreamProvider:
|
||||
mint: str | None = None,
|
||||
request_id: str | None = None,
|
||||
model_obj: Model | None = None,
|
||||
request_body: bytes | None = None,
|
||||
) -> StreamingResponse:
|
||||
"""Handle streaming Responses API response for X-Cashu payment.
|
||||
|
||||
@@ -4873,6 +4971,7 @@ class BaseUpstreamProvider:
|
||||
model: str | None = None
|
||||
reasoning_tokens = 0
|
||||
cost_data: CostData | MaxCostData | None = None
|
||||
usage_estimator = MissingUsageEstimator(request_body, model_obj)
|
||||
|
||||
for _fields, data in events:
|
||||
if data.strip() == "[DONE]":
|
||||
@@ -4883,6 +4982,7 @@ class BaseUpstreamProvider:
|
||||
continue
|
||||
if not isinstance(data_json, dict):
|
||||
continue
|
||||
usage_estimator.observe(data_json)
|
||||
# Canonical Responses API events carry model and usage nested under
|
||||
# "response" (response.completed/incomplete); older shapes put them
|
||||
# at the top level.
|
||||
@@ -4898,19 +4998,18 @@ class BaseUpstreamProvider:
|
||||
elif not model and payload.get("model"):
|
||||
model = payload["model"]
|
||||
|
||||
if usage_data is None:
|
||||
# No request body is available at this X-Cashu settlement seam, so
|
||||
# an auditable input/output estimate cannot be built. Refund the
|
||||
# token rather than treating the authorization ceiling as usage.
|
||||
logger.warning(
|
||||
"No usage in streaming Responses API response — refunding instead of charging the authorized max",
|
||||
extra={
|
||||
"model": model,
|
||||
"amount": amount,
|
||||
"unit": unit,
|
||||
"max_cost_msats": max_cost_for_model,
|
||||
},
|
||||
)
|
||||
if not usage_data:
|
||||
usage_data = usage_estimator.estimated_usage(model)
|
||||
if usage_data:
|
||||
logger.warning(
|
||||
"No usage in streaming Responses API response, billing from local token estimate",
|
||||
extra={
|
||||
"model": model,
|
||||
"amount": amount,
|
||||
"unit": unit,
|
||||
"estimated_usage": usage_data,
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"Found usage data in streaming Responses API response",
|
||||
@@ -4928,6 +5027,14 @@ class BaseUpstreamProvider:
|
||||
cost_data = await self.get_x_cashu_cost(
|
||||
response_data, max_cost_for_model, model_obj
|
||||
)
|
||||
if cost_data is not None and cost_data.total_msats == 0:
|
||||
self._log_full_refund(
|
||||
route="responses.streaming",
|
||||
model=model,
|
||||
content_str=content_str,
|
||||
amount=amount,
|
||||
unit=unit,
|
||||
)
|
||||
if cost_data:
|
||||
if unit == "msat":
|
||||
refund_amount = amount - cost_data.total_msats
|
||||
@@ -5033,6 +5140,7 @@ class BaseUpstreamProvider:
|
||||
mint: str | None = None,
|
||||
request_id: str | None = None,
|
||||
model_obj: Model | None = None,
|
||||
request_body: bytes | None = None,
|
||||
) -> Response:
|
||||
"""Handle non-streaming Responses API response for X-Cashu payment."""
|
||||
logger.debug(
|
||||
@@ -5043,9 +5151,20 @@ class BaseUpstreamProvider:
|
||||
try:
|
||||
response_json = json.loads(content_str)
|
||||
self._apply_provider_field(response_json)
|
||||
_apply_estimated_usage(
|
||||
response_json, request_body, model_obj, amount, unit, "responses"
|
||||
)
|
||||
cost_data = await self.get_x_cashu_cost(
|
||||
response_json, max_cost_for_model, model_obj
|
||||
)
|
||||
if cost_data is not None and cost_data.total_msats == 0:
|
||||
self._log_full_refund(
|
||||
route="responses",
|
||||
model=response_json.get("model"),
|
||||
content_str=content_str,
|
||||
amount=amount,
|
||||
unit=unit,
|
||||
)
|
||||
|
||||
if cost_data and "usage" in response_json:
|
||||
_inject_cost_into_usage(response_json, cost_data)
|
||||
@@ -5319,22 +5438,8 @@ class BaseUpstreamProvider:
|
||||
{k: v * self.provider_fee for k, v in base_pricing.dict().items()}
|
||||
)
|
||||
|
||||
temp_model = Model(
|
||||
id=model.id,
|
||||
name=model.name,
|
||||
created=model.created,
|
||||
description=model.description,
|
||||
context_length=model.context_length,
|
||||
architecture=model.architecture,
|
||||
pricing=adjusted_pricing,
|
||||
sats_pricing=None,
|
||||
per_request_limits=model.per_request_limits,
|
||||
top_provider=model.top_provider,
|
||||
enabled=model.enabled,
|
||||
upstream_provider_id=model.upstream_provider_id,
|
||||
canonical_slug=model.canonical_slug,
|
||||
alias_ids=model.alias_ids,
|
||||
forwarded_model_id=model.forwarded_model_id,
|
||||
temp_model = model.copy(
|
||||
update={"pricing": adjusted_pricing, "sats_pricing": None}
|
||||
)
|
||||
|
||||
(
|
||||
@@ -5343,23 +5448,7 @@ class BaseUpstreamProvider:
|
||||
adjusted_pricing.max_cost,
|
||||
) = _calculate_usd_max_costs(temp_model)
|
||||
|
||||
return Model(
|
||||
id=model.id,
|
||||
name=model.name,
|
||||
created=model.created,
|
||||
description=model.description,
|
||||
context_length=model.context_length,
|
||||
architecture=model.architecture,
|
||||
pricing=adjusted_pricing,
|
||||
sats_pricing=model.sats_pricing,
|
||||
per_request_limits=model.per_request_limits,
|
||||
top_provider=model.top_provider,
|
||||
enabled=model.enabled,
|
||||
upstream_provider_id=model.upstream_provider_id,
|
||||
canonical_slug=model.canonical_slug,
|
||||
alias_ids=model.alias_ids,
|
||||
forwarded_model_id=model.forwarded_model_id,
|
||||
)
|
||||
return model.copy(update={"pricing": adjusted_pricing})
|
||||
|
||||
async def fetch_models(self) -> list[Model]:
|
||||
"""Fetch available models from upstream API and update cache.
|
||||
|
||||
@@ -57,6 +57,37 @@ def _count_with_litellm(
|
||||
if not isinstance(messages, list):
|
||||
messages = []
|
||||
|
||||
if "input" in body:
|
||||
response_input = body["input"]
|
||||
if isinstance(response_input, str):
|
||||
messages = [{"role": "user", "content": response_input}]
|
||||
elif isinstance(response_input, list):
|
||||
messages = []
|
||||
for item in response_input:
|
||||
if not isinstance(item, dict) or "role" not in item:
|
||||
raise ValueError(
|
||||
"Responses input requires fallback token estimation"
|
||||
)
|
||||
content = item.get("content", "")
|
||||
if isinstance(content, list):
|
||||
parts = []
|
||||
for part in content:
|
||||
if not isinstance(part, dict) or part.get("type") not in (
|
||||
"input_text",
|
||||
"output_text",
|
||||
"text",
|
||||
):
|
||||
raise ValueError(
|
||||
"Non-text Responses input requires fallback token estimation"
|
||||
)
|
||||
parts.append({"type": "text", "text": part.get("text", "")})
|
||||
content = parts
|
||||
messages.append({"role": item["role"], "content": content})
|
||||
else:
|
||||
raise ValueError("Unsupported Responses input")
|
||||
if body.get("instructions"):
|
||||
messages.insert(0, {"role": "system", "content": body["instructions"]})
|
||||
|
||||
prompt_token_ids = 0
|
||||
if include_legacy_prompt:
|
||||
prompt = body.get("prompt")
|
||||
@@ -177,12 +208,26 @@ class MissingUsageEstimator:
|
||||
def observe(self, response_data: object) -> None:
|
||||
if isinstance(response_data, dict):
|
||||
event_type = response_data.get("type")
|
||||
if event_type in ("response.completed", "response.incomplete"):
|
||||
response = response_data.get("response")
|
||||
if isinstance(response, dict) and isinstance(
|
||||
response.get("output"), list
|
||||
):
|
||||
# Terminal output is a snapshot, not another text delta.
|
||||
self._output_parts = _generated_text(response["output"])
|
||||
return
|
||||
if isinstance(event_type, str) and event_type.endswith(".done"):
|
||||
# Responses API ``*.done`` events repeat text already streamed
|
||||
# via ``*.delta`` events; counting both would double-bill.
|
||||
return
|
||||
self._output_parts.extend(_generated_text(response_data))
|
||||
|
||||
def estimated_usage(self, model: str | None = None) -> dict[str, Any] | None:
|
||||
"""Local usage estimate, or None when the upstream generated no text."""
|
||||
if not self.output_text:
|
||||
return None
|
||||
return self.response_data(model)["usage"]
|
||||
|
||||
def billing_data(
|
||||
self,
|
||||
response_data: dict[str, Any] | None,
|
||||
|
||||
+133
-2
@@ -31,7 +31,7 @@ from ..core.db import (
|
||||
from ..core.db import (
|
||||
store_cashu_transaction_with_retry as store_cashu_transaction,
|
||||
)
|
||||
from ..core.exceptions import UpstreamError
|
||||
from ..core.exceptions import EhbpTimeoutError, UpstreamError
|
||||
from ..core.settings import settings
|
||||
from ..payment.cost_calculation import (
|
||||
CostData,
|
||||
@@ -46,7 +46,7 @@ from ..wallet import (
|
||||
recieve_token,
|
||||
send_token,
|
||||
)
|
||||
from .tinfoil_trailer import forward_with_trailer
|
||||
from .tinfoil_trailer import TrailerResponse, forward_with_trailer
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -62,6 +62,55 @@ _TINFOIL_ALLOWED_ENCLAVE_HOST_SUFFIX = ".tinfoil.sh"
|
||||
_TINFOIL_ALLOWED_ENCLAVE_HOSTS = frozenset({"tinfoil.sh"})
|
||||
|
||||
|
||||
_KEY_CONFIG_PROBLEM_TYPE = "urn:ietf:params:ehbp:error:key-config"
|
||||
|
||||
|
||||
def _is_ehbp_key_config_response(resp: TrailerResponse) -> bool:
|
||||
"""Check whether an upstream EHBP response is a key-config mismatch.
|
||||
|
||||
The enclave returns ``422 application/problem+json`` with
|
||||
``type=urn:ietf:params:ehbp:error:key-config`` when it cannot decrypt the
|
||||
request body — meaning the client's HPKE key is stale (the enclave rotated
|
||||
keys). The proxy must pass this response through with its original
|
||||
content type so EHBP clients can detect it and trigger re-attestation.
|
||||
"""
|
||||
if resp.status_code != 422:
|
||||
return False
|
||||
ct = ""
|
||||
for k, v in resp.headers:
|
||||
if k.lower() == "content-type":
|
||||
ct = v.lower()
|
||||
break
|
||||
media_type = ct.split(";", 1)[0].strip()
|
||||
if media_type != "application/problem+json":
|
||||
return False
|
||||
try:
|
||||
body = json.loads(resp.body)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
return False
|
||||
return isinstance(body, dict) and body.get("type") == _KEY_CONFIG_PROBLEM_TYPE
|
||||
|
||||
|
||||
def _passthrough_key_config_response(resp: TrailerResponse) -> Response:
|
||||
"""Return the enclave's key-config 422 with its original body and content
|
||||
type so the EHBP client's ``KeyConfigMismatchError`` detection fires.
|
||||
|
||||
Only the content type is forwarded. ``Ehbp-Response-Nonce`` must be
|
||||
dropped: a nonce only carries meaning for an *encrypted* response body,
|
||||
and the stock ``ehbp`` client (``shouldDecryptResponse``) checks for the
|
||||
nonce *before* checking for a key-config mismatch — forwarding it would
|
||||
send that client down the decrypt path on this plaintext error body, so
|
||||
the re-attestation loop would never fire. Content-length is recomputed
|
||||
from the body, and upstream-internal headers are filtered out.
|
||||
"""
|
||||
return Response(
|
||||
content=resp.body,
|
||||
status_code=422,
|
||||
headers={"content-type": "application/problem+json"},
|
||||
media_type="application/problem+json",
|
||||
)
|
||||
|
||||
|
||||
def _normalize_upstream_model_id(model_id: str | None) -> str:
|
||||
"""Normalize casing and whitespace for upstream identity comparisons."""
|
||||
if not model_id:
|
||||
@@ -722,6 +771,25 @@ async def forward_ehbp_request(
|
||||
"body_preview": body_preview,
|
||||
},
|
||||
)
|
||||
# Key-config mismatch (stale client HPKE key): return the
|
||||
# enclave's 422 problem+json directly so the SDK's
|
||||
# KeyConfigMismatchError detection fires and triggers
|
||||
# re-attestation. Wrapping it as application/json would
|
||||
# destroy the signal and cause permanent failure.
|
||||
if _is_ehbp_key_config_response(resp):
|
||||
logger.warning(
|
||||
"EHBP upstream %s returned key-config mismatch for model=%s, "
|
||||
"passing through for client re-attestation",
|
||||
provider_type,
|
||||
model_obj.id,
|
||||
extra={
|
||||
"provider": provider_type,
|
||||
"model": model_obj.id,
|
||||
"path": path,
|
||||
},
|
||||
)
|
||||
return _passthrough_key_config_response(resp)
|
||||
|
||||
raise UpstreamError(
|
||||
f"EHBP upstream {provider_type} returned {resp.status_code} "
|
||||
f"for model {model_obj.id}: {body_preview[:200] or '<empty>'}",
|
||||
@@ -938,6 +1006,29 @@ async def forward_ehbp_x_cashu_request(
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
# Key-config mismatch (stale client HPKE key): refund the
|
||||
# full token and pass the enclave's 422 problem+json through
|
||||
# so the SDK's KeyConfigMismatchError detection fires.
|
||||
if _is_ehbp_key_config_response(resp):
|
||||
logger.warning(
|
||||
"EHBP upstream %s returned key-config mismatch for "
|
||||
"model=%s, refunding and passing through",
|
||||
provider_type,
|
||||
model_obj.id,
|
||||
extra={
|
||||
"provider": provider_type,
|
||||
"model": model_obj.id,
|
||||
"path": path,
|
||||
"refunded_amount": amount,
|
||||
},
|
||||
)
|
||||
refund_token = await send_cashu_refund(
|
||||
amount, unit, mint, request_id
|
||||
)
|
||||
passthrough = _passthrough_key_config_response(resp)
|
||||
passthrough.headers["X-Cashu"] = refund_token
|
||||
return passthrough
|
||||
|
||||
refund_token = await send_cashu_refund(amount, unit, mint, request_id)
|
||||
error_response = Response(
|
||||
content=json.dumps(
|
||||
@@ -1042,6 +1133,46 @@ async def forward_ehbp_x_cashu_request(
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
except EhbpTimeoutError as e:
|
||||
logger.warning(
|
||||
"EHBP X-Cashu upstream timed out",
|
||||
extra={
|
||||
"error": str(e),
|
||||
"path": path,
|
||||
"method": request.method,
|
||||
"redeemed": redeemed,
|
||||
},
|
||||
)
|
||||
|
||||
if redeemed and amount > 0:
|
||||
try:
|
||||
refund_token = await send_cashu_refund(amount, unit, mint, request_id)
|
||||
error_response = create_error_response(
|
||||
"upstream_timeout",
|
||||
str(e),
|
||||
504,
|
||||
request=request,
|
||||
code="UPSTREAM_TIMEOUT",
|
||||
)
|
||||
error_response.headers["X-Cashu"] = refund_token
|
||||
return error_response
|
||||
except Exception as refund_error:
|
||||
logger.error(
|
||||
"Failed to refund EHBP X-Cashu token after timeout",
|
||||
extra={
|
||||
"error": str(refund_error),
|
||||
"original_error": str(e),
|
||||
},
|
||||
)
|
||||
|
||||
return create_error_response(
|
||||
"upstream_timeout",
|
||||
str(e),
|
||||
504,
|
||||
request=request,
|
||||
code="UPSTREAM_TIMEOUT",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
logger.error(
|
||||
|
||||
@@ -32,6 +32,7 @@ from ..core.exceptions import UpstreamError
|
||||
from ..core.redaction import redact_org_ids
|
||||
from ..payment.models import Model
|
||||
from .rate_limit import classify_rate_limit
|
||||
from .reasoning_effort import adapt_messages_body_for_litellm
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -71,6 +72,8 @@ ALLOWED_MESSAGES_REQUEST_FIELDS: frozenset[str] = frozenset(
|
||||
"tools",
|
||||
"tool_choice",
|
||||
"metadata",
|
||||
# OpenAI-shaped effort after thinking is lifted off Anthropic bodies.
|
||||
"reasoning_effort",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -131,9 +134,7 @@ def parse_sse_blocks(buffer: bytes) -> tuple[list[dict], bytes]:
|
||||
return events, buffer
|
||||
|
||||
|
||||
def events_from_chunk(
|
||||
chunk: object, sse_buffer: bytes
|
||||
) -> tuple[list[dict], bytes]:
|
||||
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
|
||||
@@ -224,9 +225,7 @@ async def aggregate_anthropic_events_to_message(
|
||||
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 {}
|
||||
)
|
||||
blocks[idx]["input"] = json.loads(raw_json) if raw_json else {}
|
||||
except json.JSONDecodeError:
|
||||
blocks[idx]["input"] = raw_json
|
||||
elif etype == "message_delta":
|
||||
@@ -468,9 +467,7 @@ async def dispatch_anthropic_messages(
|
||||
on bad input or upstream failure.
|
||||
"""
|
||||
if not request_body:
|
||||
raise UpstreamError(
|
||||
"Missing request body for /v1/messages", status_code=400
|
||||
)
|
||||
raise UpstreamError("Missing request body for /v1/messages", status_code=400)
|
||||
|
||||
try:
|
||||
body: dict = json.loads(request_body)
|
||||
@@ -489,6 +486,8 @@ async def dispatch_anthropic_messages(
|
||||
client_stream = bool(body.pop("stream", False))
|
||||
upstream_stream = True
|
||||
|
||||
adapt_messages_body_for_litellm(body, model_obj)
|
||||
|
||||
# Forward only allowlisted Anthropic Messages request fields. Any
|
||||
# other client-supplied key is dropped so it cannot leak into the
|
||||
# upstream request. See ALLOWED_MESSAGES_REQUEST_FIELDS.
|
||||
@@ -498,9 +497,7 @@ async def dispatch_anthropic_messages(
|
||||
"Dropped non-forwardable fields before litellm dispatch",
|
||||
extra={"dropped_keys": dropped},
|
||||
)
|
||||
body = {
|
||||
k: v for k, v in body.items() if k in ALLOWED_MESSAGES_REQUEST_FIELDS
|
||||
}
|
||||
body = {k: v for k, v in body.items() if k in ALLOWED_MESSAGES_REQUEST_FIELDS}
|
||||
|
||||
# Convention: `model.id` is the canonical upstream model name;
|
||||
# `forwarded_model_id` is the public alias the internal API exposes
|
||||
|
||||
@@ -66,9 +66,7 @@ class OllamaUpstreamProvider(BaseUpstreamProvider):
|
||||
"""Strip 'ollama/' prefix for Ollama API compatibility."""
|
||||
return model_id.removeprefix("ollama/")
|
||||
|
||||
def get_request_base_url(
|
||||
self, path: str, model_obj: Model | None = None
|
||||
) -> str:
|
||||
def get_request_base_url(self, path: str, model_obj: Model | None = None) -> str:
|
||||
"""Route proxy traffic through Ollama's OpenAI-compatible /v1 endpoint."""
|
||||
return f"{self.base_url.rstrip('/')}/v1"
|
||||
|
||||
@@ -185,7 +183,9 @@ class OllamaUpstreamProvider(BaseUpstreamProvider):
|
||||
except Exception:
|
||||
self._models_cache = models_with_fees
|
||||
|
||||
self._models_by_id = {m.forwarded_model_id or m.id: m for m in self._models_cache}
|
||||
self._models_by_id = {
|
||||
m.forwarded_model_id or m.id: m for m in self._models_cache
|
||||
}
|
||||
logger.info(
|
||||
f"Refreshed models cache for {self.base_url}",
|
||||
extra={"model_count": len(models)},
|
||||
@@ -224,26 +224,14 @@ class OllamaUpstreamProvider(BaseUpstreamProvider):
|
||||
Returns:
|
||||
Model with provider fee applied to pricing and max costs calculated
|
||||
"""
|
||||
from ..payment.models import Model, Pricing, _calculate_usd_max_costs
|
||||
from ..payment.models import Pricing, _calculate_usd_max_costs
|
||||
|
||||
adjusted_pricing = Pricing.parse_obj(
|
||||
{k: v * self.provider_fee for k, v in model.pricing.dict().items()}
|
||||
)
|
||||
|
||||
temp_model = Model(
|
||||
id=model.id,
|
||||
name=model.name,
|
||||
created=model.created,
|
||||
description=model.description,
|
||||
context_length=model.context_length,
|
||||
architecture=model.architecture,
|
||||
pricing=adjusted_pricing,
|
||||
sats_pricing=None,
|
||||
per_request_limits=model.per_request_limits,
|
||||
top_provider=model.top_provider,
|
||||
enabled=model.enabled,
|
||||
upstream_provider_id=model.upstream_provider_id,
|
||||
canonical_slug=model.canonical_slug,
|
||||
temp_model = model.copy(
|
||||
update={"pricing": adjusted_pricing, "sats_pricing": None}
|
||||
)
|
||||
|
||||
(
|
||||
@@ -252,18 +240,4 @@ class OllamaUpstreamProvider(BaseUpstreamProvider):
|
||||
adjusted_pricing.max_cost,
|
||||
) = _calculate_usd_max_costs(temp_model)
|
||||
|
||||
return Model(
|
||||
id=model.id,
|
||||
name=model.name,
|
||||
created=model.created,
|
||||
description=model.description,
|
||||
context_length=model.context_length,
|
||||
architecture=model.architecture,
|
||||
pricing=adjusted_pricing,
|
||||
sats_pricing=model.sats_pricing,
|
||||
per_request_limits=model.per_request_limits,
|
||||
top_provider=model.top_provider,
|
||||
enabled=model.enabled,
|
||||
upstream_provider_id=model.upstream_provider_id,
|
||||
canonical_slug=model.canonical_slug,
|
||||
)
|
||||
return model.copy(update={"pricing": adjusted_pricing})
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Map client reasoning/thinking effort onto a model's allowlist.
|
||||
|
||||
OpenRouter (and some other catalogs) publish per-model reasoning metadata:
|
||||
which effort levels are legal, the default, and whether reasoning is
|
||||
mandatory. Clients still send the generic OpenAI / Anthropic shapes
|
||||
(``reasoning_effort``, ``reasoning.effort``, ``thinking``). This module
|
||||
normalizes those into a supported effort and writes the fields the
|
||||
upstream actually accepts, instead of dropping the parameter or
|
||||
forwarding a value the model rejects.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ..payment.models import Model, Reasoning
|
||||
|
||||
# Highest first. Unknown values are treated as unranked.
|
||||
EFFORT_RANK: tuple[str, ...] = (
|
||||
"max",
|
||||
"xhigh",
|
||||
"high",
|
||||
"medium",
|
||||
"low",
|
||||
"minimal",
|
||||
"none",
|
||||
)
|
||||
_RANK_INDEX: dict[str, int] = {name: i for i, name in enumerate(EFFORT_RANK)}
|
||||
|
||||
_REASONING_KEYS = ("reasoning", "reasoning_effort", "thinking")
|
||||
|
||||
|
||||
def _normalize_effort(value: object) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
cleaned = value.strip().lower()
|
||||
return cleaned or None
|
||||
|
||||
|
||||
def closest_supported_effort(
|
||||
requested: str | None,
|
||||
supported: list[str],
|
||||
*,
|
||||
default_effort: str | None = None,
|
||||
mandatory: bool = False,
|
||||
) -> str | None:
|
||||
"""Pick a legal effort for ``requested``.
|
||||
|
||||
Exact match wins. Otherwise the nearest rank in ``EFFORT_RANK`` is
|
||||
used (preferring the higher neighbour on a tie). ``none`` is rejected
|
||||
when ``mandatory`` is set. Missing / unmapped requests fall back to
|
||||
``default_effort``, then the highest remaining supported level.
|
||||
"""
|
||||
allowed_efforts: list[str] = [
|
||||
normalized
|
||||
for item in supported
|
||||
if (normalized := _normalize_effort(item)) is not None
|
||||
]
|
||||
if mandatory:
|
||||
allowed_efforts = [item for item in allowed_efforts if item != "none"]
|
||||
if not allowed_efforts:
|
||||
if mandatory:
|
||||
return _normalize_effort(default_effort)
|
||||
return _normalize_effort(requested) or _normalize_effort(default_effort)
|
||||
|
||||
default = _normalize_effort(default_effort)
|
||||
if default not in allowed_efforts:
|
||||
default = allowed_efforts[0]
|
||||
|
||||
requested_norm = _normalize_effort(requested)
|
||||
if requested_norm is None or (requested_norm == "none" and mandatory):
|
||||
return default
|
||||
|
||||
if requested_norm in allowed_efforts:
|
||||
return requested_norm
|
||||
|
||||
if requested_norm not in _RANK_INDEX:
|
||||
return default
|
||||
|
||||
target = _RANK_INDEX[requested_norm]
|
||||
return min(
|
||||
allowed_efforts,
|
||||
key=lambda effort: (
|
||||
abs(_RANK_INDEX.get(effort, 10_000) - target),
|
||||
_RANK_INDEX.get(effort, 10_000),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def resolve_effort(requested: str | None, reasoning: Reasoning | None) -> str | None:
|
||||
"""Map ``requested`` through ``reasoning`` metadata when present."""
|
||||
if reasoning is None:
|
||||
return _normalize_effort(requested)
|
||||
supported = reasoning.supported_efforts or []
|
||||
return closest_supported_effort(
|
||||
requested,
|
||||
supported,
|
||||
default_effort=reasoning.default_effort,
|
||||
mandatory=bool(reasoning.mandatory),
|
||||
)
|
||||
|
||||
|
||||
def _effort_from_thinking(thinking: object) -> str | None:
|
||||
if not isinstance(thinking, dict):
|
||||
return None
|
||||
effort = _normalize_effort(thinking.get("effort"))
|
||||
if effort:
|
||||
return effort
|
||||
thinking_type = _normalize_effort(thinking.get("type"))
|
||||
if thinking_type in {"disabled", "none"}:
|
||||
return "none"
|
||||
return None
|
||||
|
||||
|
||||
def extract_requested_effort(data: dict[str, Any]) -> str | None:
|
||||
"""Best-effort effort string from the OpenAI / Anthropic request shapes."""
|
||||
if isinstance(data.get("reasoning"), dict):
|
||||
nested = _normalize_effort(data["reasoning"].get("effort"))
|
||||
if nested:
|
||||
return nested
|
||||
top_level = _normalize_effort(data.get("reasoning_effort"))
|
||||
if top_level:
|
||||
return top_level
|
||||
return _effort_from_thinking(data.get("thinking"))
|
||||
|
||||
|
||||
def _request_mentions_reasoning(data: dict[str, Any]) -> bool:
|
||||
return any(key in data for key in _REASONING_KEYS)
|
||||
|
||||
|
||||
def apply_reasoning_effort(
|
||||
data: dict[str, Any],
|
||||
model: Model,
|
||||
*,
|
||||
drop_thinking: bool = False,
|
||||
) -> bool:
|
||||
"""Rewrite ``data`` in place so effort matches the model allowlist.
|
||||
|
||||
Returns True when ``data`` changed. Leaves the body alone when the
|
||||
caller did not send a reasoning field and the model does not require
|
||||
one. Existing ``reasoning`` object keys (``max_tokens``, ``exclude``,
|
||||
``enabled``) are preserved; only ``effort`` is mapped.
|
||||
|
||||
``drop_thinking`` is for OpenAI-compatible backends that reject the
|
||||
Anthropic ``thinking`` object: the effort is lifted onto
|
||||
``reasoning_effort`` / ``reasoning.effort`` and ``thinking`` is removed.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
|
||||
reasoning_meta = getattr(model, "reasoning", None)
|
||||
mentioned = _request_mentions_reasoning(data)
|
||||
if not mentioned and not (reasoning_meta and reasoning_meta.mandatory):
|
||||
return False
|
||||
|
||||
resolved = resolve_effort(extract_requested_effort(data), reasoning_meta)
|
||||
changed = False
|
||||
|
||||
if drop_thinking and "thinking" in data:
|
||||
data.pop("thinking", None)
|
||||
changed = True
|
||||
|
||||
if resolved is None:
|
||||
return changed
|
||||
|
||||
if "reasoning_effort" in data:
|
||||
if data.get("reasoning_effort") != resolved:
|
||||
data["reasoning_effort"] = resolved
|
||||
changed = True
|
||||
elif drop_thinking or (reasoning_meta and reasoning_meta.mandatory):
|
||||
# Invent the OpenAI-shaped field when we stripped Anthropic
|
||||
# ``thinking``, or when the model will reject a request with no
|
||||
# effort at all.
|
||||
if not isinstance(data.get("reasoning"), dict):
|
||||
data["reasoning_effort"] = resolved
|
||||
changed = True
|
||||
|
||||
existing = data.get("reasoning")
|
||||
if isinstance(existing, dict):
|
||||
if existing.get("effort") != resolved:
|
||||
data["reasoning"] = {**existing, "effort": resolved}
|
||||
changed = True
|
||||
elif reasoning_meta and reasoning_meta.mandatory and "reasoning_effort" not in data:
|
||||
data["reasoning"] = {"effort": resolved}
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def adapt_messages_body_for_litellm(data: dict[str, Any], model: Model) -> None:
|
||||
"""Convert Anthropic ``thinking`` into OpenAI-shaped effort for litellm.
|
||||
|
||||
Litellm's Anthropic-messages adapter talking to an OpenAI-compatible
|
||||
upstream will 400 on ``thinking``. Lift the effort onto
|
||||
``reasoning_effort`` and drop the Anthropic-only object.
|
||||
"""
|
||||
apply_reasoning_effort(data, model, drop_thinking=True)
|
||||
@@ -20,11 +20,12 @@ from urllib.parse import urlsplit
|
||||
import h11
|
||||
|
||||
from ..core import get_logger
|
||||
from ..core.exceptions import EhbpTimeoutError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_READ_BUFSIZE = 65536
|
||||
_DEFAULT_TIMEOUT_SECONDS = 30.0
|
||||
_DEFAULT_TIMEOUT_SECONDS = 60.0
|
||||
_DEFAULT_CLOSE_TIMEOUT_SECONDS = 1.0
|
||||
_DEFAULT_MAX_RESPONSE_BYTES = 25 * 1024 * 1024
|
||||
_HOP_BY_HOP_HEADERS = {
|
||||
@@ -100,10 +101,15 @@ async def forward_with_trailer(
|
||||
headers = _strip_hop_by_hop_headers(headers)
|
||||
|
||||
ssl_ctx = ssl.create_default_context()
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(host, port, ssl=ssl_ctx),
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(host, port, ssl=ssl_ctx),
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
except asyncio.TimeoutError as exc:
|
||||
raise EhbpTimeoutError(
|
||||
f"EHBP upstream {host} timed out after {timeout_seconds:g}s connecting"
|
||||
) from exc
|
||||
|
||||
try:
|
||||
# Build HTTP/1.1 request
|
||||
@@ -126,7 +132,13 @@ async def forward_with_trailer(
|
||||
request_data += body
|
||||
|
||||
writer.write(request_data)
|
||||
await asyncio.wait_for(writer.drain(), timeout=timeout_seconds)
|
||||
try:
|
||||
await asyncio.wait_for(writer.drain(), timeout=timeout_seconds)
|
||||
except asyncio.TimeoutError as exc:
|
||||
raise EhbpTimeoutError(
|
||||
f"EHBP upstream {host} timed out after "
|
||||
f"{timeout_seconds:g}s sending request"
|
||||
) from exc
|
||||
|
||||
# Parse response with h11
|
||||
conn = h11.Connection(h11.CLIENT)
|
||||
@@ -140,10 +152,16 @@ async def forward_with_trailer(
|
||||
event = conn.next_event()
|
||||
|
||||
if event is h11.NEED_DATA:
|
||||
data = await asyncio.wait_for(
|
||||
reader.read(_READ_BUFSIZE),
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
try:
|
||||
data = await asyncio.wait_for(
|
||||
reader.read(_READ_BUFSIZE),
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
except asyncio.TimeoutError as exc:
|
||||
raise EhbpTimeoutError(
|
||||
f"EHBP upstream {host} timed out after "
|
||||
f"{timeout_seconds:g}s waiting for response data"
|
||||
) from exc
|
||||
conn.receive_data(data if data else b"")
|
||||
continue
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from cashu.wallet.wallet import Wallet as _CashuWallet
|
||||
from pydantic_core import PydanticUndefined
|
||||
from sqlmodel import col, select, update
|
||||
|
||||
from .cashu_compat import install_cashu_httpx_shim
|
||||
from .core import db, get_logger
|
||||
from .core.db import store_cashu_transaction_with_retry as store_cashu_transaction
|
||||
from .core.settings import settings
|
||||
@@ -36,6 +37,10 @@ from .mint import (
|
||||
)
|
||||
from .payment.lnurl import raw_send_to_lnurl
|
||||
|
||||
# cashu 0.20.x passes the `proxies` kwarg httpx removed in 0.28; see the module
|
||||
# docstring. Installed at import so no mint call can run before the patch.
|
||||
install_cashu_httpx_shim()
|
||||
|
||||
# Backwards-compatible aliases for callers/tests that imported the former
|
||||
# wallet-local policy. Production modules use the public routstr.mint API.
|
||||
_MintRateGuard = MintRateGuard
|
||||
|
||||
@@ -410,12 +410,19 @@ async def test_malformed_auxiliary_rate_is_rejected(
|
||||
("input_cache_write", float("-inf")),
|
||||
("completion", -1.0),
|
||||
):
|
||||
# Send raw bytes rather than `json=`: httpx>=0.28 refuses to encode
|
||||
# non-finite floats itself (allow_nan=False), but the point of this
|
||||
# test is that the SERVER answers the bare NaN/Infinity literals
|
||||
# with a 422, so the literals must still reach it.
|
||||
body = json.dumps(
|
||||
_payload(
|
||||
provider_id, model_id="aux-rate", pricing=_pricing(**{field: bad})
|
||||
)
|
||||
).encode("utf-8")
|
||||
resp = await integration_client.post(
|
||||
f"/admin/api/upstream-providers/{provider_id}/models",
|
||||
headers=_admin_headers(),
|
||||
json=_payload(
|
||||
provider_id, model_id="aux-rate", pricing=_pricing(**{field: bad})
|
||||
),
|
||||
headers={**_admin_headers(), "content-type": "application/json"},
|
||||
content=body,
|
||||
)
|
||||
|
||||
assert resp.status_code == 422, field
|
||||
|
||||
@@ -12,7 +12,6 @@ import uuid
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from sqlmodel import col, select, update
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
@@ -305,21 +304,20 @@ async def test_cost_error_releases_the_reservation_without_charging(
|
||||
"routstr.auth.calculate_cost",
|
||||
return_value=CostDataError(message="no pricing", code="pricing_error"),
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await adjust_payment_for_tokens(
|
||||
key,
|
||||
_response(),
|
||||
integration_session,
|
||||
reserved,
|
||||
reservation_snapshot=reservation,
|
||||
)
|
||||
assert exc.value.status_code == 400
|
||||
cost = await adjust_payment_for_tokens(
|
||||
key,
|
||||
_response(),
|
||||
integration_session,
|
||||
reserved,
|
||||
reservation_snapshot=reservation,
|
||||
)
|
||||
assert cost["charged_msats"] == 0
|
||||
|
||||
key = await integration_session.get(ApiKey, key_hash)
|
||||
assert key is not None
|
||||
assert key.balance == 10_000, "a pricing failure must not charge the user"
|
||||
assert key.total_spent == 0
|
||||
assert key.reserved_balance == 0, "funds must not stay locked after a 400"
|
||||
assert key.reserved_balance == 0, "funds must not stay locked"
|
||||
assert await _active_reservations(integration_session) == 0
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""cashu 0.20.x builds its mint client with the `proxies` kwarg httpx removed in
|
||||
0.28. These tests pin the shim that keeps every wallet call working."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from cashu.wallet import v1_api
|
||||
from httpx import AsyncClient
|
||||
|
||||
from routstr.cashu_compat import (
|
||||
_ProxiesCompatAsyncClient,
|
||||
_single_proxy,
|
||||
install_cashu_httpx_shim,
|
||||
)
|
||||
|
||||
|
||||
def test_httpx_no_longer_accepts_proxies() -> None:
|
||||
"""The premise of the shim: plain httpx rejects what cashu passes."""
|
||||
with pytest.raises(TypeError):
|
||||
httpx.AsyncClient(proxies={}) # type: ignore[call-arg]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"proxies, expected",
|
||||
[
|
||||
({}, None),
|
||||
(None, None),
|
||||
({"all://": "socks5://localhost:9050"}, "socks5://localhost:9050"),
|
||||
("socks5://localhost:9050", "socks5://localhost:9050"),
|
||||
({"http://": "http://p:1", "https://": "http://p:1"}, "http://p:1"),
|
||||
],
|
||||
)
|
||||
def test_single_proxy_collapses_cashu_mappings(
|
||||
proxies: object, expected: str | None
|
||||
) -> None:
|
||||
assert _single_proxy(proxies) == expected
|
||||
|
||||
|
||||
def test_single_proxy_fails_closed_on_unrepresentable_mapping() -> None:
|
||||
"""Never silently drop a proxy: that would send mint traffic direct."""
|
||||
with pytest.raises(ValueError, match="cannot represent proxies"):
|
||||
_single_proxy({"http://": "http://a:1", "https://": "http://b:2"})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("proxies", [{}, {"all://": "socks5://localhost:9050"}])
|
||||
async def test_compat_client_accepts_proxies(proxies: dict) -> None:
|
||||
async with _ProxiesCompatAsyncClient(
|
||||
proxies=proxies, base_url="http://mint.test"
|
||||
) as client:
|
||||
assert isinstance(client, httpx.AsyncClient)
|
||||
|
||||
|
||||
async def test_empty_proxy_map_disables_environment_proxies(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
proxy_url = "http://127.0.0.1:1"
|
||||
for name in (
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"all_proxy",
|
||||
):
|
||||
monkeypatch.setenv(name, proxy_url)
|
||||
monkeypatch.setenv("NO_PROXY", "")
|
||||
monkeypatch.setenv("no_proxy", "")
|
||||
|
||||
async def respond(_: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||
await asyncio.sleep(0)
|
||||
writer.write(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n")
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
server = await asyncio.start_server(respond, "127.0.0.1", 0)
|
||||
try:
|
||||
port = server.sockets[0].getsockname()[1]
|
||||
url = f"http://127.0.0.1:{port}/"
|
||||
|
||||
async with _ProxiesCompatAsyncClient(proxies={}, timeout=1) as client:
|
||||
assert (await client.get(url)).status_code == 204
|
||||
|
||||
async with _ProxiesCompatAsyncClient(
|
||||
proxies={}, trust_env=True, timeout=1
|
||||
) as client:
|
||||
with pytest.raises(httpx.ConnectError):
|
||||
await client.get(url)
|
||||
finally:
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
|
||||
|
||||
async def test_cashu_decorator_builds_a_client_after_shim() -> None:
|
||||
"""The real cashu decorator — the code path every mint call goes through."""
|
||||
install_cashu_httpx_shim()
|
||||
|
||||
class _Ledger:
|
||||
url = "http://mint.test/"
|
||||
# cashu's decorator assigns the client here; alias avoids shadowing.
|
||||
httpx: AsyncClient
|
||||
|
||||
@v1_api.async_set_httpx_client # type: ignore[misc]
|
||||
async def call(self) -> AsyncClient:
|
||||
return self.httpx
|
||||
|
||||
client: httpx.AsyncClient = await _Ledger().call()
|
||||
try:
|
||||
assert isinstance(client, httpx.AsyncClient)
|
||||
assert str(client.base_url) == "http://mint.test"
|
||||
finally:
|
||||
await client.aclose()
|
||||
|
||||
|
||||
def test_install_is_idempotent() -> None:
|
||||
install_cashu_httpx_shim()
|
||||
patched = v1_api.httpx
|
||||
install_cashu_httpx_shim()
|
||||
assert v1_api.httpx is patched
|
||||
|
||||
|
||||
def test_shim_namespace_passes_through_other_httpx_attributes() -> None:
|
||||
install_cashu_httpx_shim()
|
||||
assert v1_api.httpx.Response is httpx.Response
|
||||
assert v1_api.httpx.AsyncClient is _ProxiesCompatAsyncClient
|
||||
@@ -0,0 +1,198 @@
|
||||
"""Tests for client-app identification in request logging."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.datastructures import Headers
|
||||
|
||||
from routstr.core.logging import ClientAppFilter
|
||||
from routstr.core.middleware import (
|
||||
UNKNOWN_CLIENT_APP,
|
||||
LoggingMiddleware,
|
||||
client_app_context,
|
||||
client_app_from_headers,
|
||||
)
|
||||
|
||||
|
||||
def _record() -> logging.LogRecord:
|
||||
return logging.LogRecord(
|
||||
name="routstr.test",
|
||||
level=logging.INFO,
|
||||
pathname=__file__,
|
||||
lineno=1,
|
||||
msg="test",
|
||||
args=None,
|
||||
exc_info=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("headers", "expected"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"x-title": "Goose",
|
||||
"http-referer": "https://myapp.example.com",
|
||||
"user-agent": "python-httpx/0.27",
|
||||
},
|
||||
"Goose",
|
||||
),
|
||||
(
|
||||
{"http-referer": "https://myapp.example.com", "user-agent": "curl/8.4.0"},
|
||||
"https://myapp.example.com",
|
||||
),
|
||||
(
|
||||
{"referer": "https://myapp.example.com", "user-agent": "curl/8.4.0"},
|
||||
"https://myapp.example.com",
|
||||
),
|
||||
({"user-agent": "curl/8.4.0"}, "curl/8.4.0"),
|
||||
({}, UNKNOWN_CLIENT_APP),
|
||||
({"x-title": " ", "user-agent": "curl/8.4.0"}, "curl/8.4.0"),
|
||||
({"x-title": " ", "user-agent": "\t"}, UNKNOWN_CLIENT_APP),
|
||||
],
|
||||
ids=[
|
||||
"x-title-wins",
|
||||
"http-referer",
|
||||
"referer",
|
||||
"user-agent-fallback",
|
||||
"no-identity-headers",
|
||||
"blank-falls-through",
|
||||
"all-blank",
|
||||
],
|
||||
)
|
||||
def test_client_app_from_headers(headers: dict[str, str], expected: str) -> None:
|
||||
assert client_app_from_headers(Headers(headers)) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("header", ["http-referer", "referer"])
|
||||
@pytest.mark.parametrize(
|
||||
("url", "expected"),
|
||||
[
|
||||
(
|
||||
"https://alice:password@app.example:8443/private/chat?token=secret#access_token=secret",
|
||||
"https://app.example:8443",
|
||||
),
|
||||
("http://[::1]:3000/chat?key=secret", "http://[::1]:3000"),
|
||||
("https://app.example/" + "a" * 200, "https://app.example"),
|
||||
("https://[invalid", "curl/8.4.0"),
|
||||
("/private/chat?token=secret", "curl/8.4.0"),
|
||||
("javascript:secret", "curl/8.4.0"),
|
||||
("https:///private", "curl/8.4.0"),
|
||||
],
|
||||
)
|
||||
def test_referrer_only_identifies_origin(header: str, url: str, expected: str) -> None:
|
||||
headers = Headers({header: url, "user-agent": "curl/8.4.0"})
|
||||
assert client_app_from_headers(headers) == expected
|
||||
|
||||
|
||||
def test_value_is_truncated_to_120_chars() -> None:
|
||||
assert client_app_from_headers(Headers({"x-title": "a" * 500})) == "a" * 120
|
||||
|
||||
|
||||
def test_control_characters_are_stripped() -> None:
|
||||
headers = Headers({"user-agent": "evil-app\x1b[0m fake INFO line"})
|
||||
assert client_app_from_headers(headers) == "evil-app[0m fake INFO line"
|
||||
|
||||
|
||||
def test_filter_reads_context_variable() -> None:
|
||||
token = client_app_context.set("Goose")
|
||||
try:
|
||||
record = _record()
|
||||
assert ClientAppFilter().filter(record) is True
|
||||
assert record.client_app == "Goose" # type: ignore[attr-defined]
|
||||
finally:
|
||||
client_app_context.reset(token)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fail", [False, True])
|
||||
async def test_context_is_restored_after_request(fail: bool) -> None:
|
||||
middleware = LoggingMiddleware(FastAPI())
|
||||
request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"method": "GET",
|
||||
"path": "/test",
|
||||
"query_string": b"",
|
||||
"headers": [],
|
||||
}
|
||||
)
|
||||
|
||||
async def call_next(request: Request) -> Response:
|
||||
assert client_app_context.get() == UNKNOWN_CLIENT_APP
|
||||
if fail:
|
||||
raise RuntimeError("handler failed")
|
||||
return Response()
|
||||
|
||||
token = client_app_context.set("outer")
|
||||
try:
|
||||
if fail:
|
||||
with pytest.raises(RuntimeError, match="handler failed"):
|
||||
await middleware.dispatch(request, call_next)
|
||||
else:
|
||||
await middleware.dispatch(request, call_next)
|
||||
assert client_app_context.get() == "outer"
|
||||
finally:
|
||||
client_app_context.reset(token)
|
||||
|
||||
|
||||
async def test_concurrent_requests_keep_their_own_client_app() -> None:
|
||||
middleware = LoggingMiddleware(FastAPI())
|
||||
ready = asyncio.Event()
|
||||
apps: list[str] = []
|
||||
|
||||
async def call_next(request: Request) -> Response:
|
||||
apps.append(request.headers["x-title"])
|
||||
if len(apps) == 2:
|
||||
ready.set()
|
||||
await asyncio.wait_for(ready.wait(), timeout=5)
|
||||
assert client_app_context.get() == request.headers["x-title"]
|
||||
return Response()
|
||||
|
||||
await asyncio.gather(
|
||||
*(
|
||||
middleware.dispatch(
|
||||
Request(
|
||||
{
|
||||
"type": "http",
|
||||
"method": "GET",
|
||||
"path": "/test",
|
||||
"query_string": b"",
|
||||
"headers": [(b"x-title", app)],
|
||||
}
|
||||
),
|
||||
call_next,
|
||||
)
|
||||
for app in (b"Goose", b"Pi")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_filter_defaults_to_unknown_outside_request_context() -> None:
|
||||
record = _record()
|
||||
assert ClientAppFilter().filter(record) is True
|
||||
assert record.client_app == UNKNOWN_CLIENT_APP # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_handler_logs_carry_client_app(caplog: pytest.LogCaptureFixture) -> None:
|
||||
app = FastAPI()
|
||||
handler_logger = logging.getLogger("routstr.test.handler")
|
||||
|
||||
@app.get("/whoami")
|
||||
async def whoami() -> dict[str, bool]:
|
||||
handler_logger.warning("something went wrong")
|
||||
return {"ok": True}
|
||||
|
||||
app.add_middleware(LoggingMiddleware)
|
||||
|
||||
caplog.handler.addFilter(ClientAppFilter())
|
||||
handler_logger.addHandler(caplog.handler)
|
||||
try:
|
||||
TestClient(app).get("/whoami", headers={"X-Title": "Goose"})
|
||||
finally:
|
||||
handler_logger.removeHandler(caplog.handler)
|
||||
|
||||
record = next(r for r in caplog.records if r.name == "routstr.test.handler")
|
||||
assert record.client_app == "Goose" # type: ignore[attr-defined]
|
||||
@@ -0,0 +1,47 @@
|
||||
"""A pricing failure after the upstream served content must not raise a 400."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from routstr.auth import ReservationSnapshot, adjust_payment_for_tokens
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.payment import cost_calculation
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cost_data_error_releases_without_raising() -> None:
|
||||
key_hash = "a" * 64
|
||||
reservation = ReservationSnapshot(
|
||||
release_id="rel-1",
|
||||
key_hash=key_hash,
|
||||
billing_key_hash=key_hash,
|
||||
reserved_msats=7_000,
|
||||
)
|
||||
with (
|
||||
patch.object(
|
||||
cost_calculation,
|
||||
"_get_pricing_rates",
|
||||
side_effect=ValueError("no pricing for model"),
|
||||
),
|
||||
patch("routstr.auth._validate_reservation_snapshot", new=AsyncMock()),
|
||||
patch("routstr.auth._stop_reservation_heartbeat", new=AsyncMock()),
|
||||
patch(
|
||||
"routstr.auth._claim_reservation_for_charge",
|
||||
new=AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"routstr.auth._charge_reservation_rows", new=AsyncMock(return_value=True)
|
||||
),
|
||||
patch("routstr.auth.accumulate_routstr_fee", new=AsyncMock()),
|
||||
):
|
||||
cost = await adjust_payment_for_tokens(
|
||||
ApiKey(hashed_key=key_hash),
|
||||
{"model": "gpt-4o", "usage": {"prompt_tokens": 10, "completion_tokens": 5}},
|
||||
session=AsyncMock(),
|
||||
deducted_max_cost=7_000,
|
||||
reservation_snapshot=reservation,
|
||||
)
|
||||
|
||||
assert cost["total_msats"] == 0
|
||||
assert cost["charged_msats"] == 0
|
||||
@@ -272,3 +272,39 @@ def test_uses_forwarded_model_id_when_present() -> None:
|
||||
|
||||
assert captured["model"] == "claude-3-5-sonnet-20241022"
|
||||
assert _read_payload(response)["input_tokens"] == 7
|
||||
|
||||
|
||||
def test_responses_instructions_are_counted_as_system_text() -> None:
|
||||
body = {"model": "gpt-4o", "input": "Hi", "instructions": "Be concise."}
|
||||
with patch.object(
|
||||
count_tokens_module.litellm, "token_counter", return_value=12
|
||||
) as counter:
|
||||
usage = MissingUsageEstimator(_body(body), None).response_data()["usage"]
|
||||
|
||||
assert usage["input_tokens"] == 12
|
||||
counter.assert_called_once_with(
|
||||
model="gpt-4o",
|
||||
messages=[
|
||||
{"role": "system", "content": "Be concise."},
|
||||
{"role": "user", "content": "Hi"},
|
||||
],
|
||||
tools=None,
|
||||
)
|
||||
|
||||
|
||||
def test_responses_tool_results_use_fallback_instead_of_empty_messages() -> None:
|
||||
body = {
|
||||
"model": "gpt-4o",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_1",
|
||||
"output": "result " * 100,
|
||||
}
|
||||
],
|
||||
}
|
||||
with patch.object(count_tokens_module.litellm, "token_counter") as counter:
|
||||
usage = MissingUsageEstimator(_body(body), None).response_data()["usage"]
|
||||
|
||||
counter.assert_not_called()
|
||||
assert usage["input_tokens"] > 100
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from routstr.core.exceptions import EhbpTimeoutError, UpstreamError
|
||||
from routstr.upstream import ehbp as ehbp_module
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# forward_ehbp_x_cashu_request — timeout fails closed with a refund + 504
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _request() -> MagicMock:
|
||||
request = MagicMock()
|
||||
request.state.request_id = "req-123"
|
||||
request.method = "POST"
|
||||
request.query_params = {}
|
||||
request.headers = {}
|
||||
request.body = AsyncMock(return_value=b"opaque")
|
||||
return request
|
||||
|
||||
|
||||
def _ehbp_upstream_mocks() -> tuple[MagicMock, MagicMock]:
|
||||
"""Upstream and model mocks sufficient to reach the forwarding call."""
|
||||
profile = MagicMock()
|
||||
profile.client_target_url_header = None
|
||||
profile.allow_client_target_override = False
|
||||
profile.proxy_only_headers = frozenset()
|
||||
profile.usage_response_header = None
|
||||
|
||||
target = MagicMock()
|
||||
target.url = "https://inference.tinfoil.sh/v1/chat/completions"
|
||||
target.headers = {}
|
||||
target.profile = None
|
||||
|
||||
upstream = MagicMock()
|
||||
upstream.prepare_headers.return_value = {}
|
||||
upstream.get_ehbp_forwarding_target.return_value = target
|
||||
upstream.get_confidential_inference_profile.return_value = profile
|
||||
upstream.prepare_params.return_value = {}
|
||||
|
||||
model_obj = MagicMock()
|
||||
model_obj.id = "tinfoil-kimi-k2-6"
|
||||
model_obj.forwarded_model_id = "kimi-k2-6"
|
||||
return upstream, model_obj
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_x_cashu_timeout_refunds_and_returns_504(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
ehbp_module,
|
||||
"recieve_token",
|
||||
AsyncMock(return_value=(1000, "msat", None)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
ehbp_module, "store_cashu_transaction", AsyncMock(return_value=None)
|
||||
)
|
||||
send_cashu_refund_mock = AsyncMock(return_value="refund-token")
|
||||
monkeypatch.setattr(ehbp_module, "send_cashu_refund", send_cashu_refund_mock)
|
||||
monkeypatch.setattr(
|
||||
ehbp_module,
|
||||
"forward_with_trailer",
|
||||
AsyncMock(side_effect=EhbpTimeoutError("EHBP upstream timed out")),
|
||||
)
|
||||
|
||||
upstream, model_obj = _ehbp_upstream_mocks()
|
||||
|
||||
response = await ehbp_module.forward_ehbp_x_cashu_request(
|
||||
request=await _request(),
|
||||
x_cashu_token="cashu-token",
|
||||
path="v1/chat/completions",
|
||||
max_cost_for_model=5000,
|
||||
model_obj=model_obj,
|
||||
upstream=upstream,
|
||||
)
|
||||
|
||||
assert response.status_code == 504
|
||||
assert response.headers["X-Cashu"] == "refund-token"
|
||||
send_cashu_refund_mock.assert_awaited_once_with(1000, "msat", None, "req-123")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# forward_ehbp_request — the bearer path must let the timeout through, so
|
||||
# proxy.py can answer 504 instead of flattening it to a generic 500
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bearer_timeout_propagates_504(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""A timed-out bearer request must not be rewritten to a 500.
|
||||
|
||||
``forward_ehbp_request`` ends in a bare ``except Exception`` that turns any
|
||||
error into ``UpstreamError(..., status_code=500)``. The ``except
|
||||
UpstreamError: raise`` above it is the only thing preserving the 504 that
|
||||
``proxy.py`` returns to the client, so this test pins that handler.
|
||||
"""
|
||||
monkeypatch.setattr(
|
||||
ehbp_module,
|
||||
"forward_with_trailer",
|
||||
AsyncMock(
|
||||
side_effect=EhbpTimeoutError(
|
||||
"EHBP upstream inference.tinfoil.sh timed out after 60s connecting"
|
||||
)
|
||||
),
|
||||
)
|
||||
upstream, model_obj = _ehbp_upstream_mocks()
|
||||
key = MagicMock()
|
||||
key.hashed_key = "abcdef1234567890"
|
||||
|
||||
with pytest.raises(EhbpTimeoutError) as exc_info:
|
||||
await ehbp_module.forward_ehbp_request(
|
||||
request=await _request(),
|
||||
path="v1/chat/completions",
|
||||
headers={},
|
||||
request_body=b"opaque",
|
||||
upstream=upstream,
|
||||
key=key,
|
||||
max_cost_for_model=5000,
|
||||
session=MagicMock(),
|
||||
model_obj=model_obj,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 504
|
||||
assert exc_info.value.code == "UPSTREAM_TIMEOUT"
|
||||
assert isinstance(exc_info.value, UpstreamError)
|
||||
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from nostr_sdk import Event, SendEventOutput
|
||||
|
||||
from routstr.nostr import sdk
|
||||
from routstr.nostr.listing import create_listing_event, nsec_to_keypair
|
||||
|
||||
PRIVATE_KEY_HEX = "11" * 32
|
||||
PRIVATE_KEY_NSEC = "nsec1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs4rm7hz"
|
||||
PUBLIC_KEY_HEX = "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
|
||||
|
||||
|
||||
def test_nsec_and_hex_parse_to_same_keypair() -> None:
|
||||
expected = (PRIVATE_KEY_HEX, PUBLIC_KEY_HEX)
|
||||
|
||||
assert nsec_to_keypair(PRIVATE_KEY_HEX) == expected
|
||||
assert nsec_to_keypair(PRIVATE_KEY_NSEC) == expected
|
||||
|
||||
|
||||
def test_listing_event_is_valid_nip01_event() -> None:
|
||||
event = create_listing_event(
|
||||
PRIVATE_KEY_HEX,
|
||||
"provider123",
|
||||
["https://provider.example.com"],
|
||||
mint_urls=["https://mint.example.com"],
|
||||
version="1.2.3",
|
||||
metadata={"name": "Provider"},
|
||||
)
|
||||
|
||||
assert event["pubkey"] == PUBLIC_KEY_HEX
|
||||
assert event["kind"] == 38421
|
||||
assert Event.from_json(json.dumps(event)).verify()
|
||||
|
||||
|
||||
class FakeClient:
|
||||
def __init__(
|
||||
self,
|
||||
event: dict[str, Any] | None = None,
|
||||
send_failure: str | None = None,
|
||||
) -> None:
|
||||
self.event = event
|
||||
self.send_failure = send_failure
|
||||
self.relay: Any = None
|
||||
self.connected = False
|
||||
self.shutdown_called = False
|
||||
self.sent_event: Event | None = None
|
||||
|
||||
async def add_relay(self, relay: Any) -> bool:
|
||||
self.relay = relay
|
||||
return True
|
||||
|
||||
async def connect(self) -> None:
|
||||
self.connected = True
|
||||
|
||||
async def fetch_events(self, *args: Any, **kwargs: Any) -> list[Event]:
|
||||
assert self.event is not None
|
||||
return [Event.from_json(json.dumps(self.event))]
|
||||
|
||||
async def send_event(self, event: Event, **kwargs: Any) -> SendEventOutput:
|
||||
self.sent_event = event
|
||||
if self.send_failure is not None:
|
||||
return SendEventOutput(
|
||||
id=event.id(), success=[], failed={self.relay: self.send_failure}
|
||||
)
|
||||
return SendEventOutput(id=event.id(), success=[self.relay], failed={})
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
self.shutdown_called = True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_events_uses_sdk_client_and_closes_it(monkeypatch: Any) -> None:
|
||||
event = create_listing_event(
|
||||
PRIVATE_KEY_HEX,
|
||||
"provider123",
|
||||
["https://provider.example.com"],
|
||||
)
|
||||
client = FakeClient(event)
|
||||
monkeypatch.setattr(sdk, "Client", lambda: client)
|
||||
|
||||
fetched = await sdk.fetch_events(
|
||||
"wss://relay.example.com",
|
||||
kind=38421,
|
||||
author=PUBLIC_KEY_HEX,
|
||||
limit=10,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
assert fetched == [event]
|
||||
assert client.connected
|
||||
assert client.shutdown_called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_event_uses_sdk_client_and_closes_it(monkeypatch: Any) -> None:
|
||||
event = create_listing_event(
|
||||
PRIVATE_KEY_HEX,
|
||||
"provider123",
|
||||
["https://provider.example.com"],
|
||||
)
|
||||
client = FakeClient()
|
||||
monkeypatch.setattr(sdk, "Client", lambda: client)
|
||||
|
||||
await sdk.send_event("wss://relay.example.com", event, timeout=30)
|
||||
|
||||
assert client.connected
|
||||
assert client.sent_event is not None
|
||||
assert client.sent_event.verify()
|
||||
assert client.shutdown_called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_event_raises_when_relay_rejects(monkeypatch: Any) -> None:
|
||||
event = create_listing_event(
|
||||
PRIVATE_KEY_HEX,
|
||||
"provider123",
|
||||
["https://provider.example.com"],
|
||||
)
|
||||
client = FakeClient(send_failure="blocked: rate limited")
|
||||
monkeypatch.setattr(sdk, "Client", lambda: client)
|
||||
|
||||
with pytest.raises(RuntimeError, match="rate limited"):
|
||||
await sdk.send_event("wss://relay.example.com", event, timeout=30)
|
||||
|
||||
assert client.shutdown_called
|
||||
@@ -288,3 +288,519 @@ async def test_discount_cannot_be_dodged_by_hiding_prompt_in_tools() -> None:
|
||||
# Same prompt weight → at least the same reservation, never the floor.
|
||||
assert cost >= cost_messages, where
|
||||
assert cost > 1000, where
|
||||
|
||||
|
||||
async def test_discounted_max_cost_counts_responses_input_images() -> None:
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from routstr.payment.helpers import calculate_discounted_max_cost
|
||||
|
||||
pricing = Mock()
|
||||
pricing.prompt = 0.001
|
||||
pricing.completion = 0.001
|
||||
pricing.max_prompt_cost = 100.0
|
||||
pricing.max_completion_cost = 0.0
|
||||
|
||||
model_obj = Mock()
|
||||
model_obj.sats_pricing = pricing
|
||||
model_obj.top_provider = None
|
||||
model_obj.context_length = None
|
||||
|
||||
image = Image.new("RGB", (512, 512), "red")
|
||||
buffer = BytesIO()
|
||||
image.save(buffer, format="JPEG")
|
||||
data_url = "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
no_image = {
|
||||
"model": "test-model",
|
||||
"input": [{"role": "user", "content": "hi"}],
|
||||
}
|
||||
with_image = {
|
||||
"model": "test-model",
|
||||
"input": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "hi"},
|
||||
{"type": "input_image", "image_url": data_url, "detail": "high"},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(settings, "fixed_pricing", False),
|
||||
patch.object(settings, "tolerance_percentage", 0),
|
||||
patch.object(settings, "min_request_msat", 1000),
|
||||
):
|
||||
cost_no_image = await calculate_discounted_max_cost(
|
||||
100_000, no_image, model_obj
|
||||
)
|
||||
cost_with_image = await calculate_discounted_max_cost(
|
||||
100_000, with_image, model_obj
|
||||
)
|
||||
|
||||
# The 512x512 high-detail image (85 + 170 = 255 tokens) is billed as prompt
|
||||
# weight, so it reserves strictly more than the identical text-only body.
|
||||
assert cost_with_image > cost_no_image
|
||||
|
||||
|
||||
def _responses_image(url: str, detail: str | None = "original") -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "input_image", "image_url": url, "detail": detail}],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def _responses_image_tokens(input_data: list[dict[str, Any]]) -> int:
|
||||
from routstr.payment.helpers import estimate_image_tokens_in_messages
|
||||
from routstr.payment.responses_input import responses_input_to_messages
|
||||
|
||||
messages = responses_input_to_messages(input_data)
|
||||
assert messages is not None
|
||||
return await estimate_image_tokens_in_messages(messages)
|
||||
|
||||
|
||||
async def test_remote_original_image_is_fetched_not_worst_cased() -> None:
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
|
||||
image = Image.new("RGB", (512, 512), "red")
|
||||
buffer = BytesIO()
|
||||
image.save(buffer, format="JPEG")
|
||||
image_bytes = buffer.getvalue()
|
||||
|
||||
with patch(
|
||||
"routstr.payment.helpers._fetch_image_from_url",
|
||||
new=AsyncMock(return_value=image_bytes),
|
||||
):
|
||||
# 256 patches * 1.2
|
||||
assert (
|
||||
await _responses_image_tokens(_responses_image("https://x.test/i.jpg"))
|
||||
== 308
|
||||
)
|
||||
|
||||
with patch(
|
||||
"routstr.payment.helpers._fetch_image_from_url",
|
||||
new=AsyncMock(return_value=None),
|
||||
):
|
||||
assert (
|
||||
await _responses_image_tokens(_responses_image("https://x.test/i.jpg"))
|
||||
== 36_000
|
||||
)
|
||||
|
||||
|
||||
async def test_broken_data_url_reserves_declared_detail_worst_case() -> None:
|
||||
from routstr.payment.helpers import estimate_image_tokens_in_messages
|
||||
|
||||
broken = "data:image/jpeg;base64,!!!"
|
||||
assert await _responses_image_tokens(_responses_image(broken)) == 36_000
|
||||
assert await _responses_image_tokens(_responses_image(broken, "high")) == 85 + (
|
||||
170 * 4
|
||||
)
|
||||
|
||||
chat = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": broken, "detail": "original"},
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
assert await estimate_image_tokens_in_messages(chat) == 36_000
|
||||
|
||||
|
||||
async def test_chat_original_image_fetch_failure_reserves_worst_case() -> None:
|
||||
from routstr.payment.helpers import estimate_image_tokens_in_messages
|
||||
|
||||
chat = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://x.test/i.jpg", "detail": "original"},
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
with patch(
|
||||
"routstr.payment.helpers._fetch_image_from_url",
|
||||
new=AsyncMock(return_value=None),
|
||||
):
|
||||
assert await estimate_image_tokens_in_messages(chat) == 36_000
|
||||
|
||||
|
||||
async def test_responses_images_share_per_request_fetch_cap() -> None:
|
||||
from routstr.payment.helpers import IMAGE_FETCH_MAX_PER_REQUEST
|
||||
|
||||
input_data = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": f"https://x.test/{i}.jpg",
|
||||
"detail": "original",
|
||||
}
|
||||
for i in range(IMAGE_FETCH_MAX_PER_REQUEST + 1)
|
||||
],
|
||||
}
|
||||
]
|
||||
fetch = AsyncMock(return_value=None)
|
||||
with patch("routstr.payment.helpers._fetch_image_from_url", new=fetch):
|
||||
tokens = await _responses_image_tokens(input_data)
|
||||
|
||||
assert fetch.await_count == IMAGE_FETCH_MAX_PER_REQUEST
|
||||
assert tokens == 36_000 * (IMAGE_FETCH_MAX_PER_REQUEST + 1)
|
||||
|
||||
|
||||
async def test_responses_transform_failure_falls_back_to_worst_case() -> None:
|
||||
from routstr.payment.helpers import calculate_discounted_max_cost
|
||||
|
||||
pricing = Mock()
|
||||
pricing.prompt = 0.001
|
||||
pricing.completion = 0.001
|
||||
pricing.max_prompt_cost = 100.0
|
||||
pricing.max_completion_cost = 0.0
|
||||
|
||||
model_obj = Mock()
|
||||
model_obj.sats_pricing = pricing
|
||||
model_obj.top_provider = None
|
||||
model_obj.context_length = None
|
||||
|
||||
body = {
|
||||
"model": "test-model",
|
||||
"input": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_image", "image_url": "https://x.test/a.jpg"},
|
||||
{"type": "input_image", "image_url": "https://x.test/b.jpg"},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
fetch = AsyncMock(return_value=None)
|
||||
with (
|
||||
patch.object(settings, "fixed_pricing", False),
|
||||
patch.object(settings, "tolerance_percentage", 0),
|
||||
patch.object(settings, "min_request_msat", 1000),
|
||||
patch("routstr.payment.helpers._fetch_image_from_url", new=fetch),
|
||||
patch(
|
||||
"routstr.payment.responses_input.LiteLLMCompletionResponsesConfig."
|
||||
"transform_responses_api_input_to_messages",
|
||||
side_effect=RuntimeError("boom"),
|
||||
),
|
||||
):
|
||||
cost = await calculate_discounted_max_cost(100_000, body, model_obj)
|
||||
|
||||
fetch.assert_not_awaited()
|
||||
# 2 * 36,000 tokens * 0.001 sats = 72 sats reserved
|
||||
assert 72_000 <= cost < 100_000
|
||||
|
||||
|
||||
async def test_discounted_max_cost_body_max_output_tokens_fallback() -> None:
|
||||
"""Body ``max_output_tokens`` (Responses API) is honored as a completion cap."""
|
||||
from routstr.payment.helpers import calculate_discounted_max_cost
|
||||
|
||||
pricing = Mock()
|
||||
pricing.prompt = 0.001
|
||||
pricing.completion = 0.001
|
||||
pricing.max_prompt_cost = 0.0
|
||||
pricing.max_completion_cost = 100.0
|
||||
|
||||
model_obj = Mock()
|
||||
model_obj.sats_pricing = pricing
|
||||
model_obj.top_provider = None
|
||||
model_obj.context_length = None
|
||||
|
||||
body = {"max_output_tokens": 80_000}
|
||||
|
||||
with (
|
||||
patch.object(settings, "fixed_pricing", False),
|
||||
patch.object(settings, "tolerance_percentage", 0),
|
||||
patch.object(settings, "min_request_msat", 1000),
|
||||
):
|
||||
cost = await calculate_discounted_max_cost(100_000, body, model_obj)
|
||||
|
||||
assert cost == 80_000
|
||||
|
||||
|
||||
async def test_discounted_max_cost_body_max_completion_tokens_fallback() -> None:
|
||||
"""Body ``max_completion_tokens`` (modern chat) is honored as a completion cap."""
|
||||
from routstr.payment.helpers import calculate_discounted_max_cost
|
||||
|
||||
pricing = Mock()
|
||||
pricing.prompt = 0.001
|
||||
pricing.completion = 0.001
|
||||
pricing.max_prompt_cost = 0.0
|
||||
pricing.max_completion_cost = 100.0
|
||||
|
||||
model_obj = Mock()
|
||||
model_obj.sats_pricing = pricing
|
||||
model_obj.top_provider = None
|
||||
model_obj.context_length = None
|
||||
|
||||
body = {"max_completion_tokens": 80_000}
|
||||
|
||||
with (
|
||||
patch.object(settings, "fixed_pricing", False),
|
||||
patch.object(settings, "tolerance_percentage", 0),
|
||||
patch.object(settings, "min_request_msat", 1000),
|
||||
):
|
||||
cost = await calculate_discounted_max_cost(100_000, body, model_obj)
|
||||
|
||||
assert cost == 80_000
|
||||
|
||||
|
||||
async def test_discounted_max_cost_uses_largest_completion_cap() -> None:
|
||||
"""With several completion caps declared, the largest bounds the reservation.
|
||||
|
||||
Upstream precedence between ``max_tokens`` / ``max_completion_tokens`` /
|
||||
``max_output_tokens`` varies by provider, so reserving against anything
|
||||
but the largest could under-cover what the upstream bills.
|
||||
"""
|
||||
from routstr.payment.helpers import calculate_discounted_max_cost
|
||||
|
||||
pricing = Mock()
|
||||
pricing.prompt = 0.001
|
||||
pricing.completion = 0.001
|
||||
pricing.max_prompt_cost = 0.0
|
||||
pricing.max_completion_cost = 100.0
|
||||
|
||||
model_obj = Mock()
|
||||
model_obj.sats_pricing = pricing
|
||||
model_obj.top_provider = None
|
||||
model_obj.context_length = None
|
||||
|
||||
body = {
|
||||
"max_tokens": 50_000,
|
||||
"max_completion_tokens": 10_000,
|
||||
"max_output_tokens": 80_000,
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(settings, "fixed_pricing", False),
|
||||
patch.object(settings, "tolerance_percentage", 0),
|
||||
patch.object(settings, "min_request_msat", 1000),
|
||||
):
|
||||
cost = await calculate_discounted_max_cost(100_000, body, model_obj)
|
||||
|
||||
# 80_000 is the largest declared cap: 100.0 - 80.0 = 20 sats discount.
|
||||
assert cost == 80_000
|
||||
|
||||
|
||||
async def test_discounted_max_cost_invalid_completion_cap_ignored() -> None:
|
||||
"""Unparseable caps yield no completion discount rather than under-reserving."""
|
||||
from routstr.payment.helpers import calculate_discounted_max_cost
|
||||
|
||||
pricing = Mock()
|
||||
pricing.prompt = 0.001
|
||||
pricing.completion = 0.001
|
||||
pricing.max_prompt_cost = 0.0
|
||||
pricing.max_completion_cost = 100.0
|
||||
|
||||
model_obj = Mock()
|
||||
model_obj.sats_pricing = pricing
|
||||
model_obj.top_provider = None
|
||||
model_obj.context_length = None
|
||||
|
||||
with (
|
||||
patch.object(settings, "fixed_pricing", False),
|
||||
patch.object(settings, "tolerance_percentage", 0),
|
||||
patch.object(settings, "min_request_msat", 1000),
|
||||
):
|
||||
# No valid cap at all -> no completion discount.
|
||||
cost = await calculate_discounted_max_cost(
|
||||
100_000, {"max_completion_tokens": "sixty-four-k"}, model_obj
|
||||
)
|
||||
assert cost == 100_000
|
||||
|
||||
# An invalid sibling does not poison a valid cap on another field.
|
||||
cost = await calculate_discounted_max_cost(
|
||||
100_000,
|
||||
{"max_tokens": "bad", "max_completion_tokens": 80_000},
|
||||
model_obj,
|
||||
)
|
||||
assert cost == 80_000
|
||||
|
||||
|
||||
def _responses_file_image(detail: str | None) -> list[dict[str, Any]]:
|
||||
part: dict[str, Any] = {"type": "input_image", "file_id": "file-1"}
|
||||
if detail is not None:
|
||||
part["detail"] = detail
|
||||
return [{"role": "user", "content": [part]}]
|
||||
|
||||
|
||||
async def test_responses_input_detail_and_file_id() -> None:
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
|
||||
# file_id: dimensions can't be fetched, so use a conservative max-size
|
||||
# estimate (4 tiles for auto/high) and honor the detail sibling for low.
|
||||
fetch = AsyncMock(return_value=None)
|
||||
with patch("routstr.payment.helpers._fetch_image_from_url", new=fetch):
|
||||
assert await _responses_image_tokens(_responses_file_image(None)) == 85 + (
|
||||
170 * 4
|
||||
)
|
||||
assert await _responses_image_tokens(_responses_file_image("low")) == 85
|
||||
fetch.assert_not_awaited()
|
||||
|
||||
# image_url honors the sibling detail instead of always defaulting to auto.
|
||||
image = Image.new("RGB", (512, 512), "red")
|
||||
buffer = BytesIO()
|
||||
image.save(buffer, format="JPEG")
|
||||
data_url = "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
assert await _responses_image_tokens(_responses_image(data_url, "low")) == 85
|
||||
assert (
|
||||
await _responses_image_tokens(_responses_image(data_url, "high")) == 85 + 170
|
||||
) # 512x512 = 1 tile
|
||||
|
||||
|
||||
def test_calculate_image_tokens_original_detail() -> None:
|
||||
from routstr.payment.helpers import _calculate_image_tokens
|
||||
|
||||
# Patch-based pricing: ceil(patches * 1.2) tokens at 32x32px patches.
|
||||
assert _calculate_image_tokens(640, 640, "original") == 480 # 400 patches
|
||||
assert _calculate_image_tokens(2048, 2048, "original") == 4_916 # 4,096 patches
|
||||
# The same image on the tiled high-detail path caps at 765 tokens.
|
||||
assert _calculate_image_tokens(2048, 2048, "high") == 765
|
||||
# Above the 30,000-patch rejection limit the estimate is capped at
|
||||
# 36,000 tokens (30,000 patches * 1.2).
|
||||
assert _calculate_image_tokens(10_000, 10_000, "original") == 36_000
|
||||
|
||||
|
||||
async def test_responses_input_original_detail() -> None:
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
|
||||
image = Image.new("RGB", (2048, 2048), "red")
|
||||
buffer = BytesIO()
|
||||
image.save(buffer, format="JPEG")
|
||||
data_url = "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
# image_url: billed at the decoded original resolution (4,096 patches),
|
||||
# not the 765-token tile cap.
|
||||
assert await _responses_image_tokens(_responses_image(data_url)) == 4_916
|
||||
|
||||
# file_id: dimensions unknown, so use the 30,000-patch worst case.
|
||||
assert await _responses_image_tokens(_responses_file_image("original")) == 36_000
|
||||
|
||||
# Explicit null detail behaves like the auto default (tiled math).
|
||||
assert await _responses_image_tokens(_responses_image(data_url, None)) == 85 + (
|
||||
170 * 4
|
||||
)
|
||||
|
||||
|
||||
def test_responses_input_to_messages_shapes() -> None:
|
||||
from routstr.payment.responses_input import (
|
||||
FILE_ID_URL_PREFIX,
|
||||
count_input_images,
|
||||
responses_input_to_messages,
|
||||
)
|
||||
|
||||
input_data = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "hi"},
|
||||
{"type": "input_image", "file_id": "file-1", "detail": "original"},
|
||||
],
|
||||
},
|
||||
{"type": "function_call_output", "call_id": "c1", "output": "out"},
|
||||
]
|
||||
messages = responses_input_to_messages(input_data)
|
||||
assert messages is not None
|
||||
assert messages[0]["role"] == "user"
|
||||
parts = messages[0]["content"]
|
||||
assert parts[0] == {"type": "text", "text": "hi"}
|
||||
assert parts[1]["type"] == "image_url"
|
||||
assert parts[1]["image_url"] == {
|
||||
"url": f"{FILE_ID_URL_PREFIX}file-1",
|
||||
"detail": "original",
|
||||
}
|
||||
assert messages[1]["role"] == "tool"
|
||||
|
||||
# dict-form image_url: litellm nests it verbatim, so it is flattened first.
|
||||
nested = responses_input_to_messages(
|
||||
[
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": {
|
||||
"url": "https://x.test/a.jpg",
|
||||
"detail": "original",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
)
|
||||
assert nested is not None
|
||||
assert nested[0]["content"][0]["image_url"] == {
|
||||
"url": "https://x.test/a.jpg",
|
||||
"detail": "original",
|
||||
}
|
||||
|
||||
assert responses_input_to_messages("plain") == [
|
||||
{"role": "user", "content": "plain"}
|
||||
]
|
||||
assert responses_input_to_messages(None) == []
|
||||
assert count_input_images(input_data) == 1
|
||||
|
||||
|
||||
async def test_estimate_image_tokens_in_messages_original_detail() -> None:
|
||||
"""Chat Completions also accepts original detail via the nested dict."""
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from routstr.payment.helpers import estimate_image_tokens_in_messages
|
||||
|
||||
image = Image.new("RGB", (640, 640), "blue")
|
||||
buffer = BytesIO()
|
||||
image.save(buffer, format="JPEG")
|
||||
data_url = "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": data_url, "detail": "original"},
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
# 640x640 -> 20x20 = 400 patches -> ceil(400 * 1.2) = 480 tokens.
|
||||
assert await estimate_image_tokens_in_messages(messages) == 480
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_image", "file_id": "file-1", "detail": "original"}
|
||||
],
|
||||
}
|
||||
]
|
||||
assert await estimate_image_tokens_in_messages(messages) == 36_000
|
||||
|
||||
@@ -69,6 +69,24 @@ def test_unknown_paths_are_not_forwarded() -> None:
|
||||
assert _forwarding_allowed("secret-endpoint", "POST") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"moderations",
|
||||
"rerank",
|
||||
"audio/speech",
|
||||
"audio/transcriptions",
|
||||
"audio/translations",
|
||||
"images/generations",
|
||||
"images/edits",
|
||||
"images/variations",
|
||||
],
|
||||
)
|
||||
def test_unbilled_endpoints_are_not_forwarded_by_default(path: str) -> None:
|
||||
assert _forwarding_allowed(path, "POST") is False
|
||||
assert _forwarding_allowed(f"v1/{path}", "POST") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
@@ -121,9 +139,6 @@ def test_known_prefix_does_not_carry_an_unknown_endpoint(path: str) -> None:
|
||||
("v1/responses", "POST"),
|
||||
("v1/messages", "POST"),
|
||||
("v1/embeddings", "POST"),
|
||||
("moderations", "POST"),
|
||||
("audio/transcriptions", "POST"),
|
||||
("images/generations", "POST"),
|
||||
("models", "GET"),
|
||||
("attestation", "GET"),
|
||||
("tee/attestation", "GET"),
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
"""Per-model reasoning-effort catalog metadata and request mapping."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
os.environ.setdefault("UPSTREAM_BASE_URL", "http://test")
|
||||
os.environ.setdefault("UPSTREAM_API_KEY", "test")
|
||||
os.environ.setdefault("LIGHTNING_ADDRESS", "test@stm.to")
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from routstr.core.db import get_session
|
||||
from routstr.payment.models import (
|
||||
Architecture,
|
||||
Model,
|
||||
Pricing,
|
||||
Reasoning,
|
||||
models_router,
|
||||
)
|
||||
from routstr.upstream import GenericUpstreamProvider
|
||||
from routstr.upstream.reasoning_effort import (
|
||||
adapt_messages_body_for_litellm,
|
||||
apply_reasoning_effort,
|
||||
closest_supported_effort,
|
||||
extract_requested_effort,
|
||||
resolve_effort,
|
||||
)
|
||||
|
||||
|
||||
def _model(**kwargs: Any) -> Model:
|
||||
reasoning = kwargs.pop("reasoning", None)
|
||||
return Model(
|
||||
id=kwargs.get("id", "openai/gpt-5.6-sol"),
|
||||
name="test",
|
||||
created=0,
|
||||
description="",
|
||||
context_length=128000,
|
||||
architecture=Architecture(
|
||||
modality="text->text",
|
||||
input_modalities=["text"],
|
||||
output_modalities=["text"],
|
||||
tokenizer="x",
|
||||
instruct_type=None,
|
||||
),
|
||||
pricing=Pricing(prompt=0.0, completion=0.0),
|
||||
reasoning=reasoning,
|
||||
)
|
||||
|
||||
|
||||
SOL_REASONING = {
|
||||
"mandatory": False,
|
||||
"default_enabled": True,
|
||||
"supported_efforts": ["max", "xhigh", "high", "medium", "low", "none"],
|
||||
"default_effort": "medium",
|
||||
}
|
||||
|
||||
|
||||
def test_model_parses_openrouter_reasoning_object() -> None:
|
||||
model = Model.parse_obj(
|
||||
{
|
||||
"id": "openai/gpt-5.6-sol",
|
||||
"name": "GPT",
|
||||
"created": 0,
|
||||
"description": "",
|
||||
"context_length": 1,
|
||||
"architecture": {
|
||||
"modality": "text",
|
||||
"input_modalities": ["text"],
|
||||
"output_modalities": ["text"],
|
||||
"tokenizer": "x",
|
||||
"instruct_type": None,
|
||||
},
|
||||
"pricing": {"prompt": 1e-6, "completion": 1e-6},
|
||||
"reasoning": SOL_REASONING,
|
||||
"extra_ignored_field": "drop me",
|
||||
}
|
||||
)
|
||||
assert model.reasoning is not None
|
||||
assert model.reasoning.supported_efforts == [
|
||||
"max",
|
||||
"xhigh",
|
||||
"high",
|
||||
"medium",
|
||||
"low",
|
||||
"none",
|
||||
]
|
||||
dumped = model.dict()
|
||||
assert dumped["reasoning"]["supported_efforts"][0] == "max"
|
||||
assert dumped["reasoning"]["default_effort"] == "medium"
|
||||
assert "extra_ignored_field" not in dumped
|
||||
|
||||
|
||||
def test_non_reasoning_models_omit_the_field() -> None:
|
||||
dumped = _model().dict()
|
||||
assert "reasoning" not in dumped
|
||||
|
||||
|
||||
def test_malformed_reasoning_is_dropped_not_fatal() -> None:
|
||||
model = _model(reasoning=["not", "a", "dict"])
|
||||
assert model.reasoning is None
|
||||
assert "reasoning" not in model.dict()
|
||||
|
||||
|
||||
def test_closest_effort_maps_minimal_to_low() -> None:
|
||||
assert (
|
||||
closest_supported_effort(
|
||||
"minimal",
|
||||
["max", "xhigh", "high", "medium", "low", "none"],
|
||||
default_effort="medium",
|
||||
)
|
||||
== "low"
|
||||
)
|
||||
|
||||
|
||||
def test_closest_effort_maps_max_when_missing() -> None:
|
||||
assert (
|
||||
closest_supported_effort(
|
||||
"max",
|
||||
["high", "medium", "low", "none"],
|
||||
default_effort="medium",
|
||||
)
|
||||
== "high"
|
||||
)
|
||||
|
||||
|
||||
def test_mandatory_rejects_none() -> None:
|
||||
assert (
|
||||
closest_supported_effort(
|
||||
"none",
|
||||
["max", "high", "medium", "low", "none"],
|
||||
default_effort="high",
|
||||
mandatory=True,
|
||||
)
|
||||
== "high"
|
||||
)
|
||||
|
||||
|
||||
def test_missing_request_uses_default() -> None:
|
||||
reasoning = Reasoning.parse_obj(SOL_REASONING)
|
||||
assert resolve_effort(None, reasoning) == "medium"
|
||||
|
||||
|
||||
def test_extract_prefers_nested_reasoning_effort() -> None:
|
||||
assert (
|
||||
extract_requested_effort(
|
||||
{"reasoning_effort": "low", "reasoning": {"effort": "high"}}
|
||||
)
|
||||
== "high"
|
||||
)
|
||||
|
||||
|
||||
def test_prepare_request_body_rewrites_unsupported_effort() -> None:
|
||||
provider = GenericUpstreamProvider(base_url="https://openrouter.ai/api/v1")
|
||||
model = _model(reasoning=SOL_REASONING)
|
||||
body = json.dumps(
|
||||
{
|
||||
"model": "openai/gpt-5.6-sol",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"reasoning_effort": "minimal",
|
||||
}
|
||||
).encode()
|
||||
out = provider.prepare_request_body(body, model)
|
||||
assert out is not None
|
||||
data = json.loads(out)
|
||||
assert data["reasoning_effort"] == "low"
|
||||
|
||||
|
||||
def test_prepare_request_body_rewrites_nested_effort() -> None:
|
||||
provider = GenericUpstreamProvider(base_url="https://openrouter.ai/api/v1")
|
||||
model = _model(reasoning=SOL_REASONING)
|
||||
body = json.dumps(
|
||||
{
|
||||
"model": "openai/gpt-5.6-sol",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"reasoning": {"effort": "minimal", "exclude": False},
|
||||
}
|
||||
).encode()
|
||||
out = provider.prepare_request_body(body, model)
|
||||
assert out is not None
|
||||
data = json.loads(out)
|
||||
assert data["reasoning"]["effort"] == "low"
|
||||
assert data["reasoning"]["exclude"] is False
|
||||
|
||||
|
||||
def test_prepare_request_body_leaves_plain_chat_alone() -> None:
|
||||
provider = GenericUpstreamProvider(base_url="https://openrouter.ai/api/v1")
|
||||
model = _model(reasoning=SOL_REASONING)
|
||||
payload = {
|
||||
"model": "openai/gpt-5.6-sol",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
}
|
||||
body = json.dumps(payload).encode()
|
||||
out = provider.prepare_request_body(body, model)
|
||||
assert out == body
|
||||
|
||||
|
||||
def test_apply_injects_default_when_mandatory() -> None:
|
||||
data: dict[str, Any] = {
|
||||
"model": "anthropic/claude-fable-5.1",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
}
|
||||
model = _model(
|
||||
id="anthropic/claude-fable-5.1",
|
||||
reasoning={
|
||||
"mandatory": True,
|
||||
"supported_efforts": ["max", "xhigh", "high", "medium", "low"],
|
||||
"default_effort": "high",
|
||||
},
|
||||
)
|
||||
assert apply_reasoning_effort(data, model) is True
|
||||
assert data["reasoning_effort"] == "high"
|
||||
|
||||
|
||||
def test_fee_apply_preserves_reasoning() -> None:
|
||||
provider = GenericUpstreamProvider(
|
||||
base_url="https://openrouter.ai/api/v1", provider_fee=1.1
|
||||
)
|
||||
model = _model(reasoning=SOL_REASONING)
|
||||
priced = provider._apply_provider_fee_to_model(model)
|
||||
assert priced.reasoning is not None
|
||||
assert priced.reasoning.supported_efforts == SOL_REASONING["supported_efforts"]
|
||||
|
||||
|
||||
def test_v1_models_includes_reasoning_and_omits_when_absent(
|
||||
monkeypatch: Any,
|
||||
) -> None:
|
||||
with_reasoning = _model(id="openai/gpt-5.6-sol", reasoning=SOL_REASONING)
|
||||
without = _model(id="openai/gpt-4o")
|
||||
unique = {"openai/gpt-5.6-sol": with_reasoning, "openai/gpt-4o": without}
|
||||
|
||||
import routstr.proxy as proxy
|
||||
|
||||
monkeypatch.setattr(proxy, "_unique_models", unique)
|
||||
app = FastAPI()
|
||||
app.include_router(models_router)
|
||||
app.dependency_overrides[get_session] = lambda: None
|
||||
response = TestClient(app).get("/v1/models")
|
||||
assert response.status_code == 200
|
||||
by_id = {row["id"]: row for row in response.json()["data"]}
|
||||
assert by_id["openai/gpt-5.6-sol"]["reasoning"]["supported_efforts"] == [
|
||||
"max",
|
||||
"xhigh",
|
||||
"high",
|
||||
"medium",
|
||||
"low",
|
||||
"none",
|
||||
]
|
||||
assert "reasoning" not in by_id["openai/gpt-4o"]
|
||||
|
||||
|
||||
def test_messages_thinking_becomes_reasoning_effort() -> None:
|
||||
model = _model(reasoning=SOL_REASONING)
|
||||
body: dict[str, Any] = {
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"max_tokens": 16,
|
||||
"thinking": {"type": "enabled", "effort": "minimal"},
|
||||
}
|
||||
adapt_messages_body_for_litellm(body, model)
|
||||
assert "thinking" not in body
|
||||
assert body["reasoning_effort"] == "low"
|
||||
@@ -11,18 +11,25 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from routstr import proxy as proxy_module
|
||||
from routstr.core.db import ApiKey
|
||||
from routstr.upstream.ehbp import (
|
||||
_PROXY_ONLY_HEADERS,
|
||||
EHBPForwardingTarget,
|
||||
_compute_ehbp_actual_cost,
|
||||
_is_ehbp_key_config_response,
|
||||
_passthrough_key_config_response,
|
||||
_prepare_ehbp_upstream_headers,
|
||||
_resolve_ehbp_target_url,
|
||||
_strip_proxy_headers,
|
||||
forward_ehbp_x_cashu_request,
|
||||
parse_tinfoil_usage_metrics,
|
||||
)
|
||||
from routstr.upstream.tinfoil import (
|
||||
TinfoilModel,
|
||||
TinfoilUpstreamProvider,
|
||||
)
|
||||
from routstr.upstream.tinfoil_trailer import TrailerResponse
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# parse_tinfoil_usage_metrics
|
||||
@@ -106,9 +113,7 @@ class TestParseTinfoilUsageMetrics:
|
||||
|
||||
def test_old_format_still_works(self) -> None:
|
||||
"""Headers without the model field (pre-PR #385) still parse."""
|
||||
result = parse_tinfoil_usage_metrics(
|
||||
"prompt=67,completion=42,total=109"
|
||||
)
|
||||
result = parse_tinfoil_usage_metrics("prompt=67,completion=42,total=109")
|
||||
assert result == {
|
||||
"prompt_tokens": 67,
|
||||
"completion_tokens": 42,
|
||||
@@ -285,7 +290,9 @@ class TestComputeEhbpActualCost:
|
||||
assert result["output_msats"] == 20
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unpriceable_usage_does_not_charge_authorization_ceiling(self) -> None:
|
||||
async def test_unpriceable_usage_does_not_charge_authorization_ceiling(
|
||||
self,
|
||||
) -> None:
|
||||
model_obj = MagicMock()
|
||||
model_obj.id = "llama3-3-70b"
|
||||
model_obj.forwarded_model_id = "llama3-3-70b"
|
||||
@@ -390,13 +397,16 @@ class TestComputeEhbpActualCost:
|
||||
actual_model_obj.id = "tinfoil-llama3-3-70b" # client-facing of actual
|
||||
actual_model_obj.forwarded_model_id = "llama3-3-70b"
|
||||
|
||||
with patch(
|
||||
"routstr.proxy.get_model_instance",
|
||||
return_value=actual_model_obj,
|
||||
), patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc:
|
||||
with (
|
||||
patch(
|
||||
"routstr.proxy.get_model_instance",
|
||||
return_value=actual_model_obj,
|
||||
),
|
||||
patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc,
|
||||
):
|
||||
from routstr.payment.cost_calculation import CostData
|
||||
|
||||
mock_calc.return_value = CostData(
|
||||
@@ -427,13 +437,16 @@ class TestComputeEhbpActualCost:
|
||||
model_obj.id = "gpt-oss-120b"
|
||||
model_obj.forwarded_model_id = "gpt-oss-120b"
|
||||
|
||||
with patch(
|
||||
"routstr.proxy.get_model_instance",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc:
|
||||
with (
|
||||
patch(
|
||||
"routstr.proxy.get_model_instance",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc,
|
||||
):
|
||||
from routstr.payment.cost_calculation import CostData
|
||||
|
||||
mock_calc.return_value = CostData(
|
||||
@@ -492,12 +505,13 @@ class TestComputeEhbpActualCost:
|
||||
model_obj = MagicMock()
|
||||
model_obj.id = "tinfoil-glm-5-2"
|
||||
model_obj.forwarded_model_id = "glm-5-2" # lowercase
|
||||
with patch(
|
||||
"routstr.proxy.get_model_instance"
|
||||
) as mock_get_model, patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc:
|
||||
with (
|
||||
patch("routstr.proxy.get_model_instance") as mock_get_model,
|
||||
patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc,
|
||||
):
|
||||
from routstr.payment.cost_calculation import CostData
|
||||
|
||||
mock_calc.return_value = CostData(
|
||||
@@ -533,13 +547,16 @@ class TestComputeEhbpActualCost:
|
||||
resolved_model_obj.id = "other-provider-glm-5-2"
|
||||
resolved_model_obj.forwarded_model_id = "glm-5-2"
|
||||
|
||||
with patch(
|
||||
"routstr.proxy.get_model_instance",
|
||||
return_value=resolved_model_obj,
|
||||
) as mock_get_model, patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc:
|
||||
with (
|
||||
patch(
|
||||
"routstr.proxy.get_model_instance",
|
||||
return_value=resolved_model_obj,
|
||||
) as mock_get_model,
|
||||
patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc,
|
||||
):
|
||||
from routstr.payment.cost_calculation import CostData
|
||||
|
||||
mock_calc.return_value = CostData(
|
||||
@@ -570,12 +587,13 @@ class TestComputeEhbpActualCost:
|
||||
model_obj.id = "tinfoil-glm-5-2-20260415"
|
||||
model_obj.forwarded_model_id = "glm-5-2-20260415"
|
||||
|
||||
with patch(
|
||||
"routstr.proxy.get_model_instance"
|
||||
) as mock_get_model, patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc:
|
||||
with (
|
||||
patch("routstr.proxy.get_model_instance") as mock_get_model,
|
||||
patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc,
|
||||
):
|
||||
from routstr.payment.cost_calculation import CostData
|
||||
|
||||
mock_calc.return_value = CostData(
|
||||
@@ -594,10 +612,7 @@ class TestComputeEhbpActualCost:
|
||||
)
|
||||
|
||||
assert "actual_model" not in result
|
||||
assert (
|
||||
mock_calc.call_args[0][0]["model"]
|
||||
== "tinfoil-glm-5-2-20260415"
|
||||
)
|
||||
assert mock_calc.call_args[0][0]["model"] == "tinfoil-glm-5-2-20260415"
|
||||
mock_get_model.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -612,13 +627,16 @@ class TestComputeEhbpActualCost:
|
||||
resolved_model_obj.id = "other-provider-glm-5-2"
|
||||
resolved_model_obj.forwarded_model_id = "GLM-5-2"
|
||||
|
||||
with patch(
|
||||
"routstr.proxy.get_model_instance",
|
||||
return_value=resolved_model_obj,
|
||||
) as mock_get_model, patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc:
|
||||
with (
|
||||
patch(
|
||||
"routstr.proxy.get_model_instance",
|
||||
return_value=resolved_model_obj,
|
||||
) as mock_get_model,
|
||||
patch(
|
||||
"routstr.upstream.ehbp.calculate_cost",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_calc,
|
||||
):
|
||||
from routstr.payment.cost_calculation import CostData
|
||||
|
||||
mock_calc.return_value = CostData(
|
||||
@@ -650,8 +668,7 @@ class TestTinfoilUpstreamProvider:
|
||||
def test_provider_type_and_defaults(self) -> None:
|
||||
assert TinfoilUpstreamProvider.provider_type == "tinfoil"
|
||||
assert (
|
||||
TinfoilUpstreamProvider.default_base_url
|
||||
== "https://inference.tinfoil.sh"
|
||||
TinfoilUpstreamProvider.default_base_url == "https://inference.tinfoil.sh"
|
||||
)
|
||||
assert TinfoilUpstreamProvider.supports_ehbp is True
|
||||
|
||||
@@ -666,9 +683,7 @@ class TestTinfoilUpstreamProvider:
|
||||
model_obj.id = "llama3-3-70b"
|
||||
model_obj.forwarded_model_id = "llama3-3-70b"
|
||||
target = provider.get_ehbp_forwarding_target("v1/chat/completions", model_obj)
|
||||
assert (
|
||||
target.headers["X-Tinfoil-Request-Usage-Metrics"] == "true"
|
||||
)
|
||||
assert target.headers["X-Tinfoil-Request-Usage-Metrics"] == "true"
|
||||
assert "v1/chat/completions" in target.url
|
||||
|
||||
def test_get_provider_metadata(self) -> None:
|
||||
@@ -747,3 +762,276 @@ class TestTinfoilUpstreamProvider:
|
||||
models = await provider.fetch_models()
|
||||
|
||||
assert models == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EHBP key-config mismatch passthrough
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _key_config_trailer_response(
|
||||
status_code: int = 422,
|
||||
content_type: str = "application/problem+json",
|
||||
body: bytes = b'{"type":"urn:ietf:params:ehbp:error:key-config","title":"failed to read decrypted request body"}',
|
||||
) -> TrailerResponse:
|
||||
return TrailerResponse(
|
||||
status_code=status_code,
|
||||
headers=[
|
||||
("content-type", content_type),
|
||||
("content-length", str(len(body))),
|
||||
],
|
||||
body=body,
|
||||
trailers=[],
|
||||
)
|
||||
|
||||
|
||||
class TestIsEhbpKeyConfigResponse:
|
||||
def test_genuine_key_config_422(self) -> None:
|
||||
resp = _key_config_trailer_response()
|
||||
assert _is_ehbp_key_config_response(resp) is True
|
||||
|
||||
def test_200_is_not_key_config(self) -> None:
|
||||
resp = _key_config_trailer_response(status_code=200)
|
||||
assert _is_ehbp_key_config_response(resp) is False
|
||||
|
||||
def test_400_is_not_key_config(self) -> None:
|
||||
resp = _key_config_trailer_response(status_code=400)
|
||||
assert _is_ehbp_key_config_response(resp) is False
|
||||
|
||||
def test_json_content_type_is_not_key_config(self) -> None:
|
||||
"""A 422 with application/json (e.g. proxy-wrapped error) must NOT be
|
||||
treated as key-config — only the original problem+json counts."""
|
||||
resp = _key_config_trailer_response(content_type="application/json")
|
||||
assert _is_ehbp_key_config_response(resp) is False
|
||||
|
||||
def test_problem_json_with_different_type_is_not_key_config(self) -> None:
|
||||
"""A 422 problem+json with a different error type is not key-config."""
|
||||
body = b'{"type":"urn:ietf:params:ehbp:error:other","title":"other"}'
|
||||
resp = _key_config_trailer_response(body=body)
|
||||
assert _is_ehbp_key_config_response(resp) is False
|
||||
|
||||
def test_empty_body_is_not_key_config(self) -> None:
|
||||
resp = _key_config_trailer_response(body=b"")
|
||||
assert _is_ehbp_key_config_response(resp) is False
|
||||
|
||||
def test_invalid_json_body_is_not_key_config(self) -> None:
|
||||
resp = _key_config_trailer_response(body=b"not json")
|
||||
assert _is_ehbp_key_config_response(resp) is False
|
||||
|
||||
def test_problem_json_with_charset(self) -> None:
|
||||
resp = _key_config_trailer_response(
|
||||
content_type="application/problem+json; charset=utf-8"
|
||||
)
|
||||
assert _is_ehbp_key_config_response(resp) is True
|
||||
|
||||
def test_content_type_parameter_disguising_other_media_type(self) -> None:
|
||||
"""A substring check would accept this; the media type must match
|
||||
exactly, mirroring the ehbp client's isProblemJSONContentType."""
|
||||
resp = _key_config_trailer_response(
|
||||
content_type="text/html; x=application/problem+json"
|
||||
)
|
||||
assert _is_ehbp_key_config_response(resp) is False
|
||||
|
||||
def test_uppercase_media_type_with_params_matches(self) -> None:
|
||||
resp = _key_config_trailer_response(
|
||||
content_type="Application/Problem+JSON; charset=UTF-8"
|
||||
)
|
||||
assert _is_ehbp_key_config_response(resp) is True
|
||||
|
||||
def test_missing_content_type_is_not_key_config(self) -> None:
|
||||
resp = TrailerResponse(
|
||||
status_code=422,
|
||||
headers=[],
|
||||
body=b'{"type":"urn:ietf:params:ehbp:error:key-config"}',
|
||||
)
|
||||
assert _is_ehbp_key_config_response(resp) is False
|
||||
|
||||
|
||||
class TestPassthroughKeyConfigResponse:
|
||||
def test_status_and_content_type(self) -> None:
|
||||
resp = _key_config_trailer_response()
|
||||
result = _passthrough_key_config_response(resp)
|
||||
assert result.status_code == 422
|
||||
assert result.media_type == "application/problem+json"
|
||||
|
||||
def test_body_passed_through(self) -> None:
|
||||
original_body = b'{"type":"urn:ietf:params:ehbp:error:key-config","title":"failed to read decrypted request body"}'
|
||||
resp = _key_config_trailer_response(body=original_body)
|
||||
result = _passthrough_key_config_response(resp)
|
||||
assert result.body == original_body
|
||||
|
||||
def test_ehbp_nonce_header_dropped(self) -> None:
|
||||
"""The nonce must not survive the passthrough: the stock ehbp client
|
||||
checks for the nonce before the key-config mismatch, so a forwarded
|
||||
nonce would send it down the decrypt path on this plaintext error
|
||||
body and the re-attestation loop would never fire."""
|
||||
resp = TrailerResponse(
|
||||
status_code=422,
|
||||
headers=[
|
||||
("content-type", "application/problem+json"),
|
||||
("ehbp-response-nonce", "abc123"),
|
||||
("content-length", "999"),
|
||||
("server", "nginx"),
|
||||
("x-request-id", "some-id"),
|
||||
],
|
||||
body=b'{"type":"urn:ietf:params:ehbp:error:key-config","title":"test"}',
|
||||
)
|
||||
result = _passthrough_key_config_response(resp)
|
||||
assert "ehbp-response-nonce" not in result.headers
|
||||
assert "server" not in result.headers
|
||||
assert "x-request-id" not in result.headers
|
||||
# Content-length is recomputed from the actual body, not forwarded.
|
||||
assert result.headers["content-length"] == str(len(resp.body))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Key-config passthrough at the forwarding call sites
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _ehbp_tinfoil_upstream() -> MagicMock:
|
||||
"""A minimal EHBP-capable upstream stub shaped like the Tinfoil provider."""
|
||||
upstream = MagicMock()
|
||||
upstream.provider_type = "tinfoil"
|
||||
upstream.supports_ehbp = True
|
||||
upstream.prepare_headers = MagicMock(side_effect=lambda h: h)
|
||||
upstream.get_confidential_inference_profile = MagicMock(return_value=None)
|
||||
upstream.get_ehbp_forwarding_target = MagicMock(
|
||||
return_value=EHBPForwardingTarget(
|
||||
url="https://inference.tinfoil.sh/private/v1/chat/completions"
|
||||
)
|
||||
)
|
||||
upstream.prepare_params = MagicMock(return_value={})
|
||||
return upstream
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bearer_key_config_422_releases_reservation_and_passes_through() -> None:
|
||||
"""The bearer path returns the enclave's problem+json verbatim AND the
|
||||
reservation is released.
|
||||
|
||||
The early return inside ``forward_ehbp_request`` skips the UpstreamError
|
||||
handler, so the release depends on the proxy's non-200 branch treating 422
|
||||
as non-retryable. Nothing else pins that; this does.
|
||||
"""
|
||||
key = ApiKey(hashed_key="keyconfig", balance=10_000)
|
||||
session = MagicMock()
|
||||
reservation_snapshot = MagicMock()
|
||||
revert_mock = AsyncMock(return_value=True)
|
||||
|
||||
request = MagicMock()
|
||||
request.method = "POST"
|
||||
request.headers = {
|
||||
"authorization": "Bearer sk-keyconfig",
|
||||
"ehbp-encapsulated-key": "abc123",
|
||||
"x-routstr-model": "tinfoil/llama3-3-70b",
|
||||
}
|
||||
request.body = AsyncMock(return_value=b"sealed-body")
|
||||
request.query_params = {}
|
||||
|
||||
model_obj = MagicMock()
|
||||
model_obj.id = "tinfoil/llama3-3-70b"
|
||||
upstream = _ehbp_tinfoil_upstream()
|
||||
|
||||
# The enclave may include a nonce even on the 422 — the passthrough must
|
||||
# drop it, or stock ehbp clients (nonce checked before key-config) would
|
||||
# try to decrypt this plaintext body instead of re-attesting.
|
||||
upstream_resp = _key_config_trailer_response()
|
||||
upstream_resp.headers.append(("ehbp-response-nonce", "nonce-value"))
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
proxy_module, "get_candidates", return_value=[(model_obj, upstream)]
|
||||
),
|
||||
patch.object(
|
||||
proxy_module, "get_max_cost_for_model", AsyncMock(return_value=1_000)
|
||||
),
|
||||
patch.object(
|
||||
proxy_module,
|
||||
"calculate_discounted_max_cost",
|
||||
AsyncMock(return_value=1_000),
|
||||
),
|
||||
patch.object(proxy_module, "check_token_balance", MagicMock()),
|
||||
patch.object(proxy_module, "get_bearer_token_key", AsyncMock(return_value=key)),
|
||||
patch.object(proxy_module, "pay_for_request", AsyncMock(return_value=1_000)),
|
||||
patch.object(
|
||||
proxy_module,
|
||||
"get_reservation_snapshot",
|
||||
AsyncMock(return_value=reservation_snapshot),
|
||||
),
|
||||
patch.object(proxy_module, "revert_pay_for_request", revert_mock),
|
||||
patch(
|
||||
"routstr.upstream.ehbp.forward_with_trailer",
|
||||
AsyncMock(return_value=upstream_resp),
|
||||
),
|
||||
):
|
||||
response = await proxy_module.proxy(
|
||||
request, "v1/chat/completions", session=session
|
||||
)
|
||||
|
||||
# The reservation was released despite the early passthrough return.
|
||||
revert_mock.assert_awaited_once_with(key, session, 1_000, reservation_snapshot)
|
||||
# The client receives the enclave's problem+json verbatim...
|
||||
assert response.status_code == 422
|
||||
assert response.headers["content-type"] == "application/problem+json"
|
||||
assert response.body == upstream_resp.body
|
||||
# ...without the nonce.
|
||||
assert "ehbp-response-nonce" not in response.headers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_x_cashu_key_config_422_refunds_and_sets_x_cashu_header() -> None:
|
||||
"""The x-cashu path refunds the full redeemed amount and attaches the
|
||||
refund token to the passthrough response."""
|
||||
request = MagicMock()
|
||||
request.method = "POST"
|
||||
request.headers = {
|
||||
"ehbp-encapsulated-key": "abc123",
|
||||
"x-routstr-model": "tinfoil/llama3-3-70b",
|
||||
}
|
||||
request.query_params = {}
|
||||
request.body = AsyncMock(return_value=b"sealed-body")
|
||||
request.state.request_id = "req-1"
|
||||
|
||||
model_obj = MagicMock()
|
||||
model_obj.id = "tinfoil/llama3-3-70b"
|
||||
upstream = _ehbp_tinfoil_upstream()
|
||||
|
||||
upstream_resp = _key_config_trailer_response()
|
||||
refund_mock = AsyncMock(return_value="cashuArefund")
|
||||
store_mock = AsyncMock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"routstr.upstream.ehbp.recieve_token",
|
||||
AsyncMock(return_value=(50_000, "msat", "https://mint.example")),
|
||||
),
|
||||
patch("routstr.upstream.ehbp.store_cashu_transaction", store_mock),
|
||||
patch("routstr.upstream.ehbp.send_cashu_refund", refund_mock),
|
||||
patch(
|
||||
"routstr.upstream.ehbp.forward_with_trailer",
|
||||
AsyncMock(return_value=upstream_resp),
|
||||
),
|
||||
):
|
||||
response = await forward_ehbp_x_cashu_request(
|
||||
request=request,
|
||||
x_cashu_token="cashuAtoken",
|
||||
path="v1/chat/completions",
|
||||
max_cost_for_model=1_000,
|
||||
model_obj=model_obj,
|
||||
upstream=upstream,
|
||||
)
|
||||
|
||||
# Full refund of the redeemed amount (the enclave never processed it).
|
||||
refund_mock.assert_awaited_once_with(
|
||||
50_000, "msat", "https://mint.example", "req-1"
|
||||
)
|
||||
# The redemption itself was recorded.
|
||||
store_mock.assert_awaited_once()
|
||||
assert store_mock.await_args is not None
|
||||
assert store_mock.await_args.kwargs.get("typ") == "in"
|
||||
# Passthrough shape with the refund attached.
|
||||
assert response.status_code == 422
|
||||
assert response.headers["content-type"] == "application/problem+json"
|
||||
assert response.body == upstream_resp.body
|
||||
assert response.headers["x-cashu"] == "cashuArefund"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from routstr.core.exceptions import EhbpTimeoutError, UpstreamError
|
||||
from routstr.upstream.tinfoil_trailer import forward_with_trailer
|
||||
|
||||
|
||||
@@ -28,6 +30,14 @@ class FakeWriter:
|
||||
self.written += data
|
||||
|
||||
|
||||
class HangingReader:
|
||||
"""A reader that never returns data, used to trigger a read timeout."""
|
||||
|
||||
async def read(self, _size: int) -> bytes:
|
||||
await asyncio.sleep(3600)
|
||||
return b""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_with_trailer_captures_usage_trailer(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -136,3 +146,62 @@ async def test_forward_with_trailer_enforces_response_size_limit(
|
||||
)
|
||||
|
||||
writer.close.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_with_trailer_connect_timeout_raises_ehbp_timeout(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def _hang_connect(*_args: object, **_kwargs: object) -> object:
|
||||
raise asyncio.TimeoutError
|
||||
|
||||
monkeypatch.setattr(
|
||||
"routstr.upstream.tinfoil_trailer.asyncio.open_connection", _hang_connect
|
||||
)
|
||||
|
||||
with pytest.raises(EhbpTimeoutError, match="connecting"):
|
||||
await forward_with_trailer(
|
||||
method="POST",
|
||||
url="https://enclave.tinfoil.sh/v1/chat/completions",
|
||||
headers={},
|
||||
body=b"opaque",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_with_trailer_read_timeout_raises_ehbp_timeout(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
reader = HangingReader()
|
||||
writer = FakeWriter()
|
||||
monkeypatch.setattr(
|
||||
"routstr.upstream.tinfoil_trailer.asyncio.open_connection",
|
||||
AsyncMock(return_value=(reader, writer)),
|
||||
)
|
||||
|
||||
with pytest.raises(EhbpTimeoutError, match="waiting for response data"):
|
||||
await forward_with_trailer(
|
||||
method="POST",
|
||||
url="https://enclave.tinfoil.sh/v1/chat/completions",
|
||||
headers={},
|
||||
body=b"opaque",
|
||||
timeout_seconds=0.01,
|
||||
)
|
||||
|
||||
writer.close.assert_called_once()
|
||||
|
||||
|
||||
def test_ehbp_timeout_error_metadata() -> None:
|
||||
exc = EhbpTimeoutError("boom")
|
||||
assert exc.status_code == 504
|
||||
assert exc.code == "UPSTREAM_TIMEOUT"
|
||||
assert exc.details is None
|
||||
assert isinstance(exc, UpstreamError)
|
||||
|
||||
|
||||
def test_ehbp_timeout_error_forwards_details() -> None:
|
||||
"""``details`` must survive so the response builder can forward it."""
|
||||
exc = EhbpTimeoutError("boom", details={"phase": "connect"})
|
||||
assert exc.details == {"phase": "connect"}
|
||||
assert exc.status_code == 504
|
||||
assert exc.code == "UPSTREAM_TIMEOUT"
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
"""X-Cashu billing when the upstream omits usage.
|
||||
|
||||
The local token estimator bills from the request body and the generated text.
|
||||
When nothing can be estimated the prepayment is refunded in full.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("UPSTREAM_BASE_URL", "http://test")
|
||||
os.environ.setdefault("UPSTREAM_API_KEY", "test")
|
||||
|
||||
from routstr.upstream.base import BaseUpstreamProvider # noqa: E402
|
||||
|
||||
REQUEST_BODY = json.dumps(
|
||||
{"model": "gpt-4o", "messages": [{"role": "user", "content": "Tell me a joke"}]}
|
||||
).encode()
|
||||
|
||||
|
||||
def _sse(events: list[dict[str, Any]]) -> httpx.Response:
|
||||
body = "".join(f"data: {json.dumps(e)}\n\n" for e in events) + "data: [DONE]\n\n"
|
||||
return httpx.Response(
|
||||
200, headers={"content-type": "text/event-stream"}, content=body.encode()
|
||||
)
|
||||
|
||||
|
||||
def _json(payload: dict[str, Any]) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200, headers={"content-type": "application/json"}, content=json.dumps(payload)
|
||||
)
|
||||
|
||||
|
||||
async def _settle(
|
||||
response: httpx.Response,
|
||||
*,
|
||||
responses_api: bool = False,
|
||||
request_body: bytes | None = REQUEST_BODY,
|
||||
unit: str = "msat",
|
||||
) -> tuple[Any, AsyncMock, AsyncMock]:
|
||||
provider = BaseUpstreamProvider(base_url="http://test", api_key="test-key")
|
||||
get_cost = AsyncMock(side_effect=provider.get_x_cashu_cost)
|
||||
send_refund = AsyncMock(return_value="cashuBrefund")
|
||||
handler = (
|
||||
provider.handle_x_cashu_responses_completion
|
||||
if responses_api
|
||||
else provider.handle_x_cashu_chat_completion
|
||||
)
|
||||
with (
|
||||
patch.object(provider, "get_x_cashu_cost", new=get_cost),
|
||||
patch.object(provider, "send_refund", new=send_refund),
|
||||
):
|
||||
result = await handler(
|
||||
response=response,
|
||||
amount=10_000,
|
||||
unit=unit,
|
||||
max_cost_for_model=9_000,
|
||||
mint=None,
|
||||
request_body=request_body,
|
||||
)
|
||||
return result, get_cost, send_refund
|
||||
|
||||
|
||||
def _billed_usage(get_cost: AsyncMock) -> dict[str, Any] | None:
|
||||
assert get_cost.await_args is not None
|
||||
return get_cost.await_args.args[0].get("usage")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_chat_without_usage_bills_from_estimate() -> None:
|
||||
events = [
|
||||
{"model": "gpt-4o", "choices": [{"delta": {"content": "Why did the "}}]},
|
||||
{"model": "gpt-4o", "choices": [{"delta": {"content": "chicken cross"}}]},
|
||||
]
|
||||
_, get_cost, _ = await _settle(_sse(events))
|
||||
|
||||
usage = _billed_usage(get_cost)
|
||||
assert usage is not None
|
||||
assert usage["input_tokens"] > 0
|
||||
assert usage["output_tokens"] > 0
|
||||
assert usage["estimated"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_chat_without_text_refunds_everything() -> None:
|
||||
_, get_cost, send_refund = await _settle(
|
||||
_sse([{"model": "gpt-4o"}]), request_body=None
|
||||
)
|
||||
|
||||
assert _billed_usage(get_cost) is None
|
||||
assert send_refund.await_args is not None
|
||||
assert send_refund.await_args.args[0] == 10_000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_streaming_chat_without_usage_bills_from_estimate() -> None:
|
||||
payload = {
|
||||
"model": "gpt-4o",
|
||||
"choices": [
|
||||
{"message": {"role": "assistant", "content": "To get to the other side."}}
|
||||
],
|
||||
}
|
||||
_, get_cost, _ = await _settle(_json(payload))
|
||||
|
||||
usage = _billed_usage(get_cost)
|
||||
assert usage is not None
|
||||
assert usage["output_tokens"] > 0
|
||||
assert usage["estimated"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_responses_without_usage_bills_from_estimate() -> None:
|
||||
events: list[dict[str, Any]] = [
|
||||
{"type": "response.created", "response": {"model": "gpt-5-mini"}},
|
||||
{"type": "response.output_text.delta", "delta": "Why did the chicken"},
|
||||
{"type": "response.output_text.done", "text": "Why did the chicken"},
|
||||
]
|
||||
_, get_cost, _ = await _settle(_sse(events), responses_api=True)
|
||||
|
||||
usage = _billed_usage(get_cost)
|
||||
assert usage is not None
|
||||
assert usage["output_tokens"] > 0
|
||||
assert usage["estimated"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_streaming_responses_without_usage_bills_from_estimate() -> None:
|
||||
payload = {
|
||||
"model": "gpt-5-mini",
|
||||
"output": [
|
||||
{"type": "message", "content": [{"type": "output_text", "text": "Hi"}]}
|
||||
],
|
||||
}
|
||||
_, get_cost, _ = await _settle(_json(payload), responses_api=True)
|
||||
|
||||
usage = _billed_usage(get_cost)
|
||||
assert usage is not None
|
||||
assert usage["output_tokens"] > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("responses_api", [False, True])
|
||||
async def test_empty_streaming_usage_uses_estimate(responses_api: bool) -> None:
|
||||
payload: dict[str, Any] = {"model": "gpt-4o", "usage": {}}
|
||||
if responses_api:
|
||||
payload["output"] = [{"content": [{"type": "output_text", "text": "Hello"}]}]
|
||||
else:
|
||||
payload["choices"] = [{"delta": {"content": "Hello"}}]
|
||||
|
||||
_, get_cost, _ = await _settle(_sse([payload]), responses_api=responses_api)
|
||||
|
||||
usage = _billed_usage(get_cost)
|
||||
assert usage is not None
|
||||
assert usage.get("estimated") is True
|
||||
assert usage["output_tokens"] > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("terminal_type", ["response.completed", "response.incomplete"])
|
||||
async def test_responses_terminal_output_is_not_billed_twice(
|
||||
terminal_type: str,
|
||||
) -> None:
|
||||
payload = {
|
||||
"model": "gpt-4o",
|
||||
"output": [{"content": [{"type": "output_text", "text": "Hello world"}]}],
|
||||
}
|
||||
events: list[dict[str, Any]] = [
|
||||
{"type": "response.output_text.delta", "delta": "Hello world"},
|
||||
{"type": terminal_type, "response": payload},
|
||||
]
|
||||
_, streaming_cost, _ = await _settle(_sse(events), responses_api=True)
|
||||
_, json_cost, _ = await _settle(_json(payload), responses_api=True)
|
||||
|
||||
stream_usage = _billed_usage(streaming_cost)
|
||||
json_usage = _billed_usage(json_cost)
|
||||
assert stream_usage is not None and json_usage is not None
|
||||
for field in ("input_tokens", "output_tokens", "total_tokens"):
|
||||
assert stream_usage[field] == json_usage[field]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("stream", [False, True])
|
||||
@pytest.mark.parametrize("input_shape", ["string", "message", "content_blocks"])
|
||||
async def test_responses_estimate_includes_input(
|
||||
stream: bool, input_shape: str
|
||||
) -> None:
|
||||
prompt = "Explain how payment reservations work. " * 100
|
||||
response_input: Any = prompt
|
||||
if input_shape == "message":
|
||||
response_input = [{"role": "user", "content": prompt}]
|
||||
elif input_shape == "content_blocks":
|
||||
response_input = [
|
||||
{"role": "user", "content": [{"type": "input_text", "text": prompt}]}
|
||||
]
|
||||
request_body = json.dumps(
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"instructions": "Answer concisely.",
|
||||
"input": response_input,
|
||||
}
|
||||
).encode()
|
||||
payload = {
|
||||
"model": "gpt-4o",
|
||||
"output": [{"content": [{"type": "output_text", "text": "Hello"}]}],
|
||||
}
|
||||
response = _sse([payload]) if stream else _json(payload)
|
||||
_, get_cost, _ = await _settle(
|
||||
response, responses_api=True, request_body=request_body
|
||||
)
|
||||
|
||||
usage = _billed_usage(get_cost)
|
||||
assert usage is not None
|
||||
assert usage["input_tokens"] > 100
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("responses_api", [False, True])
|
||||
@pytest.mark.parametrize("stream", [False, True])
|
||||
@pytest.mark.parametrize("tokens", [0, 17])
|
||||
async def test_reported_usage_takes_precedence(
|
||||
responses_api: bool, stream: bool, tokens: int
|
||||
) -> None:
|
||||
payload: dict[str, Any] = {
|
||||
"model": "gpt-4o",
|
||||
"usage": {"input_tokens": tokens, "output_tokens": tokens},
|
||||
}
|
||||
if responses_api:
|
||||
payload["output"] = [{"content": [{"type": "output_text", "text": "Hello"}]}]
|
||||
else:
|
||||
payload["choices"] = [{"message": {"content": "Hello"}}]
|
||||
|
||||
response = _sse([payload]) if stream else _json(payload)
|
||||
_, get_cost, _ = await _settle(response, responses_api=responses_api)
|
||||
|
||||
usage = _billed_usage(get_cost)
|
||||
assert usage is not None
|
||||
assert usage["input_tokens"] == tokens
|
||||
assert usage["output_tokens"] == tokens
|
||||
assert "estimated" not in usage
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("responses_api", [False, True])
|
||||
@pytest.mark.parametrize("stream", [False, True])
|
||||
@pytest.mark.parametrize("unit", ["sat", "msat"])
|
||||
async def test_pricing_error_refunds_full_prepayment(
|
||||
responses_api: bool, stream: bool, unit: str
|
||||
) -> None:
|
||||
payload = {
|
||||
"model": "unpriced-model",
|
||||
"usage": {"input_tokens": 10, "output_tokens": 5},
|
||||
}
|
||||
response = _sse([payload]) if stream else _json(payload)
|
||||
with patch(
|
||||
"routstr.payment.cost_calculation._get_pricing_rates",
|
||||
side_effect=ValueError("No pricing for model"),
|
||||
):
|
||||
result, _, send_refund = await _settle(
|
||||
response, responses_api=responses_api, unit=unit
|
||||
)
|
||||
|
||||
send_refund.assert_awaited_once_with(10_000, unit, None, request_id=None)
|
||||
assert result.status_code == 200
|
||||
assert result.headers["X-Cashu"] == "cashuBrefund"
|
||||
assert result.headers["X-Routstr-Cost-Msats"] == "0"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("responses_api", [False, True])
|
||||
async def test_full_refund_is_logged_with_model_provider_and_body(
|
||||
responses_api: bool, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
payload = {
|
||||
"model": "unpriced-model",
|
||||
"usage": {"input_tokens": 10, "output_tokens": 5},
|
||||
}
|
||||
base_logger = logging.getLogger("routstr.upstream.base")
|
||||
base_logger.addHandler(caplog.handler)
|
||||
try:
|
||||
with patch(
|
||||
"routstr.payment.cost_calculation._get_pricing_rates",
|
||||
side_effect=ValueError("No pricing for model"),
|
||||
):
|
||||
await _settle(_json(payload), responses_api=responses_api)
|
||||
finally:
|
||||
base_logger.removeHandler(caplog.handler)
|
||||
|
||||
record = next(
|
||||
r
|
||||
for r in caplog.records
|
||||
if r.getMessage() == "Zero-cost settlement, refunding the full prepayment"
|
||||
)
|
||||
logged = record.__dict__
|
||||
assert logged["model"] == "unpriced-model"
|
||||
assert logged["provider_type"] == "base"
|
||||
assert logged["upstream_base_url"] == "http://test"
|
||||
assert logged["refund_amount"] == 10_000
|
||||
assert logged["unit"] == "msat"
|
||||
assert "unpriced-model" in logged["response_body_preview"]
|
||||
+2
-2
@@ -48,7 +48,7 @@
|
||||
"geist": "^1.7.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.575.0",
|
||||
"next": "16.2.11",
|
||||
"next": "16.3.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"qrcode": "^1.5.4",
|
||||
"radix-ui": "^1.4.3",
|
||||
@@ -74,7 +74,7 @@
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint-config-next": "16.2.11",
|
||||
"eslint-config-next": "16.3.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
|
||||
Generated
+242
-238
@@ -12,13 +12,13 @@ overrides:
|
||||
flatted: 3.4.2
|
||||
follow-redirects: 1.16.0
|
||||
form-data: 4.0.6
|
||||
js-yaml: 4.3.1
|
||||
js-yaml: 4.3.2
|
||||
minimatch@3: 3.1.4
|
||||
nanoid@3: 3.3.18
|
||||
picomatch@2: 2.3.2
|
||||
picomatch@4: 4.0.4
|
||||
postcss: 8.5.23
|
||||
sharp: 0.35.0
|
||||
sharp: 0.35.4
|
||||
|
||||
importers:
|
||||
|
||||
@@ -122,7 +122,7 @@ importers:
|
||||
version: 8.6.0(react@19.2.4)
|
||||
geist:
|
||||
specifier: ^1.7.0
|
||||
version: 1.7.0(next@16.2.11(@babel/core@7.29.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
|
||||
version: 1.7.0(next@16.3.4(@babel/core@7.29.6)(@types/node@25.4.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
|
||||
input-otp:
|
||||
specifier: ^1.4.2
|
||||
version: 1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -130,8 +130,8 @@ importers:
|
||||
specifier: ^0.575.0
|
||||
version: 0.575.0(react@19.2.4)
|
||||
next:
|
||||
specifier: 16.2.11
|
||||
version: 16.2.11(@babel/core@7.29.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
specifier: 16.3.4
|
||||
version: 16.3.4(@babel/core@7.29.6)(@types/node@25.4.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -203,8 +203,8 @@ importers:
|
||||
specifier: ^9.7.0
|
||||
version: 9.38.0(jiti@2.6.1)
|
||||
eslint-config-next:
|
||||
specifier: 16.2.11
|
||||
version: 16.2.11(@typescript-eslint/parser@8.57.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
specifier: 16.3.4
|
||||
version: 16.3.4(@typescript-eslint/parser@8.57.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint-config-prettier:
|
||||
specifier: ^10.1.8
|
||||
version: 10.1.8(eslint@9.38.0(jiti@2.6.1))
|
||||
@@ -334,9 +334,6 @@ packages:
|
||||
'@emnapi/runtime@1.11.3':
|
||||
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
|
||||
|
||||
'@emnapi/runtime@1.8.1':
|
||||
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
|
||||
|
||||
'@emnapi/wasi-threads@1.1.0':
|
||||
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
|
||||
|
||||
@@ -398,12 +395,16 @@ packages:
|
||||
peerDependencies:
|
||||
react-hook-form: ^7.55.0
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
'@humanfs/core@0.19.2':
|
||||
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
|
||||
'@humanfs/node@0.16.8':
|
||||
resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
'@humanfs/types@0.15.0':
|
||||
resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1':
|
||||
@@ -418,144 +419,144 @@ packages:
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.35.0':
|
||||
resolution: {integrity: sha512-ZgaYEwaj+lx/5n4W8GmZ2IYz0PQHjN5eqRcfijWGB+2Aq7ZInZGa0qJyAn6DEtyLuWHRSrmWOqT9q3qqTBvmUQ==}
|
||||
'@img/sharp-darwin-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.35.0':
|
||||
resolution: {integrity: sha512-c1z9LFpKB0slQW3RchwBE8iSVzGp70TNjUUO9k4BZwwW4HH7JBGHeIy4b+kk4n/kcBASb9evKCE3/7Slmslgiw==}
|
||||
'@img/sharp-darwin-x64@0.35.4':
|
||||
resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-freebsd-wasm32@0.35.0':
|
||||
resolution: {integrity: sha512-Li2KTev0H90kEtnJHkI9xQojXt1AqWmFBMXiPw5kqd1jQgP7gi5HVK/qC5Rmh/59NuAwUuPzzPITmX22NomYYQ==}
|
||||
'@img/sharp-freebsd-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
os: [freebsd]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.0':
|
||||
resolution: {integrity: sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==}
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.3.0':
|
||||
resolution: {integrity: sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==}
|
||||
'@img/sharp-libvips-darwin-x64@1.3.3':
|
||||
resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.3.0':
|
||||
resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==}
|
||||
'@img/sharp-libvips-linux-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.3.0':
|
||||
resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==}
|
||||
'@img/sharp-libvips-linux-arm@1.3.3':
|
||||
resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.0':
|
||||
resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==}
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.3':
|
||||
resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.0':
|
||||
resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==}
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.3':
|
||||
resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.3.0':
|
||||
resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==}
|
||||
'@img/sharp-libvips-linux-s390x@1.3.3':
|
||||
resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.3.0':
|
||||
resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==}
|
||||
'@img/sharp-libvips-linux-x64@1.3.3':
|
||||
resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.0':
|
||||
resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==}
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.0':
|
||||
resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==}
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
|
||||
resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm64@0.35.0':
|
||||
resolution: {integrity: sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==}
|
||||
'@img/sharp-linux-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm@0.35.0':
|
||||
resolution: {integrity: sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==}
|
||||
'@img/sharp-linux-arm@0.35.4':
|
||||
resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.35.0':
|
||||
resolution: {integrity: sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==}
|
||||
'@img/sharp-linux-ppc64@0.35.4':
|
||||
resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.35.0':
|
||||
resolution: {integrity: sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==}
|
||||
'@img/sharp-linux-riscv64@0.35.4':
|
||||
resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-s390x@0.35.0':
|
||||
resolution: {integrity: sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==}
|
||||
'@img/sharp-linux-s390x@0.35.4':
|
||||
resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-x64@0.35.0':
|
||||
resolution: {integrity: sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==}
|
||||
'@img/sharp-linux-x64@0.35.4':
|
||||
resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.35.0':
|
||||
resolution: {integrity: sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==}
|
||||
'@img/sharp-linuxmusl-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.35.0':
|
||||
resolution: {integrity: sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==}
|
||||
'@img/sharp-linuxmusl-x64@0.35.4':
|
||||
resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-wasm32@0.35.0':
|
||||
resolution: {integrity: sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==}
|
||||
'@img/sharp-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
|
||||
'@img/sharp-webcontainers-wasm32@0.35.0':
|
||||
resolution: {integrity: sha512-t+kie1TOyaDM6Dho+f+y0VqIUNhYQaKCUahuZVi0E0frgdiaOaPsDxDW3wfKacUdaNBCnK/ZDBMg33ydvHj8uA==}
|
||||
'@img/sharp-webcontainers-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.35.0':
|
||||
resolution: {integrity: sha512-M5eKxug0dabbaWgFKvPa3odNs2OpaP+81NASfGKkt4GcYXpNhSu7CaeYxWkLNV6vHmUp4hnCxnxrUyhUJhXbKA==}
|
||||
'@img/sharp-win32-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.35.0':
|
||||
resolution: {integrity: sha512-z0+pZ03QCDvdVN0Ez9IX/yjWC19ikMlXrmdYMwYNLTh2BLPx3hXWPvyqWfquZ0BTO9O6GVOjIVoTcyyacMnWlQ==}
|
||||
'@img/sharp-win32-ia32@0.35.4':
|
||||
resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
|
||||
engines: {node: ^20.9.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.35.0':
|
||||
resolution: {integrity: sha512-feNnlz5ZHKr0MY1LPHvZQyJeBkbo4ctsn0D8FvA53VTw5TC63rfEL2UrWbkSBR19htSE7Mw78xYVwdJqoMWVHw==}
|
||||
'@img/sharp-win32-x64@0.35.4':
|
||||
resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -579,56 +580,56 @@ packages:
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
'@next/env@16.2.11':
|
||||
resolution: {integrity: sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA==}
|
||||
'@next/env@16.3.4':
|
||||
resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==}
|
||||
|
||||
'@next/eslint-plugin-next@16.2.11':
|
||||
resolution: {integrity: sha512-vMEf/aXOpzFFdtIvFYOnIDPKb0xBbrXONsz83CcKdRrekfxNdL8PNkq5qHqAHSXVlIifnX68LOMaxr3z5PkeLQ==}
|
||||
'@next/eslint-plugin-next@16.3.4':
|
||||
resolution: {integrity: sha512-szW9y2Aumu4z88YXfTzcFsgUAg2k64uzbtcO5L9f1AKS4w/GUKJcbFllRflROVyNPgJtGOnvNxiyp3v6b+prIA==}
|
||||
|
||||
'@next/swc-darwin-arm64@16.2.11':
|
||||
resolution: {integrity: sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw==}
|
||||
'@next/swc-darwin-arm64@16.3.4':
|
||||
resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.2.11':
|
||||
resolution: {integrity: sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg==}
|
||||
'@next/swc-darwin-x64@16.3.4':
|
||||
resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.2.11':
|
||||
resolution: {integrity: sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g==}
|
||||
'@next/swc-linux-arm64-gnu@16.3.4':
|
||||
resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.2.11':
|
||||
resolution: {integrity: sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg==}
|
||||
'@next/swc-linux-arm64-musl@16.3.4':
|
||||
resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.2.11':
|
||||
resolution: {integrity: sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w==}
|
||||
'@next/swc-linux-x64-gnu@16.3.4':
|
||||
resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.2.11':
|
||||
resolution: {integrity: sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw==}
|
||||
'@next/swc-linux-x64-musl@16.3.4':
|
||||
resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.2.11':
|
||||
resolution: {integrity: sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g==}
|
||||
'@next/swc-win32-arm64-msvc@16.3.4':
|
||||
resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.2.11':
|
||||
resolution: {integrity: sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w==}
|
||||
'@next/swc-win32-x64-msvc@16.3.4':
|
||||
resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1433,8 +1434,8 @@ packages:
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
'@swc/helpers@0.5.23':
|
||||
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
|
||||
|
||||
'@tabby_ai/hijri-converter@1.0.5':
|
||||
resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==}
|
||||
@@ -1855,8 +1856,8 @@ packages:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
baseline-browser-mapping@2.10.0:
|
||||
resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
|
||||
baseline-browser-mapping@2.11.21:
|
||||
resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1871,8 +1872,8 @@ packages:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
browserslist@4.28.1:
|
||||
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
|
||||
browserslist@4.28.9:
|
||||
resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
@@ -1896,8 +1897,8 @@ packages:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001776:
|
||||
resolution: {integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==}
|
||||
caniuse-lite@1.0.30001810:
|
||||
resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
@@ -2068,8 +2069,8 @@ packages:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
electron-to-chromium@1.5.307:
|
||||
resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==}
|
||||
electron-to-chromium@1.5.422:
|
||||
resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==}
|
||||
|
||||
embla-carousel-react@8.6.0:
|
||||
resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==}
|
||||
@@ -2141,8 +2142,8 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
eslint-config-next@16.2.11:
|
||||
resolution: {integrity: sha512-FIpbK/dUyxUExchDB7eBg3k+VU8R2iR/Cx9/kqTBUTFv2bOIR9aRrpno4rvAQ9VhiPQAyFKNA2NlZwouGWtclA==}
|
||||
eslint-config-next@16.3.4:
|
||||
resolution: {integrity: sha512-35/8RM10huEL9vlr8hUZMERMENHBrnyHN3ZZkF9efSgzGaqK34jIqry44A956//zriUhUAUW0XSkcolhrryqAA==}
|
||||
peerDependencies:
|
||||
eslint: '>=9.0.0'
|
||||
typescript: '>=3.3.1'
|
||||
@@ -2627,8 +2628,8 @@ packages:
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
js-yaml@4.3.1:
|
||||
resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
|
||||
js-yaml@4.3.2:
|
||||
resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
|
||||
hasBin: true
|
||||
|
||||
jsesc@3.1.0:
|
||||
@@ -2820,8 +2821,8 @@ packages:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next@16.2.11:
|
||||
resolution: {integrity: sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ==}
|
||||
next@16.3.4:
|
||||
resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -2841,8 +2842,9 @@ packages:
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
node-releases@2.0.36:
|
||||
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
||||
node-releases@2.0.54:
|
||||
resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
@@ -3193,11 +3195,6 @@ packages:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.4:
|
||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3218,9 +3215,14 @@ packages:
|
||||
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
sharp@0.35.0:
|
||||
resolution: {integrity: sha512-BqvG5XbwPZ4NV0DK90d86leEECMsoa8bO0nqnKWlBDYxri4GJ7c4EDInaF6q20lTh/mATmnDIKWJFfXnoVfH5g==}
|
||||
sharp@0.35.4:
|
||||
resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '*'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
@@ -3408,8 +3410,8 @@ packages:
|
||||
unrs-resolver@1.11.1:
|
||||
resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
update-browserslist-db@1.3.2:
|
||||
resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
@@ -3576,7 +3578,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.29.0
|
||||
'@babel/helper-validator-option': 7.27.1
|
||||
browserslist: 4.28.1
|
||||
browserslist: 4.28.9
|
||||
lru-cache: 5.1.1
|
||||
semver: 6.3.1
|
||||
|
||||
@@ -3675,11 +3677,6 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/runtime@1.8.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/wasi-threads@1.1.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -3716,7 +3713,7 @@ snapshots:
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.2
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.3.1
|
||||
js-yaml: 4.3.2
|
||||
minimatch: 3.1.4
|
||||
strip-json-comments: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
@@ -3753,13 +3750,18 @@ snapshots:
|
||||
'@standard-schema/utils': 0.3.0
|
||||
react-hook-form: 7.71.2(react@19.2.4)
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
'@humanfs/core@0.19.2':
|
||||
dependencies:
|
||||
'@humanfs/core': 0.19.1
|
||||
'@humanfs/types': 0.15.0
|
||||
|
||||
'@humanfs/node@0.16.8':
|
||||
dependencies:
|
||||
'@humanfs/core': 0.19.2
|
||||
'@humanfs/types': 0.15.0
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
|
||||
'@humanfs/types@0.15.0': {}
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1': {}
|
||||
|
||||
'@humanwhocodes/retry@0.4.3': {}
|
||||
@@ -3767,108 +3769,108 @@ snapshots:
|
||||
'@img/colour@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-arm64@0.35.0':
|
||||
'@img/sharp-darwin-arm64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.0
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.35.0':
|
||||
'@img/sharp-darwin-x64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.0
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-freebsd-wasm32@0.35.0':
|
||||
'@img/sharp-freebsd-wasm32@0.35.4':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.0
|
||||
'@img/sharp-wasm32': 0.35.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.0':
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.3.0':
|
||||
'@img/sharp-libvips-darwin-x64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.3.0':
|
||||
'@img/sharp-libvips-linux-arm64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.3.0':
|
||||
'@img/sharp-libvips-linux-arm@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.0':
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.0':
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.3.0':
|
||||
'@img/sharp-libvips-linux-s390x@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.3.0':
|
||||
'@img/sharp-libvips-linux-x64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.0':
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.0':
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.35.0':
|
||||
'@img/sharp-linux-arm64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.0
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.35.0':
|
||||
'@img/sharp-linux-arm@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.3.0
|
||||
'@img/sharp-libvips-linux-arm': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.35.0':
|
||||
'@img/sharp-linux-ppc64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.0
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.35.0':
|
||||
'@img/sharp-linux-riscv64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.0
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.35.0':
|
||||
'@img/sharp-linux-s390x@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.0
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.35.0':
|
||||
'@img/sharp-linux-x64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.3.0
|
||||
'@img/sharp-libvips-linux-x64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.35.0':
|
||||
'@img/sharp-linuxmusl-arm64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.0
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.35.0':
|
||||
'@img/sharp-linuxmusl-x64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.0
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.35.0':
|
||||
'@img/sharp-wasm32@0.35.4':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.11.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-webcontainers-wasm32@0.35.0':
|
||||
'@img/sharp-webcontainers-wasm32@0.35.4':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.0
|
||||
'@img/sharp-wasm32': 0.35.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.35.0':
|
||||
'@img/sharp-win32-arm64@0.35.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.35.0':
|
||||
'@img/sharp-win32-ia32@0.35.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.35.0':
|
||||
'@img/sharp-win32-x64@0.35.4':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
@@ -3893,38 +3895,41 @@ snapshots:
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.8.1
|
||||
'@emnapi/runtime': 1.8.1
|
||||
'@emnapi/runtime': 1.11.3
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@next/env@16.2.11': {}
|
||||
'@next/env@16.3.4': {}
|
||||
|
||||
'@next/eslint-plugin-next@16.2.11':
|
||||
'@next/eslint-plugin-next@16.3.4(eslint@9.38.0(jiti@2.6.1))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.38.0(jiti@2.6.1))
|
||||
fast-glob: 3.3.1
|
||||
transitivePeerDependencies:
|
||||
- eslint
|
||||
|
||||
'@next/swc-darwin-arm64@16.2.11':
|
||||
'@next/swc-darwin-arm64@16.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.2.11':
|
||||
'@next/swc-darwin-x64@16.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.2.11':
|
||||
'@next/swc-linux-arm64-gnu@16.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.2.11':
|
||||
'@next/swc-linux-arm64-musl@16.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.2.11':
|
||||
'@next/swc-linux-x64-gnu@16.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.2.11':
|
||||
'@next/swc-linux-x64-musl@16.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.2.11':
|
||||
'@next/swc-win32-arm64-msvc@16.3.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.2.11':
|
||||
'@next/swc-win32-x64-msvc@16.3.4':
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
@@ -4758,7 +4763,7 @@ snapshots:
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
'@swc/helpers@0.5.23':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
@@ -4969,7 +4974,7 @@ snapshots:
|
||||
'@typescript-eslint/visitor-keys': 8.57.0
|
||||
debug: 4.4.3
|
||||
minimatch: 10.2.4
|
||||
semver: 7.7.4
|
||||
semver: 7.8.5
|
||||
tinyglobby: 0.2.15
|
||||
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
@@ -5179,7 +5184,7 @@ snapshots:
|
||||
|
||||
balanced-match@4.0.4: {}
|
||||
|
||||
baseline-browser-mapping@2.10.0: {}
|
||||
baseline-browser-mapping@2.11.21: {}
|
||||
|
||||
brace-expansion@1.1.18:
|
||||
dependencies:
|
||||
@@ -5194,13 +5199,13 @@ snapshots:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
browserslist@4.28.1:
|
||||
browserslist@4.28.9:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.10.0
|
||||
caniuse-lite: 1.0.30001776
|
||||
electron-to-chromium: 1.5.307
|
||||
node-releases: 2.0.36
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
baseline-browser-mapping: 2.11.21
|
||||
caniuse-lite: 1.0.30001810
|
||||
electron-to-chromium: 1.5.422
|
||||
node-releases: 2.0.54
|
||||
update-browserslist-db: 1.3.2(browserslist@4.28.9)
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
@@ -5223,7 +5228,7 @@ snapshots:
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001776: {}
|
||||
caniuse-lite@1.0.30001810: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
@@ -5384,7 +5389,7 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
electron-to-chromium@1.5.307: {}
|
||||
electron-to-chromium@1.5.422: {}
|
||||
|
||||
embla-carousel-react@8.6.0(react@19.2.4):
|
||||
dependencies:
|
||||
@@ -5571,9 +5576,9 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
eslint-config-next@16.2.11(@typescript-eslint/parser@8.57.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3):
|
||||
eslint-config-next@16.3.4(@typescript-eslint/parser@8.57.0(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 16.2.11
|
||||
'@next/eslint-plugin-next': 16.3.4(eslint@9.38.0(jiti@2.6.1))
|
||||
eslint: 9.38.0(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.38.0(jiti@2.6.1))
|
||||
@@ -5740,7 +5745,7 @@ snapshots:
|
||||
'@eslint/eslintrc': 3.3.3
|
||||
'@eslint/js': 9.38.0
|
||||
'@eslint/plugin-kit': 0.4.0
|
||||
'@humanfs/node': 0.16.7
|
||||
'@humanfs/node': 0.16.8
|
||||
'@humanwhocodes/module-importer': 1.0.1
|
||||
'@humanwhocodes/retry': 0.4.3
|
||||
'@types/estree': 1.0.8
|
||||
@@ -5867,9 +5872,9 @@ snapshots:
|
||||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
geist@1.7.0(next@16.2.11(@babel/core@7.29.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
|
||||
geist@1.7.0(next@16.3.4(@babel/core@7.29.6)(@types/node@25.4.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
|
||||
dependencies:
|
||||
next: 16.2.11(@babel/core@7.29.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
next: 16.3.4(@babel/core@7.29.6)(@types/node@25.4.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
||||
generator-function@2.0.1: {}
|
||||
|
||||
@@ -6020,7 +6025,7 @@ snapshots:
|
||||
|
||||
is-bun-module@2.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.4
|
||||
semver: 7.8.5
|
||||
|
||||
is-callable@1.2.7: {}
|
||||
|
||||
@@ -6126,7 +6131,7 @@ snapshots:
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.3.1:
|
||||
js-yaml@4.3.2:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
@@ -6279,31 +6284,32 @@ snapshots:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
next@16.2.11(@babel/core@7.29.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
next@16.3.4(@babel/core@7.29.6)(@types/node@25.4.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
'@next/env': 16.2.11
|
||||
'@swc/helpers': 0.5.15
|
||||
baseline-browser-mapping: 2.10.0
|
||||
caniuse-lite: 1.0.30001776
|
||||
'@next/env': 16.3.4
|
||||
'@swc/helpers': 0.5.23
|
||||
baseline-browser-mapping: 2.11.21
|
||||
caniuse-lite: 1.0.30001810
|
||||
postcss: 8.5.23
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
styled-jsx: 5.1.6(@babel/core@7.29.6)(react@19.2.4)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.2.11
|
||||
'@next/swc-darwin-x64': 16.2.11
|
||||
'@next/swc-linux-arm64-gnu': 16.2.11
|
||||
'@next/swc-linux-arm64-musl': 16.2.11
|
||||
'@next/swc-linux-x64-gnu': 16.2.11
|
||||
'@next/swc-linux-x64-musl': 16.2.11
|
||||
'@next/swc-win32-arm64-msvc': 16.2.11
|
||||
'@next/swc-win32-x64-msvc': 16.2.11
|
||||
sharp: 0.35.0
|
||||
'@next/swc-darwin-arm64': 16.3.4
|
||||
'@next/swc-darwin-x64': 16.3.4
|
||||
'@next/swc-linux-arm64-gnu': 16.3.4
|
||||
'@next/swc-linux-arm64-musl': 16.3.4
|
||||
'@next/swc-linux-x64-gnu': 16.3.4
|
||||
'@next/swc-linux-x64-musl': 16.3.4
|
||||
'@next/swc-win32-arm64-msvc': 16.3.4
|
||||
'@next/swc-win32-x64-msvc': 16.3.4
|
||||
sharp: 0.35.4(@types/node@25.4.0)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
|
||||
node-releases@2.0.36: {}
|
||||
node-releases@2.0.54: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
@@ -6660,10 +6666,7 @@ snapshots:
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
semver@7.8.5:
|
||||
optional: true
|
||||
semver@7.8.5: {}
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
@@ -6689,37 +6692,38 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
sharp@0.35.0:
|
||||
sharp@0.35.4(@types/node@25.4.0):
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.5
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.35.0
|
||||
'@img/sharp-darwin-x64': 0.35.0
|
||||
'@img/sharp-freebsd-wasm32': 0.35.0
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.0
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.0
|
||||
'@img/sharp-libvips-linux-arm': 1.3.0
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.0
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.0
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.0
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.0
|
||||
'@img/sharp-libvips-linux-x64': 1.3.0
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.0
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.0
|
||||
'@img/sharp-linux-arm': 0.35.0
|
||||
'@img/sharp-linux-arm64': 0.35.0
|
||||
'@img/sharp-linux-ppc64': 0.35.0
|
||||
'@img/sharp-linux-riscv64': 0.35.0
|
||||
'@img/sharp-linux-s390x': 0.35.0
|
||||
'@img/sharp-linux-x64': 0.35.0
|
||||
'@img/sharp-linuxmusl-arm64': 0.35.0
|
||||
'@img/sharp-linuxmusl-x64': 0.35.0
|
||||
'@img/sharp-webcontainers-wasm32': 0.35.0
|
||||
'@img/sharp-win32-arm64': 0.35.0
|
||||
'@img/sharp-win32-ia32': 0.35.0
|
||||
'@img/sharp-win32-x64': 0.35.0
|
||||
'@img/sharp-darwin-arm64': 0.35.4
|
||||
'@img/sharp-darwin-x64': 0.35.4
|
||||
'@img/sharp-freebsd-wasm32': 0.35.4
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.3
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.3
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.3
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.3
|
||||
'@img/sharp-libvips-linux-x64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
|
||||
'@img/sharp-linux-arm': 0.35.4
|
||||
'@img/sharp-linux-arm64': 0.35.4
|
||||
'@img/sharp-linux-ppc64': 0.35.4
|
||||
'@img/sharp-linux-riscv64': 0.35.4
|
||||
'@img/sharp-linux-s390x': 0.35.4
|
||||
'@img/sharp-linux-x64': 0.35.4
|
||||
'@img/sharp-linuxmusl-arm64': 0.35.4
|
||||
'@img/sharp-linuxmusl-x64': 0.35.4
|
||||
'@img/sharp-webcontainers-wasm32': 0.35.4
|
||||
'@img/sharp-win32-arm64': 0.35.4
|
||||
'@img/sharp-win32-ia32': 0.35.4
|
||||
'@img/sharp-win32-x64': 0.35.4
|
||||
'@types/node': 25.4.0
|
||||
optional: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
@@ -6968,9 +6972,9 @@ snapshots:
|
||||
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
|
||||
'@unrs/resolver-binding-win32-x64-msvc': 1.11.1
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.1):
|
||||
update-browserslist-db@1.3.2(browserslist@4.28.9):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
browserslist: 4.28.9
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ overrides:
|
||||
flatted: 3.4.2
|
||||
follow-redirects: 1.16.0
|
||||
form-data: 4.0.6
|
||||
js-yaml: 4.3.1
|
||||
js-yaml: 4.3.2
|
||||
minimatch@3: 3.1.4
|
||||
nanoid@3: 3.3.18
|
||||
picomatch@2: 2.3.2
|
||||
picomatch@4: 4.0.4
|
||||
postcss: 8.5.23
|
||||
sharp: 0.35.0
|
||||
sharp: 0.35.4
|
||||
|
||||
+6
-1
@@ -29,5 +29,10 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/__tests__/**",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,9 +3,28 @@ revision = 3
|
||||
requires-python = ">=3.11"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
"python_full_version < '3.14'",
|
||||
"python_full_version == '3.13.*'",
|
||||
"python_full_version < '3.13'",
|
||||
]
|
||||
|
||||
[manifest]
|
||||
constraints = [
|
||||
{ name = "grpcio", specifier = ">=1.76.0,<2.0.0" },
|
||||
{ name = "grpcio-tools", specifier = ">=1.76.0,<2.0.0" },
|
||||
{ name = "httpcore", specifier = ">=1.0.9" },
|
||||
{ name = "starlette", specifier = ">=1.3.1" },
|
||||
]
|
||||
overrides = [
|
||||
{ name = "cryptography", specifier = ">=49.0.0" },
|
||||
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.141" },
|
||||
{ name = "h11", specifier = ">=0.16.0" },
|
||||
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1,<1.0" },
|
||||
{ name = "importlib-metadata", specifier = ">=8.0.0,<9.0" },
|
||||
{ name = "setuptools", specifier = ">=83.0.0" },
|
||||
{ name = "wheel", specifier = ">=0.46.2" },
|
||||
]
|
||||
build-constraints = [{ name = "scikit-build-core", specifier = "<0.10" }]
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.1"
|
||||
@@ -172,6 +191,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-doc"
|
||||
version = "0.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
@@ -374,7 +402,7 @@ dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "ecdsa" },
|
||||
{ name = "environs" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "fastapi", extra = ["standard-no-fastapi-cloud-cli"] },
|
||||
{ name = "googleapis-common-protos" },
|
||||
{ name = "greenlet" },
|
||||
{ name = "grpcio" },
|
||||
@@ -454,47 +482,100 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
version = "2.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
|
||||
{ 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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -721,31 +802,58 @@ toml = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "43.0.3"
|
||||
version = "50.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989, upload-time = "2024-10-18T15:58:32.918Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303, upload-time = "2024-10-18T15:57:36.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905, upload-time = "2024-10-18T15:57:39.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271, upload-time = "2024-10-18T15:57:41.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606, upload-time = "2024-10-18T15:57:42.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484, upload-time = "2024-10-18T15:57:45.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131, upload-time = "2024-10-18T15:57:47.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647, upload-time = "2024-10-18T15:57:49.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873, upload-time = "2024-10-18T15:57:51.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039, upload-time = "2024-10-18T15:57:54.426Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984, upload-time = "2024-10-18T15:57:56.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968, upload-time = "2024-10-18T15:57:58.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754, upload-time = "2024-10-18T15:58:00.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458, upload-time = "2024-10-18T15:58:02.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220, upload-time = "2024-10-18T15:58:04.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898, upload-time = "2024-10-18T15:58:06.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592, upload-time = "2024-10-18T15:58:08.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145, upload-time = "2024-10-18T15:58:10.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026, upload-time = "2024-10-18T15:58:11.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -818,44 +926,48 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.14"
|
||||
version = "0.141.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
standard-no-fastapi-cloud-cli = [
|
||||
{ name = "email-validator" },
|
||||
{ name = "fastapi-cli", extra = ["standard"] },
|
||||
{ name = "httpx" },
|
||||
{ name = "fastapi-cli", extra = ["standard-no-fastapi-cloud-cli"] },
|
||||
{ name = "httpx", extra = ["socks"] },
|
||||
{ name = "jinja2" },
|
||||
{ name = "pydantic-extra-types" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi-cli"
|
||||
version = "0.0.7"
|
||||
version = "0.0.32"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "rich-toolkit" },
|
||||
{ name = "typer" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/73/82a5831fbbf8ed75905bacf5b2d9d3dfd6f04d6968b29fe6f72a5ae9ceb1/fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", size = 16753, upload-time = "2024-12-15T14:28:10.028Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/eb/3b534c6f8e157f9ddbf2a153512307c886cad0b258739c200dd8ff8c4452/fastapi_cli-0.0.32.tar.gz", hash = "sha256:38024d2345275e1b37ce8848727a580d84901b570e96b3256d9d36a9a5039424", size = 26636, upload-time = "2026-07-16T12:16:58.678Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/e6/5daefc851b514ce2287d8f5d358ae4341089185f78f3217a69d0ce3a390c/fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4", size = 10705, upload-time = "2024-12-15T14:28:06.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/53/56ae5ae17bb0a5d89d1d31e5320eb1865553ebbfbde91cdc4c221245f2a8/fastapi_cli-0.0.32-py3-none-any.whl", hash = "sha256:8dcc286fa32f01bbd3f65dd09cfd5a2540ed5f2230b77db7fd30978d6165f3c4", size = 14670, upload-time = "2026-07-16T12:16:57.297Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
standard-no-fastapi-cloud-cli = [
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
@@ -1070,92 +1182,115 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "grpcio"
|
||||
version = "1.74.0"
|
||||
version = "1.83.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/b4/35feb8f7cab7239c5b94bd2db71abb3d6adb5f335ad8f131abb6060840b6/grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1", size = 12756048, upload-time = "2025-07-24T18:54:23.039Z" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e2/b1/46539f5050d7c316a13396d185451f95084a74ddc68b12d818595bef0377/grpcio-1.83.1.tar.gz", hash = "sha256:9cee6fcbf2eb57c4b49451787bfa87be8efc1ca02a0b327dd4b54d44502e362b", size = 13445033, upload-time = "2026-08-28T07:09:11.464Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/77/b2f06db9f240a5abeddd23a0e49eae2b6ac54d85f0e5267784ce02269c3b/grpcio-1.74.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:69e1a8180868a2576f02356565f16635b99088da7df3d45aaa7e24e73a054e31", size = 5487368, upload-time = "2025-07-24T18:53:03.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/99/0ac8678a819c28d9a370a663007581744a9f2a844e32f0fa95e1ddda5b9e/grpcio-1.74.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8efe72fde5500f47aca1ef59495cb59c885afe04ac89dd11d810f2de87d935d4", size = 10999804, upload-time = "2025-07-24T18:53:05.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/c6/a2d586300d9e14ad72e8dc211c7aecb45fe9846a51e558c5bca0c9102c7f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a8f0302f9ac4e9923f98d8e243939a6fb627cd048f5cd38595c97e38020dffce", size = 5987667, upload-time = "2025-07-24T18:53:07.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/57/5f338bf56a7f22584e68d669632e521f0de460bb3749d54533fc3d0fca4f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f609a39f62a6f6f05c7512746798282546358a37ea93c1fcbadf8b2fed162e3", size = 6655612, upload-time = "2025-07-24T18:53:09.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/ea/a4820c4c44c8b35b1903a6c72a5bdccec92d0840cf5c858c498c66786ba5/grpcio-1.74.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98e0b7434a7fa4e3e63f250456eaef52499fba5ae661c58cc5b5477d11e7182", size = 6219544, upload-time = "2025-07-24T18:53:11.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/17/0537630a921365928f5abb6d14c79ba4dcb3e662e0dbeede8af4138d9dcf/grpcio-1.74.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:662456c4513e298db6d7bd9c3b8df6f75f8752f0ba01fb653e252ed4a59b5a5d", size = 6334863, upload-time = "2025-07-24T18:53:12.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a6/85ca6cb9af3f13e1320d0a806658dca432ff88149d5972df1f7b51e87127/grpcio-1.74.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3d14e3c4d65e19d8430a4e28ceb71ace4728776fd6c3ce34016947474479683f", size = 7019320, upload-time = "2025-07-24T18:53:15.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/a7/fe2beab970a1e25d2eff108b3cf4f7d9a53c185106377a3d1989216eba45/grpcio-1.74.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bf949792cee20d2078323a9b02bacbbae002b9e3b9e2433f2741c15bdeba1c4", size = 6514228, upload-time = "2025-07-24T18:53:16.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c2/2f9c945c8a248cebc3ccda1b7a1bf1775b9d7d59e444dbb18c0014e23da6/grpcio-1.74.0-cp311-cp311-win32.whl", hash = "sha256:55b453812fa7c7ce2f5c88be3018fb4a490519b6ce80788d5913f3f9d7da8c7b", size = 3817216, upload-time = "2025-07-24T18:53:20.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/d1/a9cf9c94b55becda2199299a12b9feef0c79946b0d9d34c989de6d12d05d/grpcio-1.74.0-cp311-cp311-win_amd64.whl", hash = "sha256:86ad489db097141a907c559988c29718719aa3e13370d40e20506f11b4de0d11", size = 4495380, upload-time = "2025-07-24T18:53:22.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/5d/e504d5d5c4469823504f65687d6c8fb97b7f7bf0b34873b7598f1df24630/grpcio-1.74.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8533e6e9c5bd630ca98062e3a1326249e6ada07d05acf191a77bc33f8948f3d8", size = 5445551, upload-time = "2025-07-24T18:53:23.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/01/730e37056f96f2f6ce9f17999af1556df62ee8dab7fa48bceeaab5fd3008/grpcio-1.74.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2918948864fec2a11721d91568effffbe0a02b23ecd57f281391d986847982f6", size = 10979810, upload-time = "2025-07-24T18:53:25.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/3d/09fd100473ea5c47083889ca47ffd356576173ec134312f6aa0e13111dee/grpcio-1.74.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:60d2d48b0580e70d2e1954d0d19fa3c2e60dd7cbed826aca104fff518310d1c5", size = 5941946, upload-time = "2025-07-24T18:53:27.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/99/12d2cca0a63c874c6d3d195629dcd85cdf5d6f98a30d8db44271f8a97b93/grpcio-1.74.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3601274bc0523f6dc07666c0e01682c94472402ac2fd1226fd96e079863bfa49", size = 6621763, upload-time = "2025-07-24T18:53:29.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/2c/930b0e7a2f1029bbc193443c7bc4dc2a46fedb0203c8793dcd97081f1520/grpcio-1.74.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:176d60a5168d7948539def20b2a3adcce67d72454d9ae05969a2e73f3a0feee7", size = 6180664, upload-time = "2025-07-24T18:53:30.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d5/ff8a2442180ad0867717e670f5ec42bfd8d38b92158ad6bcd864e6d4b1ed/grpcio-1.74.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e759f9e8bc908aaae0412642afe5416c9f983a80499448fcc7fab8692ae044c3", size = 6301083, upload-time = "2025-07-24T18:53:32.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ba/b361d390451a37ca118e4ec7dccec690422e05bc85fba2ec72b06cefec9f/grpcio-1.74.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e7c4389771855a92934b2846bd807fc25a3dfa820fd912fe6bd8136026b2707", size = 6994132, upload-time = "2025-07-24T18:53:34.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/0c/3a5fa47d2437a44ced74141795ac0251bbddeae74bf81df3447edd767d27/grpcio-1.74.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cce634b10aeab37010449124814b05a62fb5f18928ca878f1bf4750d1f0c815b", size = 6489616, upload-time = "2025-07-24T18:53:36.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/95/ab64703b436d99dc5217228babc76047d60e9ad14df129e307b5fec81fd0/grpcio-1.74.0-cp312-cp312-win32.whl", hash = "sha256:885912559974df35d92219e2dc98f51a16a48395f37b92865ad45186f294096c", size = 3807083, upload-time = "2025-07-24T18:53:37.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/59/900aa2445891fc47a33f7d2f76e00ca5d6ae6584b20d19af9c06fa09bf9a/grpcio-1.74.0-cp312-cp312-win_amd64.whl", hash = "sha256:42f8fee287427b94be63d916c90399ed310ed10aadbf9e2e5538b3e497d269bc", size = 4490123, upload-time = "2025-07-24T18:53:39.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/d8/1004a5f468715221450e66b051c839c2ce9a985aa3ee427422061fcbb6aa/grpcio-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2bc2d7d8d184e2362b53905cb1708c84cb16354771c04b490485fa07ce3a1d89", size = 5449488, upload-time = "2025-07-24T18:53:41.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/0e/33731a03f63740d7743dced423846c831d8e6da808fcd02821a4416df7fa/grpcio-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c14e803037e572c177ba54a3e090d6eb12efd795d49327c5ee2b3bddb836bf01", size = 10974059, upload-time = "2025-07-24T18:53:43.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/c6/3d2c14d87771a421205bdca991467cfe473ee4c6a1231c1ede5248c62ab8/grpcio-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f6ec94f0e50eb8fa1744a731088b966427575e40c2944a980049798b127a687e", size = 5945647, upload-time = "2025-07-24T18:53:45.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/83/5a354c8aaff58594eef7fffebae41a0f8995a6258bbc6809b800c33d4c13/grpcio-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:566b9395b90cc3d0d0c6404bc8572c7c18786ede549cdb540ae27b58afe0fb91", size = 6626101, upload-time = "2025-07-24T18:53:47.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/ca/4fdc7bf59bf6994aa45cbd4ef1055cd65e2884de6113dbd49f75498ddb08/grpcio-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1ea6176d7dfd5b941ea01c2ec34de9531ba494d541fe2057c904e601879f249", size = 6182562, upload-time = "2025-07-24T18:53:48.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/48/2869e5b2c1922583686f7ae674937986807c2f676d08be70d0a541316270/grpcio-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:64229c1e9cea079420527fa8ac45d80fc1e8d3f94deaa35643c381fa8d98f362", size = 6303425, upload-time = "2025-07-24T18:53:50.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/0e/bac93147b9a164f759497bc6913e74af1cb632c733c7af62c0336782bd38/grpcio-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:0f87bddd6e27fc776aacf7ebfec367b6d49cad0455123951e4488ea99d9b9b8f", size = 6996533, upload-time = "2025-07-24T18:53:52.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/35/9f6b2503c1fd86d068b46818bbd7329db26a87cdd8c01e0d1a9abea1104c/grpcio-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3b03d8f2a07f0fea8c8f74deb59f8352b770e3900d143b3d1475effcb08eec20", size = 6491489, upload-time = "2025-07-24T18:53:55.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/33/a04e99be2a82c4cbc4039eb3a76f6c3632932b9d5d295221389d10ac9ca7/grpcio-1.74.0-cp313-cp313-win32.whl", hash = "sha256:b6a73b2ba83e663b2480a90b82fdae6a7aa6427f62bf43b29912c0cfd1aa2bfa", size = 3805811, upload-time = "2025-07-24T18:53:56.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/80/de3eb55eb581815342d097214bed4c59e806b05f1b3110df03b2280d6dfd/grpcio-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd3c71aeee838299c5887230b8a1822795325ddfea635edd82954c1eaa831e24", size = 4489214, upload-time = "2025-07-24T18:53:59.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/e8/2a69fe506c992fc4b02ede5a6255a4b19b7922527d7f0ab2229695463fc1/grpcio-1.83.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:907a5e5afb31f7a46376afc1a1edddd7afa00a74bbbc5b78979bbc34479581f6", size = 6340700, upload-time = "2026-08-28T07:07:49.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/56/6628e935ca7c5b9270810dd1cb61e5d2ea53eb7d26c62d2987bfef46e022/grpcio-1.83.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:547645f02499c972f3edec9be4db9997f1d03df307c1c199772342ed6d8b3c6d", size = 12183471, upload-time = "2026-08-28T07:07:51.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c9/f748ae4bd2120c91cf07e7a74cfd3ceac0e36b06d082cd3db802163a372a/grpcio-1.83.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:34f1841fc6d1d76f8a2d74177eafa2d1ec7d7e039633488c9fcc1b375a1fc165", size = 6924669, upload-time = "2026-08-28T07:07:53.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/d1/797b72d87b0ad8cce15fb4ad247472054655bce94bf64027b2285d7c3666/grpcio-1.83.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:05ba265193fbd9f63355311ec7567bba32a72aeb8e9fd7b3443e4fcad87b0750", size = 7654706, upload-time = "2026-08-28T07:07:54.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/bf/7f0850aa13d98bb4dd8e7633b6beb639e6fd80a8c0813ad36dec3240f70e/grpcio-1.83.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cce1d9fe2887239f054dc9c314597e04f33d2e6bd3150a91c4946d7e5be5d98", size = 7083816, upload-time = "2026-08-28T07:07:56.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/8a/5047fc4041cb6499d836001b4ac2ced0b00aecd2fc6d88e81993066ebbf6/grpcio-1.83.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f732feb060ef57c1a040c24cee072ba9fab99bd0a7d2c916ef3f1c4d84b98974", size = 7607412, upload-time = "2026-08-28T07:07:58.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/83/b397786f79323c2c127733d6310a5dbe3898333bc5d1153785dbdbcfce9b/grpcio-1.83.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:145b0050d24eb38accd9dc7ae09a3c09b8e7330159f3cfb46b1dba8711d50c42", size = 8643027, upload-time = "2026-08-28T07:08:00.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/3c/ac8ca4521760c8c5876af2f284109c3f826d3ba9836b2226c76ba06258ee/grpcio-1.83.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e844cdb25c3c93c7572e0a37137c12305efea493be4eb65801b3ee93f180c186", size = 8010270, upload-time = "2026-08-28T07:08:02.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/3a/ec8680e53ead511e8e3e6efb98771b32b96ea7a0c6a3ad23ffe85a8f83d3/grpcio-1.83.1-cp311-cp311-win32.whl", hash = "sha256:0d07661944477517b12a239e18720c8d9038f80a62f2c56260fae80327f43d2a", size = 4405295, upload-time = "2026-08-28T07:08:04.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/77/c169e2cee593c49399273912bacab20e50422489c0f884c1e3ae95a1af08/grpcio-1.83.1-cp311-cp311-win_amd64.whl", hash = "sha256:e572da3e247b28a98f46636d33c756e81ffb0f5def96c231ba45332333060595", size = 5166265, upload-time = "2026-08-28T07:08:06.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/9e/a3ba13e08bbee5bf6e57597dfe4823961fd7e94c0b8afe3a4bb7dca639f3/grpcio-1.83.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:5acd14c6ddf047de62cbf8745b11103ea91abbf57d1b8edd5395ccd9fcd13abb", size = 6303170, upload-time = "2026-08-28T07:08:08.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/ae/65ce56a2527faa17d02cba4c2231c74047ad898be339486ba87f093bfb66/grpcio-1.83.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:16138031a47b771860a16a975b53087f4fd5bbdbb2c03a188c5d90ad65d2bdae", size = 12165806, upload-time = "2026-08-28T07:08:10.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/91/40432480088a2243d360864de072ed5b78c4ebbaabd29c28918f1e1b1454/grpcio-1.83.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5ccc26715fd4defca5e129e280dd883b1737b65045ec50ffe22ce42104089519", size = 6872490, upload-time = "2026-08-28T07:08:12.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/62/3da2300c8c79fd20a78a8a4bb6251e5068d9af33bc8fd389b98fec35e8a3/grpcio-1.83.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b74f2a1d9ab1dfa3e263ef33d581613679b78d0884babf11671af26e45570ead", size = 7618367, upload-time = "2026-08-28T07:08:14.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/19/9fc702e31a631262d7a752fa699f6022821e707fefc8bff49b1550a57729/grpcio-1.83.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72578aa07a4008f17521ef52debcc3acfd1e2c5426243bc3ffb56a38bfe610b7", size = 7040936, upload-time = "2026-08-28T07:08:15.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/56/95933cc44cba2429765fa065c951dd529e5771b119d9d2481b4646f1d6a5/grpcio-1.83.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c12e1fc59c6dc26d10d9144453ddc6cbfe4cd4c31e874ed2d0132f88e685eb8b", size = 7573096, upload-time = "2026-08-28T07:08:17.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/80/af63359da06b016de48cb111f144703a10043850dafa43ae0a038907b9e8/grpcio-1.83.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4910b62f7d12197160bfb7de06d876d64dd12d43483e8292f98f49ca09b628d9", size = 8609442, upload-time = "2026-08-28T07:08:19.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/fa/f0586c56bdfb8a7a2adda01e0ac2413447cde3141ab09411a5d5afdcffd3/grpcio-1.83.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e703effe3ae779925c82ac24fdb82cf4105e1096810151ed9501c5f34546b9c", size = 7984321, upload-time = "2026-08-28T07:08:22.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8a/14ec05669f9eb295801e26c2ea8c561a1b786b0e3557c2c22131165ab010/grpcio-1.83.1-cp312-cp312-win32.whl", hash = "sha256:a2aea8bd6e0a34f12cbaddb7bb70bec836818789fa5c7ab7572c6b745396a2d4", size = 4395604, upload-time = "2026-08-28T07:08:24.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/37/8c2f7cc16089e36a3fbacaacc7a3d043912aa0d2dfae5556f6450414ea6e/grpcio-1.83.1-cp312-cp312-win_amd64.whl", hash = "sha256:583bf2e8255040a4a312f9572dfe62a05271437b149550e1a536d5c47d2d1e8a", size = 5161512, upload-time = "2026-08-28T07:08:25.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fd/d1fc58933bf88c9209f89dc570c810f1aa57cb04b3459cf2b26f61e32112/grpcio-1.83.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:8d228e253b77865efcbdd7b5894ca882c9e0ea98c02b7d20582e61ded8dfd4b5", size = 6305628, upload-time = "2026-08-28T07:08:27.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/49/0b40bae059c619505c9b751cee6caa208e4904e290aaefa1728c4c2c67a5/grpcio-1.83.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0468b627f2987c9a77f7580030207cbd85457ffe52998beff4f0b5c38c58a72c", size = 12156839, upload-time = "2026-08-28T07:08:30.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/4b/e8c0d635da0ee5ddd9950c8d540f5dcdd0ef1854a382cc55496a487a8d31/grpcio-1.83.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6a282e81530cead60bbd752cc04950a57f224379e9821495d6a35bd5ce9b1f4", size = 6877036, upload-time = "2026-08-28T07:08:32.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/d4/760a33f339a7dd3d5f4b3e0e9bec5472d95592a80f887b2e9dab4e41cfbc/grpcio-1.83.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:947d945f52e8ecf3cafd2bb7113502a16ccfda3e12c854443094de32d83ad432", size = 7624404, upload-time = "2026-08-28T07:08:34.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/ec/bd798654b06fb42a92b57d1dc1b530084fa89ed442806fcd0a833a36f9b3/grpcio-1.83.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:55656318d5dd387077396dffb929171ca3966e24bfead9a6c5dba9f889062cb4", size = 7042942, upload-time = "2026-08-28T07:08:36.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/b0/c00f86614566dd0961825cf0f43d4f96a74371d9d95f952bcbc4b86d9a27/grpcio-1.83.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9daf5acf4fc9d5f5627229969c2580a91e511779d76e4ccdeb9f4770f05d8bc2", size = 7576937, upload-time = "2026-08-28T07:08:38.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/38/85eff43a5c89dc666a252b5c9f8e9ab03f89e11c95b6263d2933f08fdbe7/grpcio-1.83.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7b94174cbca93316888f805efbeb08f1c020f7b7493d2d50cc4f6b64ebb7e8bd", size = 8608391, upload-time = "2026-08-28T07:08:40.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/4e/82835483e2f812494be865e7965c0d626cb9e71ab0d83a420d75aea4ad67/grpcio-1.83.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65c5a7210911ffe0f67b1cdc5308f9854b6d1f1b345e3e49ab7cac1ba50fa346", size = 7980060, upload-time = "2026-08-28T07:08:42.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/68a98bef733fef704fbcfb3957c8dba67e3e38ca7a7fea851195bc97c648/grpcio-1.83.1-cp313-cp313-win32.whl", hash = "sha256:179368d9361854616ce6f397d4716e07480129652752fcbcfc5a7260455ad6f2", size = 4395226, upload-time = "2026-08-28T07:08:44.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a0/df4de3b51d37ac8fb0320bb9668381ce2bd3b7aa990880bfc56a8a26f665/grpcio-1.83.1-cp313-cp313-win_amd64.whl", hash = "sha256:2e57af456385491a76e13c4aada8c8f43a8e47051e06ea97a9dbe2a49654e6db", size = 5160273, upload-time = "2026-08-28T07:08:46.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/9c/484d981d8b90c4e6abf3030bd2ed747e84d1eb192b3ec9cbb41e0b73e4bf/grpcio-1.83.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b3c87ca908296bf125f841d3e1a2225a2b39aaa8ed7a57e7ccde465ee519bab", size = 6306089, upload-time = "2026-08-28T07:08:48.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/01/0afec1c92e4f292f74a44ecf75eabbf40903125b8c4df103c9868d6338da/grpcio-1.83.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c0f3f20c90e72a171917ae65706500b096a1c3eb5f162c3ce702a2e25635f132", size = 12170381, upload-time = "2026-08-28T07:08:50.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/5a/e9a2383804433a0a61d6d93777ad321c7f36ac1cfdaa4c6d1a7c9ac846b7/grpcio-1.83.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81bbf35a46bf8cad2dfbb2eccc19c711befb58b288acb534bbcd0d74283202a6", size = 6883286, upload-time = "2026-08-28T07:08:53.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/e7/f8ca8f76994e14c70b9a0052e82f10de497a23db450c36379c9716ebfc4d/grpcio-1.83.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:215cec07d11176507387bda4bf2751816e880f9bff8dc1ca524bfbb8ed8f2fad", size = 7624293, upload-time = "2026-08-28T07:08:55.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/7a/4b672814b0cd0fe63bdd735379d88b165759f3144ab023ad8ec5fc4d53ac/grpcio-1.83.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:abce7d43ec29cd39230fa8339de1a07643b55adc412a454850fbd875349950ff", size = 7044346, upload-time = "2026-08-28T07:08:57.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b8/d89fe60e4239ad51be333dd9cc703741d449a35064e51f8a0b5bfa755432/grpcio-1.83.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e256f95a40e3b0183a98556fb7164d24b97eeb353123ccabfcba94712b35ee2a", size = 7584187, upload-time = "2026-08-28T07:08:59.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b2/b290d7402633d9166e4dd47e6f5f74a24ce10a8340b84455896ebc349f85/grpcio-1.83.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2110059146fb0ea216e1ffddb29377b5cc2fd412a5b0a92e102616bd5edf18c2", size = 8608730, upload-time = "2026-08-28T07:09:02.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/44/fa89e44d1b5cf5b9fa71b2fd7abf506f182fd43917231a92fbf1ea326b02/grpcio-1.83.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20d944d967843f8183f9f23d5916388362e5f8eeeae855bbe4354d906dc9f31b", size = 7983283, upload-time = "2026-08-28T07:09:05.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/b8/9db73ed1f35ffa76124ac574bf296d06a359798dfd6b50d382f2b8a060a1/grpcio-1.83.1-cp314-cp314-win32.whl", hash = "sha256:623c87c6d4a1cb30d82c4e896f95477050f2e01b4a1f8cf91ff2b1abdf89c457", size = 4474327, upload-time = "2026-08-28T07:09:07.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/22/fc9a622d885a7a37ff972a12faaef443d74e47407181da70d0ab62ab41f0/grpcio-1.83.1-cp314-cp314-win_amd64.whl", hash = "sha256:47e6934ad38779271e2e7cc5f78a63a407cf3d98114c65c1fdbcd3f5a716f29b", size = 5302032, upload-time = "2026-08-28T07:09:09.285Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grpcio-tools"
|
||||
version = "1.74.0"
|
||||
version = "1.81.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "grpcio" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/c8/bca79cb8c14bb63027831039919c801db9f593c7504c09433934f5dff6a4/grpcio_tools-1.74.0.tar.gz", hash = "sha256:88ab9eb18b6ac1b4872add6b394073bd8d44eee7c32e4dc60a022e25ffaffb95", size = 5390007, upload-time = "2025-07-24T18:57:23.852Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/b3/1c5951352d6777fd7f99a0ccee04617fdfd8a5dbf2918a1f58c8b2b280b8/grpcio_tools-1.81.1.tar.gz", hash = "sha256:a22a3870180927fdd84e2b27d079ef5b7f5f8c6110181b6736afc17a463481f1", size = 6236155, upload-time = "2026-06-11T12:51:21.235Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/50/7bafe168b4b3494e7b96d4838b0d35eab62e5c74bf9c91e8f14233c94f60/grpcio_tools-1.74.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:9d9e28fbbab9b9e923c3d286949e8ff81ebbb402458698f0a2b1183b539779db", size = 2545457, upload-time = "2025-07-24T18:56:12.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/1c/8a0eb4e101f2fe8edc12851ddfccf4f2498d5f23d444ea73d09c94202b46/grpcio_tools-1.74.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:41040eb1b5d1e582687f6f19cf2efc4c191b6eab56b16f6fba50ac085c5ca4dd", size = 5842973, upload-time = "2025-07-24T18:56:14.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/f2/eb1bac2dd6397f5ca271e6cb2566b61d4a4bf8df07db0988bc55200f254d/grpcio_tools-1.74.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:1fdc013118e4e9054b6e1a64d16a0d4a17a4071042e674ada8673406ddb26e59", size = 2515918, upload-time = "2025-07-24T18:56:15.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/fe/d270fd30ccd04d5faa9c3f2796ce56a0597eddf327a0fc746ccbb273cdd9/grpcio_tools-1.74.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f037414c527a2c4a3af15451d9e58d7856d0a62b3f6dd3f5b969ecba82f5e843", size = 2904944, upload-time = "2025-07-24T18:56:17.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/9f/3adb6e1ae826d9097745f4ad38a84c8c2edb4d768871222c95aa541f8e54/grpcio_tools-1.74.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536f53a6a8d1ba1c469d085066cfa0dd3bb51f07013b71857bc3ad1eabe3ab49", size = 2656300, upload-time = "2025-07-24T18:56:18.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/15/e532439218674c9e451e7f965a0a6bcd53344c4178c62dc1acd66ed93797/grpcio_tools-1.74.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1e23ff54dea7f6e9543dcebd2c0f4b7c9af39812966c05e1c5289477cb2bf2f7", size = 3051857, upload-time = "2025-07-24T18:56:19.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/06/a63aeb1a16ab1508f2ed349faafb4e2e1fb2b048168a033e7392adab14c7/grpcio_tools-1.74.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:76072dee9fa99b33eb0c334a16e70d694df762df705c7a2481f702af33d81a28", size = 3501682, upload-time = "2025-07-24T18:56:21.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/1f/81da8c39874d9152fba5fa2bf3b6708c29ea3621fde30667509b9124ef06/grpcio_tools-1.74.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bdf91eb722f2990085b1342c277e212ec392e37bd493a2a21d9eb9238f28c3e", size = 3125364, upload-time = "2025-07-24T18:56:23.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/64/a23256ecd34ceebe8aac8adedd4f65ed240572662899acb779cfcf5e0277/grpcio_tools-1.74.0-cp311-cp311-win32.whl", hash = "sha256:a036cd2a4223901e7a9f6a9b394326a9352a4ad70bdd3f1d893f1b231fcfdf7e", size = 993385, upload-time = "2025-07-24T18:56:25.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b8/a0d7359d93f0a2bbaf3b0d43eb8fa3e9f315e03ef4a4ebe05b4315a64644/grpcio_tools-1.74.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1fdf245178158a92a2dc78e3545b6d13b6c917d9b80931fc85cfb3e9534a07d", size = 1157908, upload-time = "2025-07-24T18:56:27.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/9c/08a4018e19c937af14bfa052ad3d7826a1687da984992d31d15139c7c8d3/grpcio_tools-1.74.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:61d84f6050d7170712600f7ee1dac8849f5dc0bfe0044dd71132ee1e7aa2b373", size = 2546097, upload-time = "2025-07-24T18:56:28.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/7b/b2985b1b8aa295d745b2e105c99401ad674fcdc2f5a9c8eb3ec0f57ad397/grpcio_tools-1.74.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f0129a62711dbc1f1efd51d069d2ce0631d69e033bf3a046606c623acf935e08", size = 5839819, upload-time = "2025-07-24T18:56:30.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/40/de0fe696d50732c8b1f0f9271b05a3082f2a91e77e28d70dd3ffc1e4aaa5/grpcio_tools-1.74.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:5ec661f3bb41f0d2a30125ea382f4d5c874bf4f26d4d8e3839bb7e3b3c037b3e", size = 2517611, upload-time = "2025-07-24T18:56:32.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/6d/949d3b339c3ff3c631168b355ce7be937f10feb894fdabe66c48ebd82394/grpcio_tools-1.74.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7970a9cf3002bec2eff5a449ac7398b77e5d171cbb534c47258c72409d0aea74", size = 2905274, upload-time = "2025-07-24T18:56:33.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/6b/f9b2e7b15c147ad6164e9ac7b20ee208435ca3243bcc97feb1ab74dcb902/grpcio_tools-1.74.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f56d67b04790f84e216353341c6b298f1aeb591e1797fe955f606516c640936", size = 2656414, upload-time = "2025-07-24T18:56:35.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/de/621dde431314f49668c25b26a12f624c3da8748ac29df9db7d0a2596e575/grpcio_tools-1.74.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3d0c33cc984d21525f190cb1af479f8da46370df5f2ced1a4e50769ababd0c0", size = 3052690, upload-time = "2025-07-24T18:56:37.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/82/d43c9484174feea5a153371a011e06eabe508b97519a1e9a338b7ebdf43b/grpcio_tools-1.74.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:88e535c1cf349e57e371529ea9918f811c5eff88161f322bbc06d6222bad6d50", size = 3501214, upload-time = "2025-07-24T18:56:39.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/fc/195b90e4571f6c70665a25c7b748e13c2087025660d6d5aead9093f28b18/grpcio_tools-1.74.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c3cf9401ce72bc49582c2d80e0a2ee0e573e1c3c998c8bc5f739db8845e8e148", size = 3125689, upload-time = "2025-07-24T18:56:41.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/81/fe8980e5fb768090ffc531902ec1b7e5bf1d92108ecf8b7305405b297475/grpcio_tools-1.74.0-cp312-cp312-win32.whl", hash = "sha256:b63e250da44b15c67b9a34c5c30c81059bde528fc8af092d7f43194469f7c719", size = 993069, upload-time = "2025-07-24T18:56:43.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/a9/7b081924d655787d56d2b409f703f0bf457b3dac10a67ad04dc7338e9aae/grpcio_tools-1.74.0-cp312-cp312-win_amd64.whl", hash = "sha256:519d7cae085ae6695a8031bb990bf7766a922332b0a531e51342abc5431b78b5", size = 1157502, upload-time = "2025-07-24T18:56:44.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/65/307a72cf4bfa553a25e284bd1f27b94a53816ac01ddf432c398117b91b2a/grpcio_tools-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e2e22460355adbd0f25fdd7ed8b9ae53afb3875b9d5f34cdf1cf12559418245e", size = 2545750, upload-time = "2025-07-24T18:56:46.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/8e/9b2217c15baadc7cfca3eba9f980e147452ca82f41767490f619edea3489/grpcio_tools-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0cab5a2c6ae75b555fee8a1a9a9b575205171e1de392fe2d4139a29e67d8f5bb", size = 5838169, upload-time = "2025-07-24T18:56:48.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/42/a6a158b7e91c0a358cddf3f9088b004c2bfa42d1f96154b9b8eb17e16d73/grpcio_tools-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:9b18afca48b55832402a716ea4634ef2b68927a8a17ddf4038f51812299255c9", size = 2517140, upload-time = "2025-07-24T18:56:49.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/db/d4576a07b2d1211822a070f76a99a9f4f4cb63496a02964ce77c88df8a28/grpcio_tools-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85f442a9e89e276bf89a0c9c76ea71647a927d967759333c1fa40300c27f7bd", size = 2905214, upload-time = "2025-07-24T18:56:51.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/dc/3713e75751f862d8c84f823ba935d486c0aac0b6f789fa61fbde04ad5019/grpcio_tools-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:051ce925b0b99ae2daf61b3cba19962b8655cc2a72758ce4081b89272206f5a3", size = 2656245, upload-time = "2025-07-24T18:56:53.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/e4/01f9e8e0401d8e11a70ae8aff6899eb8c16536f69a0a9ffb25873588721c/grpcio_tools-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:98c7b8eb0de6984cd7fa7335ce3383b3bb9a1559edc238c811df88008d5d3593", size = 3052327, upload-time = "2025-07-24T18:56:55.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c2/264b4e705375a834c9c7462847ae435c0be1644f03a705d3d7464af07bd5/grpcio_tools-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f8f7d17b7573b9a2a6b4183fa4a56a2ab17370c8d0541e1424cf0c9c6f863434", size = 3500706, upload-time = "2025-07-24T18:56:57.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/c0/cc034cec5871a1918e7888e8ce700e06fab5bbb328f998a2f2750cd603b5/grpcio_tools-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:db08b91ea0cd66dc4b1b929100e7aa84c9c10c51573c8282ec1ba05b41f887ef", size = 3125098, upload-time = "2025-07-24T18:56:59.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/55/5792b681af82b3ff1e50ce0ccfbb6d52fc68a13932ed3da57e58d7dfb67b/grpcio_tools-1.74.0-cp313-cp313-win32.whl", hash = "sha256:4b6c5efb331ae9e5f614437f4a5938459a8a5a1ab3dfe133d2bbdeaba39b894d", size = 992431, upload-time = "2025-07-24T18:57:00.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/9f/626f0fe6bfc1c6917785c6a5ee2eb8c07b5a30771e4bf4cff3c1ab5b431b/grpcio_tools-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8324cd67f61f7900d227b36913ee5f0302ba3ba8777c8bc705afa8174098d28", size = 1157064, upload-time = "2025-07-24T18:57:02.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/76/14ff87090199a36f914388299a1148d0734a20cea1b0ca8480bae1f373f1/grpcio_tools-1.81.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:8161f398f957a376cae7385ea7c8684f439d460ef702b528912da3bcb31fc515", size = 2586251, upload-time = "2026-06-11T12:49:43.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/a8/d5aa99de9d8b2dd2a8192c1779796eda8b0d0f1dd915422e0a8a61b80391/grpcio_tools-1.81.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:53ef76cc3b0493ff734a5e8c39d5b519e1822236fcccdfe7677c5e1efd767761", size = 5818063, upload-time = "2026-06-11T12:49:45.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/cb/2e9a6dbc6a514dd3cd264fb3bf9217937453a4d45dbc3ca6ca4ee34ba1a7/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:690e6dcaa8b8a7886ce206ba344e2127211597e1a1ddab73df9f3d80c8f6707e", size = 2634061, upload-time = "2026-06-11T12:49:48.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/2b/2ccd1a929e6c8ad84a0aa8d66ad9f615b4a8e79d9927373d86aa36b4ba2e/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ad7a997c07bd345e84842e60561e7e2cc090ce6c4e1d2f0407e31b85b40fc49a", size = 2958029, upload-time = "2026-06-11T12:49:50.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/67/2da8cd312edc348f44f26f82096b25cdb7d2905cd786acc6bf777b169502/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b6bd163ece4535726e5292b845ed80ae9b2cae73ba091c7d6c66033c430e3857", size = 2698031, upload-time = "2026-06-11T12:49:52.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/ba/ad1680fbdf9317c4f1e54c37c96d1f422370df66ac9adbd175c7cb3531d7/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2baa7e735f35b2a648144c03348a126097b13e101d3c242d5edb6ac91437ccbe", size = 3147541, upload-time = "2026-06-11T12:49:54.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/c1/57cd08eef293d713cb8935295e4f08d8f0013480b2ba3aad1af0271eb7ba/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d602b410b2b2addc434cace9ce4fe2035974a3078228f98ffa049a5c90acc2f", size = 3708524, upload-time = "2026-06-11T12:49:56.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/31/01ea8ca9c82fe2c79b5b594c3ae427d56699bc106b2d91caca129add8b10/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8cb64f87c45ccca8234fa47e6b21f09e43801ff11b556deecb461b3b3e9f292", size = 3367022, upload-time = "2026-06-11T12:49:59.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/35/8140cd175602df3d17215cfb28a7ea55b7a67e2b872be76e1ee4af5c4df9/grpcio_tools-1.81.1-cp311-cp311-win32.whl", hash = "sha256:87b25ca0e27373a4a32a629a4ba976f5764b9887dd50d6fe017d38009a0363e8", size = 1008980, upload-time = "2026-06-11T12:50:01.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/86/1bd29ab3c52457702b96536f1f208ab27695322d855f95c9666dfb713019/grpcio_tools-1.81.1-cp311-cp311-win_amd64.whl", hash = "sha256:204de03b539a4b08772c6553b92bcc112cbc965e0ac22f909f6d133b8ac33a8c", size = 1174840, upload-time = "2026-06-11T12:50:03.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/8a/824a9ca20bcdce8a568bb8c9f98bfeb7fad62129235e6d2ae7576fd1250a/grpcio_tools-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:353b1fafcc739c31ed42271052709595b340d34f27c459beeb78a32938305bb5", size = 2585927, upload-time = "2026-06-11T12:50:05.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/35/e5f9f671378b1b89a896150d3e4fa2c6ec61a5e1e9e5107ce4c140ccc931/grpcio_tools-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:768f584c2423cbeb6cb6867817a39365b987ff16b8259a3adbc6546b9e303a4e", size = 5815665, upload-time = "2026-06-11T12:50:08.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/02/631b628e4072e988c669bd8f1b2406ef3c9a4cfcb2625bbf2a308a07b71d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1680b35a84f4694401819ac4acac42dda6dbc7bb8fc74112fd1a60425a07adf4", size = 2635518, upload-time = "2026-06-11T12:50:10.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/7c/2e3537e3ea3d1c0ddd6766cf6a7c62b487d89fb005713df2781d5f21483a/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f64e665c8ec639278ecf009beb92cbdcc5994f617c1af3d58036e1f70b1423ec", size = 2958252, upload-time = "2026-06-11T12:50:12.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/68/14013cb2942bdac354746b643b4c37dd91906da8dce00f41c616e88bf33d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f1ae82ad199f43448995715445cc623fb20d3882382e4be61f0da8ccb3f0e", size = 2698439, upload-time = "2026-06-11T12:50:15.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/45/000c14c0338a7ad36054b9f17ea41842deb7841c05c067dd36cc831bc0f4/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7b6d1e986d5923751bfe2b5cca9c4cb3d5653446e4fa4aacd438033e2dc360a", size = 3152160, upload-time = "2026-06-11T12:50:17.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/97/881930ca3967d2c8a95649bea8ebc991a7cf2331bc96679fd3600450dccc/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f208c207aca639dcb34648d3826c38d7cf3485118fb2065117e9fc4827406b3", size = 3710468, upload-time = "2026-06-11T12:50:19.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b5/67baeba7366162652cdc1dbd962289accde07241bc8f42f6f02b305efcc6/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724ecb69af63d2f6d4ccea3e6fa0ca110ed9c5824d48c2f887c631bbb03c1c3c", size = 3370797, upload-time = "2026-06-11T12:50:21.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/5d/34f2dce2125ccb107e32b57f5a9c1257edcc0793b0d2fef1e8b13a6bac3c/grpcio_tools-1.81.1-cp312-cp312-win32.whl", hash = "sha256:895a6782cec86beac71ccebb4b9848259c6f04a3028b8e42fa8d40cfe5146593", size = 1008453, upload-time = "2026-06-11T12:50:23.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/be/09da8256ec8d2a5ce8a1acc51cbbc4ca52a462d78ed3412778440a56502e/grpcio_tools-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:0265fd1386b7458302f79542558345880d484f8fa92ae196c0c0268242c5f23a", size = 1174857, upload-time = "2026-06-11T12:50:25.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/90/5faa8b26e03495e5117f93bef8293cbada4af136362745dad7d1813ef0b0/grpcio_tools-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:3d604b4fd114b79ebb9f865bf3e04fd3ae93c704e1fad96f7fd03b0865c263b7", size = 2586071, upload-time = "2026-06-11T12:50:28.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/9a/85dc589fa6ae2439451eaa81a1578de31e29c676980d38bef7549b8a1f45/grpcio_tools-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3389e705460efa3f3758141ba5520e6743b131c9576197c944fb9cbe49048126", size = 5813299, upload-time = "2026-06-11T12:50:31.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/fd/c53994e58a837e6eefe48f53eb3492afc04f2b8af255df4adb37d14378f8/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8a17d8ceeb6a855fadf39f5171c80a382d97c4db98d5943eca553497fdebf84b", size = 2634668, upload-time = "2026-06-11T12:50:33.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/32/de988e86688686a2117e7ce6ce9eff4f638c929bb55b0afe60d6fbd2e45c/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:43baf71dc60fd653062da2e95e95c73b35dd130be8f9fa3d544c3af3f808a290", size = 2957930, upload-time = "2026-06-11T12:50:36.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/97/3f18a0ea32b5f809d21961dbd0bc382b589a4c3d501e3d67c345d5456ed3/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136e90906af0df51ad929713244ba812d0dbb1844b4f467d5d86bdb054698f90", size = 2697760, upload-time = "2026-06-11T12:50:39.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/c0/dbf5cbc877290ff7504a59959a8af4fdcfdaa1e84237948405ccf1aa82a6/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd6c3bf3ea6a61eb58c54368d72ada591f2a270f3a31a32e8536e773337e76d9", size = 3151456, upload-time = "2026-06-11T12:50:41.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/ea/16fe2dc83140a59e5c0a0b9dc2693dd36bfaa6bd835724b4ec66a68eab7b/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c306c307f8f74cddc4056fdbb6f1da55de087a21120efbd02bd915daa5a52fd", size = 3710469, upload-time = "2026-06-11T12:50:44.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/7d/df987d7d81e7ad2f7516d9e9d56ff29c54dbc6d8587e425688dca9a28e49/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bdbdc927be2e0ea13c32564a72ee31d712a716fb6f8c0d53d37a77d8277c272c", size = 3370488, upload-time = "2026-06-11T12:50:47.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/c5/5a63444d694ea47bf670138208f71830cc1759c402c8818092b28ab2dc5f/grpcio_tools-1.81.1-cp313-cp313-win32.whl", hash = "sha256:9d383724bcd67244b6def9e9164c640ee9380c0b7534ee7545a6fb0022a59afe", size = 1008229, upload-time = "2026-06-11T12:50:49.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/75/3945e26d5c94ae6ed9be5caef73d4d66c47dc8cfdd7b4995efaf942754e0/grpcio_tools-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:f3eb15849979ca7bb864ce81a74d68b0f225a7f111ed3fe212bfc08cf9812b10", size = 1174523, upload-time = "2026-06-11T12:50:51.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/08/e581ad42ae517a61172285047e4d710e2ac75f2f1915f7c91f284254e6d5/grpcio_tools-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:7d168ea26390717d0462c0d0408331dc98a60fc7f7e6118afac9b73f5a66d87c", size = 2585944, upload-time = "2026-06-11T12:50:54.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/c8/200d90ebad685af7eea5ff7e0360c504dd01ec053fe0f1f9c4abe3ea2d5a/grpcio_tools-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:43c528655b226375013036692d8db4cd59060c1f41dd62c77f4d17b69f6ce828", size = 5813492, upload-time = "2026-06-11T12:50:57.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c0/60da2a1af37aa8eb47308cec24d9f7709a8976fdec3a53fd35b56b358326/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a9c6fcc68c9d5a208967bfe4fd3224d3c3be9a950c3e827e8f4b17e15c2dc555", size = 2634991, upload-time = "2026-06-11T12:50:59.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/7f/dede28b579ae9bf9079ba1aa913e8088d1dc0cdbe21c85caa22f0790cad2/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a987c85dcbe1b32066d7acd46266d1a428aecbd629331bf5b853e74c835bf876", size = 2957913, upload-time = "2026-06-11T12:51:02.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/38/4de2118adb58ec7ffba65ec623b5836db769665c192517cbf187db3f6145/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a882382507bb5ec6d7edc9648053dfd3bc8f9285cde56a6fa9b9a83b4bd07f1c", size = 2697709, upload-time = "2026-06-11T12:51:05.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/e1/762ced51059e4f694fd337ecae491581d42a4e61dcb0415d8c5c60e6ddcb/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7746e508d4239a02f7e93638be5bc0ebb0120ddb796f7506aaae9d47a4599d97", size = 3151884, upload-time = "2026-06-11T12:51:07.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/d8/9823090dc801e7229944874e7429c3b98e741ac778d8dc373f60240e1c43/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fc3d2a41a7a4467fa03b391394fffada9291fe8feebc8679b526f6bc36942b25", size = 3710404, upload-time = "2026-06-11T12:51:10.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/4e/4eae98d02148cb6f9f452f09942afba407afa6851e6c1fddc5ae9ec0b4ed/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:21bb3ba90e6d8df1ff663d4ee39a4e5b25a64e8ed4902476ca9ded0954d3917a", size = 3370525, upload-time = "2026-06-11T12:51:12.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/3e/2206e597a128da6a03a6106d2eaf2c3e72c7d80843d4be933e3a3d10d02a/grpcio_tools-1.81.1-cp314-cp314-win32.whl", hash = "sha256:3dca56016d90a710c4d9861bae793dc089c1430a90c79ce672e948ddb65fa539", size = 1030582, upload-time = "2026-06-11T12:51:14.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/f2/bbeef86c687225b7bbc7c0acdfbd25c8bcaa3f5b1c941db053e5c3d9e859/grpcio_tools-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:cb08172b7b629e75cb33866928d319a3196540a725eaab628ba721007140f1af", size = 1207490, upload-time = "2026-06-11T12:51:17.598Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload-time = "2022-09-25T15:40:01.519Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1192,15 +1327,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385, upload-time = "2025-04-11T14:42:46.661Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732, upload-time = "2025-04-11T14:42:44.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1234,18 +1369,17 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.25.2"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/23/911d93a022979d3ea295f659fbe7edb07b3f4561a477e83b3a6d0e0c914e/httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8", size = 123889, upload-time = "2023-11-24T12:36:33.988Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/65/6940eeb21dcb2953778a6895281c179efd9100463ff08cb6232bb6480da7/httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118", size = 74980, upload-time = "2023-11-24T12:36:31.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -1261,7 +1395,7 @@ 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 = "httpx", extra = ["socks"] },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "tqdm" },
|
||||
@@ -1284,14 +1418,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "6.11.0"
|
||||
version = "8.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "zipp" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/eb/58c2ab27ee628ad801f56d4017fe62afab0293116f6d0b08f1d5bd46e06f/importlib_metadata-6.11.0.tar.gz", hash = "sha256:1231cf92d825c9e03cfc4da076a16de6422c863558229ea0b22b675657463443", size = 54593, upload-time = "2023-12-03T17:33:10.693Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/72/c600ae4f68c28fc19f9c31b9403053e5dbb8cace2e6842c7b7c3e4d42fe9/importlib_metadata-8.9.0.tar.gz", hash = "sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee", size = 56140, upload-time = "2026-03-20T16:56:26.362Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/9b/ecce94952ab5ea74c31dcf9ccf78ccd484eebebef06019bf8cb579ab4519/importlib_metadata-6.11.0-py3-none-any.whl", hash = "sha256:f0afba6205ad8f8947c7d338b5342d5db2afbfd82f9cbef7879a9539cc12eb9b", size = 23427, upload-time = "2023-12-03T17:33:08.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/f9/97f2ca8bb3ec6e4b1d64f983ebe98b9a192faddff67fac3d6303a537e670/importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f", size = 27220, upload-time = "2026-03-20T16:56:25.07Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1418,13 +1552,13 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.83.0"
|
||||
version = "1.93.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "click" },
|
||||
{ name = "fastuuid" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx", extra = ["socks"] },
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "jsonschema" },
|
||||
@@ -1434,9 +1568,36 @@ dependencies = [
|
||||
{ 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" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/97/dd/28024c0e4cf2dc6ab1bad59b8357af7f460e952c69526eae28f12ac4ee5e/litellm-1.93.2.tar.gz", hash = "sha256:c5d5223ef07f36e0886397fb45cc9db4150f86a0c6f6835cee1d5524cab69dfd", size = 15955441, upload-time = "2026-08-09T02:17:49.646Z" }
|
||||
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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/c7/cb3f49dc60d57dda7fe368310fd5da2a94ec9b6a746bcf343a61e10bdeda/litellm-1.93.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:1bd0690efc94357e559de97927fd98437555cd5b5dd832544cfcca87297ccb80", size = 19938326, upload-time = "2026-08-09T02:16:38.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/bd/d77184fdaaf57d67d65da91dcfc61c7f656703e7ce4f950e07523e7de4e3/litellm-1.93.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:845ececc628737909b1422d1af18bd19ae453727a66244aa9da3ca37a3773111", size = 19862606, upload-time = "2026-08-09T02:16:40.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/99/d8dd58b6840754a13cc2e1111b283aa28cbfc0ccc653a8725050916bb08e/litellm-1.93.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:498f9878ea773305e0638b6159d7e1ef27bb0b9a4292538d6634312d18a4e781", size = 20168532, upload-time = "2026-08-09T02:16:42.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ca/559ca0f5e0b99b9f641086ae924c782f8d521d09384fbe9abbe0bddb6e61/litellm-1.93.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1e5618ef495b2e02299b376ca84ffb2647837aafee478cf3a1be17d47a8f0f73", size = 20162696, upload-time = "2026-08-09T02:16:45.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/3e/18c31b27c7d1271b43bdc8ffbef01bfba68d90248bbe60bb2130dd17e43c/litellm-1.93.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2da463d70c9fffbea9532fd000e035328f5266b399a2fb4c6c76b3470478337", size = 20233518, upload-time = "2026-08-09T02:16:47.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/98/a6bae7c52f09cd03487a040f98eeedb899b3cf3fc541b87c6d051ee92e0d/litellm-1.93.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2cf122399f84f8f04621ed6ef8f276dd6d61f4fab108932ce0e30368de34dd42", size = 20291180, upload-time = "2026-08-09T02:16:50.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/2d/81d974f2533cf039afda7e3e0f769dc73dc692c75ec867cf29ec6f41c06f/litellm-1.93.2-cp311-cp311-win_amd64.whl", hash = "sha256:8eaaf780fab9a19234735ef94225172179d15bc28b67ddbec125194249a504b7", size = 19775654, upload-time = "2026-08-09T02:16:53.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/05/72fd8051f0f2f3c84b90986e6f4551db7c8b190ba3300f111461b7701689/litellm-1.93.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3bf532c164ad7cb1b76f2c62afefdcc656b9b296374d075a4150e2ce10bb74c3", size = 19937403, upload-time = "2026-08-09T02:16:55.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/4d/5081b39bdb73cab04f8a86294a4534a029cf0434ac6932c7ae8049d55723/litellm-1.93.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:526b7afc037f79dfdd5c607f5085ac597c7fd301a6dedabea40baae899b27f19", size = 19853652, upload-time = "2026-08-09T02:16:57.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/3f/fb70691266a7fd08c202406abea0153e82fa17f134cd9d58e4029cc741db/litellm-1.93.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:294ad19f356f821ce97a5428d09439be5f38d22b218c73008d8a49e3e42eb145", size = 20165680, upload-time = "2026-08-09T02:17:00.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/91/84424ce2a25595463e5d24e9cf8949877cd4ce93c0fcbf6486ecd685094f/litellm-1.93.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6f6a5e3907f0a1c9d8ff8d71a6cbac8a592e47a40da3f97167074947b5ba7d11", size = 20157772, upload-time = "2026-08-09T02:17:03.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/8d/b0eac7ee6d174564f820565c8c9a726ae83dbb8c4d3522daf175b95da002/litellm-1.93.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8541f1b7fd5c437ad249ad68d0a11f68e5e2866b0649da5fa7d63b595e9b8b22", size = 20229256, upload-time = "2026-08-09T02:17:05.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/6d/03e931c1cb2d1e1b7a968de21aa9e4db853928200da856c35c940ee6faa9/litellm-1.93.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:712c9387419d7b06a10df59973f5e530592d61b2314102b0fa3142f3743f9a9e", size = 20287257, upload-time = "2026-08-09T02:17:08.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/05/6c0fe2fcf31c260474c55fabe4ecb0e9e1343c9b9132e28589391b2ad33e/litellm-1.93.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc0d58ccabd22ef7ef44a9e6f7247deb54ae42f5e126e6f00360c2b28b41bc2b", size = 19772580, upload-time = "2026-08-09T02:17:11.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/74/e9046cffa69b32b710452480598e418b26a29896ece680c80ec23997fd16/litellm-1.93.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f4071bef03e4c2942cd2ddc752727345b85447d6a7fee1ff5a4f8b92187966b0", size = 19938095, upload-time = "2026-08-09T02:17:13.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/db/6ef38a7a2f73d5cc507423954fa535a8546ead375c4c71265c093bdb4e9e/litellm-1.93.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a99ac7c0c1b78acd6bfd1959e9f203dca71fdbceb5f0c8691c2ad8eee450d7d", size = 19854187, upload-time = "2026-08-09T02:17:16.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b3/80ee0143b88e2921f8c8f24c7331478258a8bf25a3d4d4450bd96043403e/litellm-1.93.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a81ceff44c58ef504ab8bd787d03b82618765b9cfd530942386ae6d23c58be94", size = 20166307, upload-time = "2026-08-09T02:17:19.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/60/cb326e1094f7042f28f9e21543d9f367a8aa25af6915bf4253b77da5c2a2/litellm-1.93.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:dee1b02b7f52a5a408bf7c8d499f0834e49194651743758a511dcdd926c0b692", size = 20158336, upload-time = "2026-08-09T02:17:21.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/87/bad75146863531172c9dbae189486c7f4425b56a6641b55ab20745316048/litellm-1.93.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2edfa14b99bce706b35981703692e3ee631f9b87bf6dc28fb53b574f6480b20", size = 20229711, upload-time = "2026-08-09T02:17:24.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/28/040b1853021ed8fd57be19eb2affb024d168951fe7e7abdbad91da3f6f3f/litellm-1.93.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae75a61c9abc827aa3131b7e640c952367a450830bb7c531b426b4ec2bb45f85", size = 20287584, upload-time = "2026-08-09T02:17:26.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/0b/4208815b0d666636cbf7afbd571eec3004d3a15d3150a23a9009fc2ce930/litellm-1.93.2-cp313-cp313-win_amd64.whl", hash = "sha256:c54a09ab20f94120a9d60a30d9970439dcefa00d2565d190505ff006a80c7a69", size = 19772641, upload-time = "2026-08-09T02:17:29.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/4a/ff7a9c000519d2bab362318bf744a24c2500228e5fceaa6ac23acab96fa0/litellm-1.93.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:204cb0763fff9285bc87eb2dc0fc59b591999e5d94863d0964f806424d3c0cd6", size = 19943639, upload-time = "2026-08-09T02:17:31.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/26/29e9276ce4aa8ed133d9fd5ecc07375017d2228215547c6bbb17ccbc59b4/litellm-1.93.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3126c84361606b9fb07fde7d57eccd8a1747304d64c4143e2e5e40ae6e7693fb", size = 19855435, upload-time = "2026-08-09T02:17:34.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/20/2c9c818248ae019b2d496ca41900a9a5651ab05e2400794cd8dc8b89b6d2/litellm-1.93.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:1c84f7c4acb4e926a79b93145ab23231b300fc687bde7172ef884fc52d6011e0", size = 20166947, upload-time = "2026-08-09T02:17:36.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/af/4016682be48350407837941ad1a1ae8185cca65b102e04e89eee2a2abccb/litellm-1.93.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:cacf35cf703b12c54516fc6464a3e08c6dbb1dcfb97239e1f629294fe36a1cba", size = 20160055, upload-time = "2026-08-09T02:17:39.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/b5/c25d7fbe08490d8211bd6b69af23f3a922b68ad8c87c776480b0de64a505/litellm-1.93.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a7f3e5138e307e429bd8fa29cc0c48bb1e2b827792e8f7799ca4c8cff736103", size = 20230910, upload-time = "2026-08-09T02:17:42.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/27/341b18a40d4d98a2ac09025c248a3a7edddaf15ce4096ac4a783ff2f70db/litellm-1.93.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8684629be3f7b5f8e2b6e5fe5ea27ff957c63a8d525d81c1460d8436e2e1857", size = 20288903, upload-time = "2026-08-09T02:17:44.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/45/dd9ef72075a83854f852b1bf9a97ec7029a2be9fb4e338fc6623eb09fc90/litellm-1.93.2-cp314-cp314-win_amd64.whl", hash = "sha256:a783b8b18ed68cb6a3b79d2b00273ec21aef92442e9b2712a50036cb84bfe583", size = 19772974, upload-time = "2026-08-09T02:17:46.972Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1696,19 +1857,23 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nostr"
|
||||
version = "0.0.2"
|
||||
name = "nostr-sdk"
|
||||
version = "0.45.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "pycparser" },
|
||||
{ name = "secp256k1" },
|
||||
{ name = "websocket-client" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/e1/1e24d8d2d75d28871f5b7d03304eda8250121ab665180872b9ba4ff70cc9/nostr-0.0.2.tar.gz", hash = "sha256:5c0c472f69764ae57870710d6b3bfe584df3ccdb0e2e3cd3f302f6b848124d24", size = 17189, upload-time = "2023-01-26T14:02:38.676Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/ef/468ed56f0bea8e8979acf561273f8af2e7c9b3d8dc37bf80e81df08372a3/nostr-0.0.2-py3-none-any.whl", hash = "sha256:3d17d22dbd3aecf1ddf8cc72e330f14702e159fb0f43320d1ac88142db96aaba", size = 15397, upload-time = "2023-01-26T14:02:36.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/53/dca0a65a44538448cc18ad358fe8c8cd861f7d2733781834a232f0cb13ae/nostr_sdk-0.45.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8f5a121884ca991f4563354305705f78fa1822bf9b3e54ffbf64d6b89e4ff196", size = 3839165, upload-time = "2026-08-19T16:10:20.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/81/aa8b46b84fea0a71873643b3e31fe927bb705f6426a22218dcde5ffdb265/nostr_sdk-0.45.1-cp39-abi3-macosx_11_0_x86_64.whl", hash = "sha256:2b13cea42c9329b85e3748680058c77eab66f760652da871e8ce8808afbd9400", size = 3893446, upload-time = "2026-08-19T16:10:21.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/e3/084ac8f2ae947b86d3d8c5ae89e3f7b0389c6e4ab207397c95bdc76121d2/nostr_sdk-0.45.1-cp39-abi3-manylinux_2_17_aarch64.whl", hash = "sha256:b7d1ddb5e84c49d394c5a5290fd8982d75dd05684d07fda1f7cf10434d7671d0", size = 4319292, upload-time = "2026-08-19T16:10:22.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5b/0cfea91f8af05bc677aea4980a66759b7f75e5722dd4976de3f607c14300/nostr_sdk-0.45.1-cp39-abi3-manylinux_2_17_armv7l.whl", hash = "sha256:1d68cdf81ee7c0356731b1f148413bc393abd03fabda0845e66c9e0e7fba1605", size = 3859951, upload-time = "2026-08-19T16:10:24.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/fb/37a5b1080f4b5bd12537325f23d1652be2c3c9a8e4d1b36c0259e96566a9/nostr_sdk-0.45.1-cp39-abi3-manylinux_2_17_i686.whl", hash = "sha256:4f778445d9fcfc764a941e8918089879e0af1f6d9b2d6bd53af865bac10ef361", size = 4379812, upload-time = "2026-08-19T16:10:25.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/fe/f05d7e8b9fa2c3541d4a4eeb077a3db63c1d722f991c6b08d8d2a8e3afc5/nostr_sdk-0.45.1-cp39-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:5e051a662fac81aa2e15e8221915a0ee4c38791fe6284bc286c5f4cc769cc22e", size = 4417903, upload-time = "2026-08-19T16:10:26.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/9e/ed21138d709ad5fd11a2a23c699131be6a8d97bbf4d6482e90e3caceac08/nostr_sdk-0.45.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8c769eee70727d8f73aef2032aa2fb599538c871748c18dae2738c30f77da2b7", size = 4328833, upload-time = "2026-08-19T16:10:28.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/21/862d0dc7cda61174e5b4fa17d85e6f5565e024517b5dc834db6cbdb204b4/nostr_sdk-0.45.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:154eb4227948507b0cee98a73872443bb71a54c78726e2f55aa95e353694d618", size = 3850546, upload-time = "2026-08-19T16:10:29.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/0f/0d43864bb71e41b7d5b84ec3e480693b283780948946fec6322a2ae1de45/nostr_sdk-0.45.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:fbdb80e0c9daa57804c50a78c2928fa00fe15deef24edbc296fe41db8b3eda68", size = 4209394, upload-time = "2026-08-19T16:10:30.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/2e/efe77f4ba3c91d6cb4bb06845d157d51fa64e0bedf979f8e6e934ca6850e/nostr_sdk-0.45.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3375566eb6e35af9cfe8d0e4f459d485543a24f8bbe3a05c7a459529a764cd2d", size = 4416706, upload-time = "2026-08-19T16:10:32.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/7b/865d81abd1b368dbe17a85f57ce468b130d5913a6bc30e50670f68380596/nostr_sdk-0.45.1-cp39-abi3-win32.whl", hash = "sha256:6bcd1f1462a6ce946bb8943a1c0c078431d645e1d7e2f2124c9bed508d3a22a0", size = 3544889, upload-time = "2026-08-19T16:10:33.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/8d/4cd89a50d2c9eb568c6303f03c6c8f9b938e672c29ae01214af551764a27/nostr_sdk-0.45.1-cp39-abi3-win_amd64.whl", hash = "sha256:937d1e8eec83114bfd1821e78a5e1a7130473c8998e98cdf8408361021e9590b", size = 3864710, upload-time = "2026-08-19T16:10:34.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/55/69ab2693809deba62311323ccac7d8aced80750893b1675dd1bff418562f/nostr_sdk-0.45.1-cp39-abi3-win_arm64.whl", hash = "sha256:cf246932c5a405a3acc0e921e94615fb8ef5b3e88280735b5a525f46fe906dac", size = 3706921, upload-time = "2026-08-19T16:10:36.232Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1718,7 +1883,7 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx", extra = ["socks"] },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
@@ -2110,6 +2275,19 @@ wheels = [
|
||||
{ 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-extra-types"
|
||||
version = "2.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.14.2"
|
||||
@@ -2421,34 +2599,34 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "rich-toolkit"
|
||||
version = "0.14.9"
|
||||
version = "0.20.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/4f/ec4addb95da2abe9e988c206436193d3b4e678f3113b40dfd61628a2d7e6/rich_toolkit-0.14.9.tar.gz", hash = "sha256:090b6c3f87261bc1ca4fe7fc9b0d3625b5af917ccdbcd316a26719e5d3ab20b9", size = 111025, upload-time = "2025-07-28T13:25:39.604Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/1c/f134352beb393cc17e6241ecf0bf4dd41a6759e2e3971a69a6ad185b87a2/rich_toolkit-0.20.5.tar.gz", hash = "sha256:0c9e1c414ffb0720be26285d472e263d1e704b71d31a7e13274b9996db4969e1", size = 213207, upload-time = "2026-09-08T16:13:12.007Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/13/39030884b963a602041e4c0c90bd1a58b068f8ec9d33baddd62216eee56c/rich_toolkit-0.14.9-py3-none-any.whl", hash = "sha256:e2404f1f088286f2f9d7f3a1a7591c8057792db466f6fecabfae283fa64126e2", size = 25018, upload-time = "2025-07-28T13:25:38.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/5f/ee8c39750ed9837f54021a8a0eb96f971cd788c1a7d40931867d6d0518bd/rich_toolkit-0.20.5-py3-none-any.whl", hash = "sha256:e21fd616db9c0539d3c50f3433fb1173db95c19d3cbf215615e224182fad6c18", size = 39370, upload-time = "2026-09-08T16:13:10.885Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "routstr"
|
||||
version = "0.4.5"
|
||||
version = "0.4.7"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "alembic" },
|
||||
{ name = "cashu" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "fastapi", extra = ["standard-no-fastapi-cloud-cli"] },
|
||||
{ name = "greenlet" },
|
||||
{ name = "h11" },
|
||||
{ name = "httpx", extra = ["socks"] },
|
||||
{ name = "litellm" },
|
||||
{ name = "marshmallow" },
|
||||
{ name = "mdurl" },
|
||||
{ name = "nostr" },
|
||||
{ name = "nostr-sdk" },
|
||||
{ name = "openai" },
|
||||
{ name = "pillow" },
|
||||
{ name = "python-json-logger" },
|
||||
@@ -2459,7 +2637,7 @@ dependencies = [
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx", extra = ["socks"] },
|
||||
{ name = "mypy" },
|
||||
{ name = "openai" },
|
||||
{ name = "psutil" },
|
||||
@@ -2476,18 +2654,18 @@ requires-dist = [
|
||||
{ name = "aiosqlite", specifier = ">=0.20" },
|
||||
{ name = "alembic", specifier = ">=1.13" },
|
||||
{ name = "cashu", specifier = ">=0.20" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115" },
|
||||
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.141" },
|
||||
{ name = "greenlet", specifier = ">=3.2.1" },
|
||||
{ name = "h11", specifier = ">=0.14" },
|
||||
{ name = "httpx", extras = ["socks"], specifier = ">=0.25.2" },
|
||||
{ name = "litellm", specifier = ">=1.55.0" },
|
||||
{ name = "h11", specifier = ">=0.16" },
|
||||
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1" },
|
||||
{ name = "litellm", specifier = ">=1.93.0,<1.94" },
|
||||
{ name = "marshmallow", specifier = ">=3.13,<4.0" },
|
||||
{ name = "mdurl", specifier = "==0.1.2" },
|
||||
{ name = "nostr", specifier = ">=0.0.2" },
|
||||
{ name = "nostr-sdk", specifier = ">=0.45.1,<0.46" },
|
||||
{ name = "openai", specifier = ">=1.98.0" },
|
||||
{ name = "pillow", specifier = ">=10" },
|
||||
{ name = "python-json-logger", specifier = ">=2.0.0" },
|
||||
{ name = "sqlmodel", specifier = ">=0.0.24" },
|
||||
{ name = "sqlmodel", specifier = ">=0.0.42" },
|
||||
{ name = "websockets", specifier = ">=12.0" },
|
||||
]
|
||||
|
||||
@@ -2639,33 +2817,13 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/41/bb668a6e4192303542d2d90c3b38d564af3c17c61bd7d4039af4f29405fe/secp256k1-0.14.0.tar.gz", hash = "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397", size = 2420607, upload-time = "2021-11-06T01:36:10.707Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/12/4c9815a819816587df70aa38fe7d09b54724a0b1b9b8e8ea2af1c205f2a5/secp256k1-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:539d1d9750299ec4e8df6211978ba78779f5095c7ef19985313f03d1d1b816bd", size = 1298105, upload-time = "2026-01-29T16:26:28.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/86/f01ee0f4c44e12933c460f2b868a3888b93a7c7f4e9fc9be173401b55e8d/secp256k1-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d597a59e3918b0e41181a1c872851ac2e6137882de7f0487b8c42b25333ada", size = 1498906, upload-time = "2026-01-29T16:26:30.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c8/79f2990b72556c3f416ecfde2116a08afb41e324f51b8bf61268d7b72715/secp256k1-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:393d189b4ada9ab3de0b053f484a3b7e86024f4b8cd36616c05f07dbae3ca180", size = 1494612, upload-time = "2026-01-29T16:26:32.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/e8/8dd140270b4e12a7f5876f1641f996854d700866352875f161f770b69ebb/secp256k1-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e4ec14534c1e8b8991376915ef059b7a3e62366aeda60df50b3932ad6529d26a", size = 1298100, upload-time = "2026-01-29T16:26:33.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/6c/e63892de8d7582ab30602ccc1cf0ecd88a30b1a09424eb847c863fd46d9f/secp256k1-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1041694e429eb465123cb742911d2aad5cbd9e0cf2891aaaf794a887938647d1", size = 1499269, upload-time = "2026-01-29T16:26:35.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/5c/2faa8c523c0204af249890eb51b697e9a19d59d101625149d7b4f482e894/secp256k1-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf03e6d45892172046d4e085d5cc91d13a73a465c0f4c8b5633d823b0ca667e2", size = 1494878, upload-time = "2026-01-29T16:26:37.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/27/702d5683d211644f4d286463d7b1c25aeed26275f7b0e2a5a8dc83e7a598/secp256k1-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d90725a63e8e1d6d1483a135649c30ba949185702d3e5acbc075cdab3a44a37f", size = 1298097, upload-time = "2026-01-29T16:26:39.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/1e/928647ac138fddfb4c5ee8aa4140a5786e51c75e9062b7f8d1a0362565df/secp256k1-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cd60d76d95e2eb977edc6523d1178a496fa1634517b497d4cdc7c9aa5e93aa3", size = 1499198, upload-time = "2026-01-29T16:26:41.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/30/c4168076a3cd66ce8ddb28ea127a5f97b088452f1ccb2a3208219fc4f77b/secp256k1-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:245b91f4bfe3a151e3e361f7e7ed634744d35e87c9ac6cf3eb0e4269801d9f7e", size = 1494778, upload-time = "2026-01-29T16:26:43.167Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "75.9.1"
|
||||
version = "84.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/df/ec5ad16b0ec305081c372bd0550fd638fa96e472cd5a03049c344076ea76/setuptools-75.9.1.tar.gz", hash = "sha256:b6eca2c3070cdc82f71b4cb4bb2946bc0760a210d11362278cf1ff394e6ea32c", size = 1345088, upload-time = "2025-03-09T03:18:44.129Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/28/19ad82a0549d73ec6feffa6711eacf9246035a9426b8a8b528440c9959d2/setuptools-75.9.1-py3-none-any.whl", hash = "sha256:0a6f876d62f4d978ca1a11ab4daf728d1357731f978543ff18ecdbf9fd071f73", size = 1231632, upload-time = "2025-03-09T03:18:42.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2722,6 +2880,7 @@ version = "2.0.42"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/03/a0af991e3a43174d6b83fca4fb399745abceddd1171bdabae48ce877ff47/sqlalchemy-2.0.42.tar.gz", hash = "sha256:160bedd8a5c28765bd5be4dec2d881e109e33b34922e50a3b881a7681773ac5f", size = 9749972, upload-time = "2025-07-29T12:48:09.323Z" }
|
||||
@@ -2760,27 +2919,29 @@ asyncio = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlmodel"
|
||||
version = "0.0.24"
|
||||
version = "0.0.42"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780, upload-time = "2025-03-07T05:43:32.887Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/50/27188d8cbccabf9968d74d4095765399dbc4f9fcae95e928fda69aa44a21/sqlmodel-0.0.42.tar.gz", hash = "sha256:9ecec2b6aa4c2aa58da39d2572e7bef4373419dcd6ec52973c07de7be2b1c516", size = 91737, upload-time = "2026-08-28T19:42:01.611Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622, upload-time = "2025-03-07T05:43:30.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/37/d7d6b12b9066005c237de3d52b599298a2693ac423bff0e6b97a90b90bf8/sqlmodel-0.0.42-py3-none-any.whl", hash = "sha256:a732dab1a40e5cbd6ac250cf2b12e8d9b1bf1a47edf27c34d612f359e8875c63", size = 29991, upload-time = "2026-08-28T19:42:00.568Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.46.2"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" }
|
||||
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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2916,17 +3077,17 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.16.0"
|
||||
version = "0.27.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3144,11 +3305,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "wheel"
|
||||
version = "0.41.3"
|
||||
version = "0.48.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/d0/0b4c18a0b85c20233b0c3bc33f792aefd7f12a5832b4da77419949ff6fd9/wheel-0.41.3.tar.gz", hash = "sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841", size = 98880, upload-time = "2023-10-30T10:02:45.125Z" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/20/50ed6bdf27dec98b568a8ae25dc599f35baa3d9709f9e83fd1edb56b9a90/wheel-0.48.0.tar.gz", hash = "sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322", size = 66471, upload-time = "2026-08-11T22:02:27.327Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/7f/4c07234086edbce4a0a446209dc0cb08a19bb206a3ea53b2f56a403f983b/wheel-0.41.3-py3-none-any.whl", hash = "sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942", size = 65801, upload-time = "2023-10-30T10:02:42.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/29/69cfbb602cd91690c55d38ba9fe53e6a7e76a6fa647bf38f19c138d25449/wheel-0.48.0-py3-none-any.whl", hash = "sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab", size = 33320, upload-time = "2026-08-11T22:02:26.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user