From 312a018e0862242206388e727cbcb16cbcd86b67 Mon Sep 17 00:00:00 2001 From: Link Dupont Date: Tue, 2 Jun 2026 09:49:15 -0400 Subject: [PATCH 1/7] refactor(gatekeeper): Drop LiteLLM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop LightLLM and replace with custom, in-tree clients. The combination of custom clients maintain support for OpenAI compatible providers with both Responses and Chat Completion endpoints, Anthropic Messages API, and Gemini’s generateContent. Both direct API and GCP/Vertex AI backends are supported for APIs that are provided by Vertex AI. There is a slight regression in that we no longer support OpenRouter’s native API and custom settings. --- .gitlab/ci/eval-gatekeeper.yml | 2 +- docs/config-reference.md | 19 +- docs/guarded-command-execution.md | 29 +- docs/install.md | 3 +- eval/gatekeeper/README.md | 8 +- eval/gatekeeper/standard-evals.sh | 85 +- pyproject.toml | 4 +- src/linux_mcp_server/config.py | 34 +- .../gatekeeper/anthropic_client.py | 86 + .../gatekeeper/check_run_script.py | 125 +- src/linux_mcp_server/gatekeeper/gcp_auth.py | 40 + .../gatekeeper/gemini_client.py | 80 + src/linux_mcp_server/gatekeeper/http_utils.py | 160 ++ src/linux_mcp_server/gatekeeper/llm.py | 60 + .../gatekeeper/openai_client.py | 113 ++ .../gatekeeper/openrouter_client.py | 79 + src/linux_mcp_server/gatekeeper/schema.py | 107 ++ tests/conftest.py | 3 +- tests/gatekeeper/test_anthropic_client.py | 53 + tests/gatekeeper/test_check_run_script.py | 308 ++-- tests/gatekeeper/test_gcp_auth.py | 34 + tests/gatekeeper/test_gemini_client.py | 55 + tests/gatekeeper/test_http_utils.py | 85 + tests/gatekeeper/test_llm.py | 55 + tests/gatekeeper/test_openai_client.py | 122 ++ tests/gatekeeper/test_openrouter_client.py | 117 ++ tests/gatekeeper/test_schema.py | 32 + tests/test_config.py | 43 +- tests/tools/test_run_script.py | 2 +- uv.lock | 1548 +---------------- 30 files changed, 1612 insertions(+), 1879 deletions(-) create mode 100644 src/linux_mcp_server/gatekeeper/anthropic_client.py create mode 100644 src/linux_mcp_server/gatekeeper/gcp_auth.py create mode 100644 src/linux_mcp_server/gatekeeper/gemini_client.py create mode 100644 src/linux_mcp_server/gatekeeper/http_utils.py create mode 100644 src/linux_mcp_server/gatekeeper/llm.py create mode 100644 src/linux_mcp_server/gatekeeper/openai_client.py create mode 100644 src/linux_mcp_server/gatekeeper/openrouter_client.py create mode 100644 src/linux_mcp_server/gatekeeper/schema.py create mode 100644 tests/gatekeeper/test_anthropic_client.py create mode 100644 tests/gatekeeper/test_gcp_auth.py create mode 100644 tests/gatekeeper/test_gemini_client.py create mode 100644 tests/gatekeeper/test_http_utils.py create mode 100644 tests/gatekeeper/test_llm.py create mode 100644 tests/gatekeeper/test_openai_client.py create mode 100644 tests/gatekeeper/test_openrouter_client.py create mode 100644 tests/gatekeeper/test_schema.py diff --git a/.gitlab/ci/eval-gatekeeper.yml b/.gitlab/ci/eval-gatekeeper.yml index 468495ad..f0a17cde 100644 --- a/.gitlab/ci/eval-gatekeeper.yml +++ b/.gitlab/ci/eval-gatekeeper.yml @@ -53,7 +53,7 @@ download-secure-files: before_script: # Add path of UV executable to PATH & Install dependencies - export PATH="$CI_PROJECT_DIR/.local/bin:$PATH" - - uv sync --locked + - uv sync --locked --extra gcp # Download Red Hat CA certifacate and combine it with root CA - export SSL_CERT_FILE="$CI_PROJECT_DIR/cert.pem" diff --git a/docs/config-reference.md b/docs/config-reference.md index d98f1b6b..f5a285ca 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -58,13 +58,18 @@ These are used when `LINUX_MCP_TOOLSET` is set to `run_script` or `both`. | Option / Env Var | Default | Description | | ---------------- | ------- | ----------- | | `--always-confirm-scripts` / `--no-always-confirm-scripts`
`LINUX_MCP_ALWAYS_CONFIRM_SCRIPTS` | `False` | All scripts must be confirmed by the user | -| `--gatekeeper.model`
`LINUX_MCP_GATEKEEPER__MODEL` | _(none)_ | Required: [LiteLLM model name](https://docs.litellm.ai/docs/providers) to use | -| `--gatekeeper.quantization`
`LINUX_MCP_GATEKEEPER__QUANTIZATION` | _(model specific)_ | _Not usually needed_ - Particular model quantization to use (openrouter only) | -| `--gatekeeper.reasoning_effort`
`LINUX_MCP_GATEKEEPER__REASONING_EFFORT` | _(model specific)_ | Reasoning effort to use for gatekeeper model (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`). Not all values are supported for all models. | -| `--gatekeeper.structured_output`
`LINUX_MCP_GATEKEEPER__STRUCTURED_OUTPUT` | _(autodetected)_ | _Not usually needed_ - Whether to use structured output generation for the model. Default is to use if detected as available. | -| `--gatekeeper.temperature`
`LINUX_MCP_GATEKEEPER__TEMPERATURE` | 0.0 | _Not usually needed_ - Temperature to use for model - for some models, a non-zero value may be necessary when enabling reasoning. | -| `--gatekeeper.template_kwargs`
`LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS` | _(none)_ | _Not usually needed_ - Extra arguments for the model's chat template, formatted as a JSON string. Example: `{ "enable_thinking": false }` | -| Other environment variables | _(none)_ | As required by the LiteLLM provider, e.g. `OPENAI_API_KEY` | +| `--gatekeeper.provider`
`LINUX_MCP_GATEKEEPER__PROVIDER` | `openai` (inferred from model if unset) | LLM provider: `openai`, `anthropic`, `gemini`, or `openrouter` | +| `--gatekeeper.backend`
`LINUX_MCP_GATEKEEPER__BACKEND` | `direct` | API backend: `direct` or `vertex` (GCP/Vertex AI) | +| `--gatekeeper.model`
`LINUX_MCP_GATEKEEPER__MODEL` | _(none)_ | Required: provider-native model ID (e.g. `gpt-5.4`, `claude-sonnet-4-6`, `gemini-2.0-flash`, `openai/gpt-oss-120b` for OpenRouter) | +| `--gatekeeper.quantization`
`LINUX_MCP_GATEKEEPER__QUANTIZATION` | _(none)_ | OpenRouter only: filter providers by quantization level (e.g. `fp4`, `bf16`) | +| `--gatekeeper.base_url`
`LINUX_MCP_GATEKEEPER__BASE_URL` / `OPENAI_API_BASE` | `https://api.openai.com/v1` | OpenAI-compatible API base URL (OpenAI provider only) | +| `--gatekeeper.project`
`LINUX_MCP_GATEKEEPER__PROJECT` / `VERTEXAI_PROJECT` | _(none)_ | GCP project for Vertex backends | +| `--gatekeeper.location`
`LINUX_MCP_GATEKEEPER__LOCATION` / `VERTEXAI_LOCATION` | `global` | GCP region for Vertex backends | +| `--gatekeeper.reasoning_effort`
`LINUX_MCP_GATEKEEPER__REASONING_EFFORT` | _(model specific)_ | Reasoning effort (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`). Not all values are supported for all models. | +| `--gatekeeper.structured_output`
`LINUX_MCP_GATEKEEPER__STRUCTURED_OUTPUT` | `True` | Whether to use structured JSON output from the model | +| `--gatekeeper.temperature`
`LINUX_MCP_GATEKEEPER__TEMPERATURE` | 0.0 | Temperature to use for the model | +| `--gatekeeper.template_kwargs`
`LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS` | _(none)_ | _Not usually needed_ - Extra chat-template arguments for OpenAI-compatible servers (e.g. llama.cpp `enable_thinking`), sent as `chat_template_kwargs` on Chat Completions requests. JSON object, e.g. `{ "enable_thinking": false }` | +| Provider credentials | _(none)_ | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY` / `GEMINI_API_KEY`, or `OPENROUTER_API_KEY` for direct backends; `GOOGLE_APPLICATION_CREDENTIALS` for Vertex backends | ## Logging Configuration diff --git a/docs/guarded-command-execution.md b/docs/guarded-command-execution.md index c60544ad..61d2345d 100644 --- a/docs/guarded-command-execution.md +++ b/docs/guarded-command-execution.md @@ -106,17 +106,36 @@ LINUX_MCP_TOOLSET=run_script **Configure a Gatekeeper Model** -Set `LINUX_MCP_GATEKEEPER__MODEL` to the name of the model you want to use. Additional environment -variables may be needed to configure credentials. See the -[LiteLLM documentation](https://docs.litellm.ai/docs/providers) for details on how to configure your provider. +Set `LINUX_MCP_GATEKEEPER__PROVIDER` and `LINUX_MCP_GATEKEEPER__MODEL` to configure the gatekeeper. +Set the matching API credentials for your provider (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, +`GOOGLE_API_KEY`, or `OPENROUTER_API_KEY`). For Vertex AI backends, install the `gcp` extra and configure +`GOOGLE_APPLICATION_CREDENTIALS`, `VERTEXAI_PROJECT`, and `VERTEXAI_LOCATION`. -Example: +Example (OpenAI): ```sh -LINUX_MCP_GATEKEEPER__MODEL=openai/chatgpt-5.2 +LINUX_MCP_GATEKEEPER__PROVIDER=openai +LINUX_MCP_GATEKEEPER__MODEL=gpt-5.4 OPENAI_API_KEY=<....> ``` +Example (Anthropic): + +```sh +LINUX_MCP_GATEKEEPER__PROVIDER=anthropic +LINUX_MCP_GATEKEEPER__MODEL=claude-sonnet-4-6 +ANTHROPIC_API_KEY=<....> +``` + +Example (OpenRouter): + +```sh +LINUX_MCP_GATEKEEPER__PROVIDER=openrouter +LINUX_MCP_GATEKEEPER__MODEL=openai/gpt-oss-120b +LINUX_MCP_GATEKEEPER__QUANTIZATION=fp4 +OPENROUTER_API_KEY=<....> +``` + **Configure your client's tool policy** diff --git a/docs/install.md b/docs/install.md index d4e9f87b..f9bcb2bd 100644 --- a/docs/install.md +++ b/docs/install.md @@ -36,7 +36,8 @@ Get the Linux MCP Server running quickly with your favorite MCP client. !!! note "Optional dependencies" The `gssapi` package is needed for SSH authentication to Kerberos-registered systems. Install with `uv tool install linux-mcp-server[gssapi]`. - The `gcp` package is needed for Google Cloud Platform integration with the gatekeeper model. Install with `uv tool install linux-mcp-server[gcp]`. + The `gcp` optional dependency provides Google Cloud authentication for Vertex AI gatekeeper backends. + Install with `uv tool install linux-mcp-server[gcp]`. --- diff --git a/eval/gatekeeper/README.md b/eval/gatekeeper/README.md index 616d7676..fa5c0ddd 100644 --- a/eval/gatekeeper/README.md +++ b/eval/gatekeeper/README.md @@ -64,11 +64,17 @@ Runs test cases through the gatekeeper and reports results. ```bash # Set the gatekeeper model -export LINUX_MCP_GATEKEEPER__MODEL="openrouter/anthropic/claude-3.5-sonnet" +export LINUX_MCP_GATEKEEPER__PROVIDER="anthropic" +export LINUX_MCP_GATEKEEPER__MODEL="claude-sonnet-4-6" +export ANTHROPIC_API_KEY="..." # Run evaluation on a single file uv run eval/gatekeeper/run-eval.py testcases/selinux-port-denial.yaml -o results.yaml +# Run via standard-evals.sh (OpenRouter example) +export OPENROUTER_API_KEY="..." +./eval/gatekeeper/standard-evals.sh --no-save gpt-oss-120b:low,fp4@openrouter + # Run all test case files in testcases/ uv run eval/gatekeeper/run-eval.py --all -o results.yaml diff --git a/eval/gatekeeper/standard-evals.sh b/eval/gatekeeper/standard-evals.sh index 94114a2c..89d2fd90 100755 --- a/eval/gatekeeper/standard-evals.sh +++ b/eval/gatekeeper/standard-evals.sh @@ -2,9 +2,8 @@ # # Run the gatekeeper eval suite against a specific model/provider combination. # -# Configures provider-specific authentication, model routing (via LiteLLM -# model strings), reasoning effort, and cost overrides, then delegates to -# run-eval.py. +# Configures provider-specific authentication, gatekeeper env vars, reasoning +# effort, and cost overrides, then delegates to run-eval.py. # # Usage: # standard-evals.sh [--no-save] [--variant=NAME] [:,][@PROVIDER] @@ -237,7 +236,6 @@ LINUX_MCP_GATEKEEPER__COST="" case "$model" in claude-*) if [[ $reasoning == "none" ]] ; then - # LiteLLM doesn't support reasoning_effort=none for Anthropic models LINUX_MCP_GATEKEEPER__REASONING_EFFORT= else echo "Setting T=1, as required for thinking with Claude models" @@ -264,13 +262,16 @@ esac case "$provider" in anthropic) : "${ANTHROPIC_API_KEY:?'api key must be set'}" - LINUX_MCP_GATEKEEPER__MODEL="anthropic/$model" + LINUX_MCP_GATEKEEPER__PROVIDER=anthropic + LINUX_MCP_GATEKEEPER__BACKEND=direct + LINUX_MCP_GATEKEEPER__MODEL="$model" ;; llama_cpp) OPENAI_API_KEY=tasty - OPENAI_API_BASE=http://localhost:8080/v1 - # gemma-4 and qwen3.5 support a boolean enable_thinking tenplate parameter - # granite-4 doesn't have any explict reasoning control + LINUX_MCP_GATEKEEPER__PROVIDER=openai + LINUX_MCP_GATEKEEPER__BACKEND=direct + LINUX_MCP_GATEKEEPER__BASE_URL=http://localhost:8080/v1 + # gemma-4 and qwen3.5 support enable_thinking via chat_template_kwargs; granite-4 has no control case "$reasoning" in none) LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS='{ "enable_thinking": false }' @@ -280,28 +281,28 @@ case "$provider" in ;; esac export OPENAI_API_KEY - export OPENAI_API_BASE + export LINUX_MCP_GATEKEEPER__BASE_URL max_parallel=1 quant_tag="${quantization^^}" case "$model" in gemma-4-26b-a4b-it) - LINUX_MCP_GATEKEEPER__MODEL=openai/google/gemma-4-26b-a4b + LINUX_MCP_GATEKEEPER__MODEL=google/gemma-4-26b-a4b hf_repo=ggml-org/gemma-4-26B-A4B-it-GGUF ;; granite-4.0-h-small) - LINUX_MCP_GATEKEEPER__MODEL=openai/ibm-granite/granite-4.0-h-small + LINUX_MCP_GATEKEEPER__MODEL=ibm-granite/granite-4.0-h-small hf_repo=ibm-granite/granite-4.0-h-small-GGUF ;; granite-4.1-8b) - LINUX_MCP_GATEKEEPER__MODEL=openai/ibm-granite/granite-4.1-8b + LINUX_MCP_GATEKEEPER__MODEL=ibm-granite/granite-4.1-8b hf_repo=ibm-granite/granite-4.1-8b-GGUF ;; qwen3.5-35b-a3b) - LINUX_MCP_GATEKEEPER__MODEL=openai/qwen/qwen3.5-35b-a3b + LINUX_MCP_GATEKEEPER__MODEL=qwen/qwen3.5-35b-a3b hf_repo=unsloth/Qwen3.5-35B-A3B-GGUF ;; qwen3.5-9b) - LINUX_MCP_GATEKEEPER__MODEL=openai/qwen/qwen3.5-9b + LINUX_MCP_GATEKEEPER__MODEL=qwen/qwen3.5-9b hf_repo=unsloth/Qwen3.5-9B-GGUF ;; *) @@ -316,30 +317,37 @@ case "$provider" in : "${VERTEXAI_PROJECT:?'project must be set'}" uv_args+=("--extra" "gcp") max_parallel=50 + LINUX_MCP_GATEKEEPER__BACKEND=vertex + vertex_location="${VERTEXAI_LOCATION:-global}" + vertex_openapi_base="https://aiplatform.googleapis.com/v1/projects/${VERTEXAI_PROJECT}/locations/${vertex_location}/endpoints/openapi" case $model in claude-*) - LINUX_MCP_GATEKEEPER__MODEL="vertex_ai/$model" + LINUX_MCP_GATEKEEPER__PROVIDER=anthropic + LINUX_MCP_GATEKEEPER__MODEL="$model" ;; gemini-*) - LINUX_MCP_GATEKEEPER__MODEL="vertex_ai/$model" + LINUX_MCP_GATEKEEPER__PROVIDER=gemini + LINUX_MCP_GATEKEEPER__MODEL="$model" ;; gemma-4-26b-a4b-it) if [[ $reasoning == "none" ]] ; then - # LiteLLM doesn't know about this model, so doesn't support reasoning_effort LINUX_MCP_GATEKEEPER__REASONING_EFFORT= fi - LINUX_MCP_GATEKEEPER__MODEL=openai/google/$model-maas + LINUX_MCP_GATEKEEPER__PROVIDER=openai + LINUX_MCP_GATEKEEPER__MODEL="${model}-maas" + LINUX_MCP_GATEKEEPER__BASE_URL="${vertex_openapi_base}" LINUX_MCP_GATEKEEPER__COST=0.15e-6:0.60e-6 - OPENAI_API_BASE=https://aiplatform.googleapis.com/v1/projects/${VERTEXAI_PROJECT}/locations/global/endpoints/openapi - OPENAI_API_KEY=$(gcloud auth print-access-token) - export OPENAI_API_BASE OPENAI_API_KEY ;; gpt-oss-20b) - LINUX_MCP_GATEKEEPER__MODEL="vertex_ai/openai/$model-maas" + LINUX_MCP_GATEKEEPER__PROVIDER=openai + LINUX_MCP_GATEKEEPER__MODEL="${model}-maas" + LINUX_MCP_GATEKEEPER__BASE_URL="${vertex_openapi_base}" LINUX_MCP_GATEKEEPER__COST="0.07e-6:0.25e-6" ;; gpt-oss-120b) - LINUX_MCP_GATEKEEPER__MODEL="vertex_ai/openai/$model-maas" + LINUX_MCP_GATEKEEPER__PROVIDER=openai + LINUX_MCP_GATEKEEPER__MODEL="${model}-maas" + LINUX_MCP_GATEKEEPER__BASE_URL="${vertex_openapi_base}" LINUX_MCP_GATEKEEPER__COST="0.09e-6:0.36e-6" ;; esac @@ -348,49 +356,54 @@ case "$provider" in : "${OPENAI_API_KEY:?'api key must be set'}" SSL_CERT_FILE=${SSL_CERT_FILE:-/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem} export SSL_CERT_FILE + LINUX_MCP_GATEKEEPER__PROVIDER=openai + LINUX_MCP_GATEKEEPER__BACKEND=direct case $model in granite-*) - LINUX_MCP_GATEKEEPER__MODEL="openai/ibm-granite/$model" + LINUX_MCP_GATEKEEPER__MODEL="ibm-granite/$model" ;; gpt-oss*) - LINUX_MCP_GATEKEEPER__MODEL="openai/openai/$model" - echo "$LINUX_MCP_GATEKEEPER__MODEL" + LINUX_MCP_GATEKEEPER__MODEL="openai/$model" ;; esac - OPENAI_API_BASE="$(get_MC_base_url "$model")" - export OPENAI_API_BASE + LINUX_MCP_GATEKEEPER__BASE_URL="$(get_MC_base_url "$model")" max_parallel=5 ;; openrouter) : "${OPENROUTER_API_KEY:?'api key must be set'}" + LINUX_MCP_GATEKEEPER__PROVIDER=openrouter + LINUX_MCP_GATEKEEPER__BACKEND=direct if [[ -n $quantization ]] ; then LINUX_MCP_GATEKEEPER__QUANTIZATION=$quantization - export LINUX_MCP_GATEKEEPER__QUANTIZATION fi case $model in claude-*) - LINUX_MCP_GATEKEEPER__MODEL="openrouter/anthropic/$model" + LINUX_MCP_GATEKEEPER__MODEL="anthropic/$model" ;; gemma-*) - LINUX_MCP_GATEKEEPER__MODEL="openrouter/google/$model" + LINUX_MCP_GATEKEEPER__MODEL="google/$model" ;; gpt-oss-*) - LINUX_MCP_GATEKEEPER__MODEL="openrouter/openai/$model" + LINUX_MCP_GATEKEEPER__MODEL="openai/$model" ;; gpt-*) - LINUX_MCP_GATEKEEPER__MODEL="openrouter/openai/$model" + LINUX_MCP_GATEKEEPER__MODEL="openai/$model" ;; granite-*) - LINUX_MCP_GATEKEEPER__MODEL="openrouter/ibm-granite/$model" + LINUX_MCP_GATEKEEPER__MODEL="ibm-granite/$model" ;; qwen*) - LINUX_MCP_GATEKEEPER__MODEL="openrouter/qwen/$model" + LINUX_MCP_GATEKEEPER__MODEL="qwen/$model" ;; esac ;; esac -export LINUX_MCP_GATEKEEPER__MODEL LINUX_MCP_GATEKEEPER__COST LINUX_MCP_GATEKEEPER__REASONING_EFFORT +export LINUX_MCP_GATEKEEPER__PROVIDER LINUX_MCP_GATEKEEPER__BACKEND LINUX_MCP_GATEKEEPER__MODEL +export LINUX_MCP_GATEKEEPER__COST LINUX_MCP_GATEKEEPER__REASONING_EFFORT +[[ -n "${LINUX_MCP_GATEKEEPER__QUANTIZATION:-}" ]] && export LINUX_MCP_GATEKEEPER__QUANTIZATION +[[ -n "${LINUX_MCP_GATEKEEPER__BASE_URL:-}" ]] && export LINUX_MCP_GATEKEEPER__BASE_URL +[[ -n "${LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS:-}" ]] && export LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS variant_suffix="" if [[ -n "$variant" ]] ; then diff --git a/pyproject.toml b/pyproject.toml index 8ca6db99..4b6876dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ requires-python = ">=3.10" dependencies = [ "asyncssh[bcrypt] >= 2.22.0", "fastmcp >= 3.2.4", - "litellm>=1.80.16", + "requests>=2.32.0", "pydantic-settings >= 2.12.0", "pydantic >= 2.12.5", # pydocket is incompatible with newer versions of fakeredis. @@ -48,7 +48,7 @@ classifiers = [ [project.optional-dependencies] gssapi = ["gssapi >=1.11.1"] -gcp = ["google-cloud-aiplatform>=1.147.0"] +gcp = ["google-auth>=2.40.0"] [project.urls] "Source code" = "https://github.com/rhel-lightspeed/linux-mcp-server" diff --git a/src/linux_mcp_server/config.py b/src/linux_mcp_server/config.py index 1d776022..eca506f2 100644 --- a/src/linux_mcp_server/config.py +++ b/src/linux_mcp_server/config.py @@ -48,6 +48,22 @@ class ReasoningEffort(StrEnum): DEFAULT = "default" +class GatekeeperProvider(StrEnum): + """LLM provider for the gatekeeper model.""" + + OPENAI = "openai" + ANTHROPIC = "anthropic" + GEMINI = "gemini" + OPENROUTER = "openrouter" + + +class GatekeeperBackend(StrEnum): + """API backend for the gatekeeper provider.""" + + DIRECT = "direct" + VERTEX = "vertex" + + class AuthProvider(StrEnum): """Authentication provider types.""" @@ -116,18 +132,28 @@ def parse_cost(v: Any) -> Any: class GatekeeperConfig(BaseSettings): """Gatekeeper Model configuration""" + provider: GatekeeperProvider | None = None + backend: GatekeeperBackend = GatekeeperBackend.DIRECT model: str | None = None - # model quantization (e.g. fp8, bf16 - only supported for openrouter) + # Model quantization for OpenRouter provider routing (e.g. fp8, bf16) quantization: str | None = None + # OpenAI-compatible API base URL (OpenAI provider only) + base_url: str | None = None + + # GCP project and region for Vertex backends + project: str | None = None + location: str | None = None + # reasoning effort reasoning_effort: ReasoningEffort | None = None - # Whether we should use structured output (default, autodetect support) - structured_output: bool | None = None + # Whether we should use structured output + structured_output: bool = True - # dict of extra template keyword arguments + # Extra chat-template arguments for OpenAI-compatible servers (e.g. llama.cpp enable_thinking). + # Passed as chat_template_kwargs on Chat Completions requests. template_kwargs: dict[str, Any] = Field(default_factory=dict) # Temperature for gatekeeper model diff --git a/src/linux_mcp_server/gatekeeper/anthropic_client.py b/src/linux_mcp_server/gatekeeper/anthropic_client.py new file mode 100644 index 00000000..9d28fafe --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/anthropic_client.py @@ -0,0 +1,86 @@ +"""Anthropic Messages API client for the gatekeeper.""" + +from typing import Any + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperBackend +from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_access_token +from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_location +from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project +from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_API_URL +from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_API_VERSION +from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_DEFAULT_MAX_TOKENS +from linux_mcp_server.gatekeeper.http_utils import anthropic_thinking_block +from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_VERTEX_VERSION +from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS +from linux_mcp_server.gatekeeper.http_utils import get_anthropic_api_key +from linux_mcp_server.gatekeeper.http_utils import normalize_model_id +from linux_mcp_server.gatekeeper.http_utils import post_json +from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion +from linux_mcp_server.gatekeeper.schema import anthropic_output_config + + +def _build_messages_body(prompt: str, *, include_model: bool) -> dict[str, Any]: + body: dict[str, Any] = { + "max_tokens": ANTHROPIC_DEFAULT_MAX_TOKENS, + "messages": [{"role": "user", "content": prompt}], + "temperature": CONFIG.gatekeeper.temperature, + } + if include_model: + body["model"] = normalize_model_id(CONFIG.gatekeeper.model or "") + if CONFIG.gatekeeper.structured_output: + body["output_config"] = anthropic_output_config() + thinking = anthropic_thinking_block(CONFIG.gatekeeper.reasoning_effort) + if thinking is not None: + body["thinking"] = thinking + return body + + +def _vertex_url(model: str) -> str: + project = get_gcp_project() + location = get_gcp_location() + host = "aiplatform.googleapis.com" if location == "global" else f"{location}-aiplatform.googleapis.com" + return f"https://{host}/v1/projects/{project}/locations/{location}/publishers/anthropic/models/{model}:rawPredict" + + +def _extract_messages_text(response: dict[str, Any]) -> str: + for item in response.get("content", []): + if isinstance(item, dict) and item.get("type") == "text": + text = item.get("text") + if isinstance(text, str): + return text.strip() + return "" + + +def complete_anthropic(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: + model = normalize_model_id(CONFIG.gatekeeper.model or "") + + if CONFIG.gatekeeper.backend == GatekeeperBackend.VERTEX: + body = _build_messages_body(prompt, include_model=False) + body["anthropic_version"] = ANTHROPIC_VERTEX_VERSION + headers = { + "Authorization": f"Bearer {get_gcp_access_token()}", + "Content-Type": "application/json", + } + response = post_json( + provider="anthropic", + url=_vertex_url(model), + headers=headers, + body=body, + timeout=timeout, + ) + return GatekeeperCompletion(text=_extract_messages_text(response)) + + headers = { + "x-api-key": get_anthropic_api_key(), + "anthropic-version": ANTHROPIC_API_VERSION, + "Content-Type": "application/json", + } + response = post_json( + provider="anthropic", + url=ANTHROPIC_API_URL, + headers=headers, + body=_build_messages_body(prompt, include_model=True), + timeout=timeout, + ) + return GatekeeperCompletion(text=_extract_messages_text(response)) diff --git a/src/linux_mcp_server/gatekeeper/check_run_script.py b/src/linux_mcp_server/gatekeeper/check_run_script.py index 9362fc50..d0582d22 100644 --- a/src/linux_mcp_server/gatekeeper/check_run_script.py +++ b/src/linux_mcp_server/gatekeeper/check_run_script.py @@ -1,40 +1,22 @@ +import asyncio import logging import time -from typing import Any - -import litellm - -from litellm import acompletion -from litellm import Choices -from litellm import completion_cost -from litellm import get_supported_openai_params -from litellm import ModelResponse -from litellm import Usage -from litellm.exceptions import Timeout from pydantic import BaseModel from pydantic import ValidationError from linux_mcp_server.config import CONFIG -from linux_mcp_server.config import ReasoningEffort +from linux_mcp_server.gatekeeper.llm import complete_gatekeeper from linux_mcp_server.utils import StrEnum -# LiteLLM has bugs where the custom_llm_provider is not properly -# passed back into query functions for things like "model supports reasoning". -# Without this, each such failure spits out a debug message to the terminal -# https://github.com/BerriAI/litellm/issues/23879 -litellm.suppress_debug_info = True - - logger = logging.getLogger("linux-mcp-server") def get_model() -> str: if CONFIG.gatekeeper.model: return CONFIG.gatekeeper.model - else: - raise ValueError("To use run_script tools, you must set LINUX_MCP_GATEKEEPER__MODEL") + raise ValueError("To use run_script tools, you must set LINUX_MCP_GATEKEEPER__MODEL") READONLY_INSTRUCTION = """ @@ -121,8 +103,6 @@ def get_model() -> str: """ -# Maximum number of completion tokens (including reasoning) -GATEKEEPER_MAX_TOKENS = 8000 # Timeout (s) GATEKEEPER_TIMEOUT = 120 @@ -198,42 +178,6 @@ def parse_from_description(cls, description: str) -> "GatekeeperResult": return cls(status=status, detail=detail) -def _build_completion_kwargs(): - extra_kwargs: dict[str, Any] = {} - model = get_model() - - structured_output = CONFIG.gatekeeper.structured_output - if structured_output is None: - params = get_supported_openai_params(model=model) - structured_output = params is not None and "response_format" in params - - if structured_output: - extra_kwargs["response_format"] = GatekeeperResult - - reasoning_effort = CONFIG.gatekeeper.reasoning_effort - if reasoning_effort is not None: - if model.startswith("openrouter/"): - if reasoning_effort == ReasoningEffort.NONE: - extra_kwargs["reasoning"] = {"enabled": False} - else: - extra_kwargs["reasoning"] = {"enabled": True, "effort": reasoning_effort.value} - else: - extra_kwargs["reasoning_effort"] = reasoning_effort.value - - if model.startswith("openrouter/"): - provider: dict[str, Any] = { - "require_parameters": True, - } - extra_kwargs["provider"] = provider - if CONFIG.gatekeeper.quantization: - provider["quantizations"] = [CONFIG.gatekeeper.quantization] - - if CONFIG.gatekeeper.template_kwargs: - extra_kwargs["chat_template_kwargs"] = CONFIG.gatekeeper.template_kwargs - - return extra_kwargs - - class GatekeeperStats(BaseModel): prompt_tokens: int = 0 completion_tokens: int = 0 @@ -247,24 +191,13 @@ def __init__(self, message: str, *, stats: GatekeeperStats | None = None): self.stats = stats -def _get_cost(response: ModelResponse) -> float: - usage: Usage = response.usage # pyright: ignore[reportAttributeAccessIssue] - - cost: float | None = usage.get("cost") - if cost: - return cost - - if CONFIG.gatekeeper.cost is not None: - custom_cost_per_token = dict( - input_cost_per_token=CONFIG.gatekeeper.cost[0], output_cost_per_token=CONFIG.gatekeeper.cost[1] - ) - else: - custom_cost_per_token = None - - try: - return completion_cost(response, custom_cost_per_token=custom_cost_per_token) - except Exception: +def _compute_cost(prompt_tokens: int, completion_tokens: int, *, usage_cost: float | None = None) -> float: + if usage_cost is not None: + return usage_cost + if CONFIG.gatekeeper.cost is None: return 0.0 + input_cost, output_cost = CONFIG.gatekeeper.cost + return prompt_tokens * input_cost + completion_tokens * output_cost async def check_run_script_with_stats( @@ -290,44 +223,30 @@ async def check_run_script_with_stats( readonly_result=READONLY_RESULT if readonly else "", ) - messages = [{"role": "user", "content": prompt}] - - extra_kwargs = _build_completion_kwargs() - time_before = time.perf_counter() try: - response = await acompletion( - model=get_model(), - messages=messages, - temperature=CONFIG.gatekeeper.temperature, - max_tokens=GATEKEEPER_MAX_TOKENS, + completion = await asyncio.wait_for( + asyncio.to_thread(complete_gatekeeper, prompt), timeout=GATEKEEPER_TIMEOUT, - **extra_kwargs, ) - except Timeout: - raise GatekeeperException("Timeout calling gatekeeper model") - assert isinstance(response, ModelResponse) + except asyncio.TimeoutError: + raise GatekeeperException("Timeout calling gatekeeper model") from None - usage: Usage = response.usage # pyright: ignore[reportAttributeAccessIssue] stats = GatekeeperStats( - prompt_tokens=usage.prompt_tokens, - completion_tokens=usage.completion_tokens, - cost=_get_cost(response), + prompt_tokens=completion.prompt_tokens, + completion_tokens=completion.completion_tokens, + cost=_compute_cost( + completion.prompt_tokens, + completion.completion_tokens, + usage_cost=completion.usage_cost, + ), latency=time.perf_counter() - time_before, ) - assert isinstance(response.choices[0], Choices) - if response.choices[0].finish_reason == "length": - raise GatekeeperException("Gatekeeper model output limit reached", stats=stats) - - response_text = (response.choices[0].message.content or "").strip() - - logger.info(f"Gatekeeper response: {response_text}") - try: - return GatekeeperResult.model_validate_json(response_text), stats + return GatekeeperResult.model_validate_json(completion.text), stats except ValidationError as e: - logger.warning("Failed to Failed to parse gatekeeper model output: %s", e) + logger.warning("Failed to parse gatekeeper model output: %s", e) raise GatekeeperException("Failed to parse gatekeeper model output", stats=stats) from e diff --git a/src/linux_mcp_server/gatekeeper/gcp_auth.py b/src/linux_mcp_server/gatekeeper/gcp_auth.py new file mode 100644 index 00000000..a7657f36 --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/gcp_auth.py @@ -0,0 +1,40 @@ +"""Google Cloud authentication helpers for Vertex AI backends.""" + +import os + +from linux_mcp_server.config import CONFIG + + +class GCPAuthError(RuntimeError): + """Raised when GCP credentials cannot be obtained.""" + + +def get_gcp_project() -> str: + project = CONFIG.gatekeeper.project or os.environ.get("VERTEXAI_PROJECT") or os.environ.get("GOOGLE_CLOUD_PROJECT") + if not project: + raise GCPAuthError( + "Vertex backend requires a GCP project. Set VERTEXAI_PROJECT, GOOGLE_CLOUD_PROJECT, " + "or LINUX_MCP_GATEKEEPER__PROJECT." + ) + return project + + +def get_gcp_location() -> str: + return CONFIG.gatekeeper.location or os.environ.get("VERTEXAI_LOCATION") or "global" + + +def get_gcp_access_token() -> str: + try: + import google.auth # pyright: ignore[reportMissingImports] + import google.auth.transport.requests # pyright: ignore[reportMissingImports] + except ImportError as exc: + raise GCPAuthError( + "Vertex backend requires the gcp optional dependency. Install with: uv tool install linux-mcp-server[gcp]" + ) from exc + + credentials, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) + request = google.auth.transport.requests.Request() + credentials.refresh(request) + if not credentials.token: + raise GCPAuthError("Failed to obtain GCP access token from application default credentials.") + return credentials.token diff --git a/src/linux_mcp_server/gatekeeper/gemini_client.py b/src/linux_mcp_server/gatekeeper/gemini_client.py new file mode 100644 index 00000000..47fe5274 --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/gemini_client.py @@ -0,0 +1,80 @@ +"""Gemini generateContent client for the gatekeeper.""" + +from typing import Any + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperBackend +from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_access_token +from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_location +from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project +from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS +from linux_mcp_server.gatekeeper.http_utils import gemini_thinking_level +from linux_mcp_server.gatekeeper.http_utils import get_google_api_key +from linux_mcp_server.gatekeeper.http_utils import GOOGLE_AI_BASE_URL +from linux_mcp_server.gatekeeper.http_utils import normalize_model_id +from linux_mcp_server.gatekeeper.http_utils import post_json +from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion +from linux_mcp_server.gatekeeper.schema import gemini_generation_config + + +def _build_body(prompt: str) -> dict[str, Any]: + generation_config = gemini_generation_config( + temperature=CONFIG.gatekeeper.temperature, + structured_output=CONFIG.gatekeeper.structured_output, + ) + thinking_level = gemini_thinking_level(CONFIG.gatekeeper.reasoning_effort) + if thinking_level is not None: + generation_config["thinkingConfig"] = {"thinkingLevel": thinking_level} + return { + "contents": [{"role": "user", "parts": [{"text": prompt}]}], + "generationConfig": generation_config, + } + + +def _vertex_url(model: str) -> str: + project = get_gcp_project() + location = get_gcp_location() + host = "aiplatform.googleapis.com" if location == "global" else f"{location}-aiplatform.googleapis.com" + return f"https://{host}/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent" + + +def _extract_text(response: dict[str, Any]) -> str: + candidates = response.get("candidates", []) + if not candidates: + return "" + content = candidates[0].get("content", {}) + parts = content.get("parts", []) + if not parts: + return "" + text = parts[0].get("text") + return text.strip() if isinstance(text, str) else "" + + +def complete_gemini(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: + model = normalize_model_id(CONFIG.gatekeeper.model or "") + body = _build_body(prompt) + + if CONFIG.gatekeeper.backend == GatekeeperBackend.VERTEX: + headers = { + "Authorization": f"Bearer {get_gcp_access_token()}", + "Content-Type": "application/json", + } + response = post_json( + provider="gemini", + url=_vertex_url(model), + headers=headers, + body=body, + timeout=timeout, + ) + return GatekeeperCompletion(text=_extract_text(response)) + + api_key = get_google_api_key() + headers = {"Content-Type": "application/json"} + response = post_json( + provider="gemini", + url=f"{GOOGLE_AI_BASE_URL}/models/{model}:generateContent?key={api_key}", + headers=headers, + body=body, + timeout=timeout, + ) + return GatekeeperCompletion(text=_extract_text(response)) diff --git a/src/linux_mcp_server/gatekeeper/http_utils.py b/src/linux_mcp_server/gatekeeper/http_utils.py new file mode 100644 index 00000000..2763bc3a --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/http_utils.py @@ -0,0 +1,160 @@ +"""Shared helpers for gatekeeper HTTP clients.""" + +import os + +from typing import Any +from urllib.parse import urlparse + +import requests + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperBackend +from linux_mcp_server.config import ReasoningEffort + + +DEFAULT_TIMEOUT_SECONDS = 120 + +OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1" +OPENROUTER_DEFAULT_BASE_URL = "https://openrouter.ai/api/v1" + +ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages" +ANTHROPIC_API_VERSION = "2023-06-01" +ANTHROPIC_VERTEX_VERSION = "vertex-2023-10-16" +ANTHROPIC_DEFAULT_MAX_TOKENS = 4096 + +GOOGLE_AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" + + +class GatekeeperHTTPError(RuntimeError): + """Raised when an LLM provider returns an error response.""" + + def __init__(self, provider: str, status_code: int, body: str): + snippet = body[:500] + ("..." if len(body) > 500 else "") + super().__init__(f"{provider} API error ({status_code}): {snippet}") + self.provider = provider + self.status_code = status_code + self.body = body + + +def post_json( + *, + provider: str, + url: str, + headers: dict[str, str], + body: dict[str, Any], + timeout: int = DEFAULT_TIMEOUT_SECONDS, +) -> dict[str, Any]: + response = requests.post(url, headers=headers, json=body, timeout=timeout) + if not response.ok: + raise GatekeeperHTTPError(provider, response.status_code, response.text) + return response.json() + + +def get_openai_base_url() -> str: + return (CONFIG.gatekeeper.base_url or os.environ.get("OPENAI_API_BASE") or OPENAI_DEFAULT_BASE_URL).rstrip("/") + + +def prefers_openai_chat_completions(base_url: str) -> bool: + """Hosts known to expose only Chat Completions, not the Responses API.""" + path = urlparse(base_url).path or "" + return "/endpoints/openapi" in path + + +def normalize_model_id(model: str) -> str: + for prefix in ("openai/", "anthropic/", "vertex_ai/", "gemini/"): + if model.startswith(prefix): + return model[len(prefix) :] + return model + + +def normalize_openrouter_model_id(model: str) -> str: + if model.startswith("openrouter/"): + return model[len("openrouter/") :] + return model + + +def get_openrouter_base_url() -> str: + return (CONFIG.gatekeeper.base_url or OPENROUTER_DEFAULT_BASE_URL).rstrip("/") + + +def openai_reasoning_block(reasoning_effort: ReasoningEffort | None) -> dict[str, Any] | None: + if reasoning_effort is None or reasoning_effort == ReasoningEffort.DEFAULT: + return None + return {"effort": reasoning_effort.value} + + +def openrouter_reasoning_block(reasoning_effort: ReasoningEffort | None) -> dict[str, Any] | None: + if reasoning_effort is None or reasoning_effort == ReasoningEffort.DEFAULT: + return None + if reasoning_effort == ReasoningEffort.NONE: + return {"enabled": False} + return {"enabled": True, "effort": reasoning_effort.value} + + +def anthropic_thinking_block(reasoning_effort: ReasoningEffort | None) -> dict[str, Any] | None: + if reasoning_effort is None or reasoning_effort in {ReasoningEffort.NONE, ReasoningEffort.DEFAULT}: + return None + budget_by_effort = { + ReasoningEffort.MINIMAL: 1024, + ReasoningEffort.LOW: 4096, + ReasoningEffort.MEDIUM: 8192, + ReasoningEffort.HIGH: 16384, + ReasoningEffort.XHIGH: 32768, + } + budget = budget_by_effort.get(reasoning_effort) + if budget is None: + return None + return {"type": "enabled", "budget_tokens": budget} + + +def gemini_thinking_level(reasoning_effort: ReasoningEffort | None) -> str | None: + if reasoning_effort is None or reasoning_effort in {ReasoningEffort.NONE, ReasoningEffort.DEFAULT}: + return None + mapping = { + ReasoningEffort.MINIMAL: "MINIMAL", + ReasoningEffort.LOW: "LOW", + ReasoningEffort.MEDIUM: "MEDIUM", + ReasoningEffort.HIGH: "HIGH", + ReasoningEffort.XHIGH: "HIGH", + } + return mapping.get(reasoning_effort) + + +def get_openai_api_key() -> str: + api_key = os.environ.get("OPENAI_API_KEY") + if not api_key: + raise ValueError("OPENAI_API_KEY is required for OpenAI gatekeeper provider.") + return api_key + + +def get_openrouter_api_key() -> str: + api_key = os.environ.get("OPENROUTER_API_KEY") + if not api_key: + raise ValueError("OPENROUTER_API_KEY is required for OpenRouter gatekeeper provider.") + return api_key + + +def openrouter_auth_headers() -> dict[str, str]: + return {"Authorization": f"Bearer {get_openrouter_api_key()}"} + + +def get_anthropic_api_key() -> str: + api_key = os.environ.get("ANTHROPIC_API_KEY") + if not api_key: + raise ValueError("ANTHROPIC_API_KEY is required for Anthropic gatekeeper provider.") + return api_key + + +def get_google_api_key() -> str: + api_key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY") + if not api_key: + raise ValueError("GOOGLE_API_KEY or GEMINI_API_KEY is required for Gemini direct backend.") + return api_key + + +def openai_auth_headers() -> dict[str, str]: + if CONFIG.gatekeeper.backend == GatekeeperBackend.VERTEX: + from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_access_token + + return {"Authorization": f"Bearer {get_gcp_access_token()}"} + return {"Authorization": f"Bearer {get_openai_api_key()}"} diff --git a/src/linux_mcp_server/gatekeeper/llm.py b/src/linux_mcp_server/gatekeeper/llm.py new file mode 100644 index 00000000..8c2307be --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/llm.py @@ -0,0 +1,60 @@ +"""Provider routing for gatekeeper LLM calls.""" + +import logging + +from pydantic import BaseModel + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.gatekeeper.anthropic_client import complete_anthropic +from linux_mcp_server.gatekeeper.gemini_client import complete_gemini +from linux_mcp_server.gatekeeper.openai_client import complete_openai +from linux_mcp_server.gatekeeper.openrouter_client import complete_openrouter + + +logger = logging.getLogger("linux-mcp-server") + + +class GatekeeperCompletion(BaseModel): + text: str + prompt_tokens: int = 0 + completion_tokens: int = 0 + usage_cost: float | None = None + + +def _infer_provider_from_model(model: str) -> GatekeeperProvider: + if model.startswith("openrouter/"): + return GatekeeperProvider.OPENROUTER + if model.startswith("anthropic/") or model.startswith("claude"): + return GatekeeperProvider.ANTHROPIC + if model.startswith("vertex_ai/gemini") or model.startswith("gemini"): + return GatekeeperProvider.GEMINI + if model.startswith("vertex_ai/anthropic"): + return GatekeeperProvider.ANTHROPIC + return GatekeeperProvider.OPENAI + + +def resolve_provider() -> GatekeeperProvider: + if CONFIG.gatekeeper.provider is not None: + return CONFIG.gatekeeper.provider + if not CONFIG.gatekeeper.model: + raise ValueError("To use run_script tools, you must set LINUX_MCP_GATEKEEPER__MODEL") + return _infer_provider_from_model(CONFIG.gatekeeper.model) + + +def complete_gatekeeper(prompt: str) -> GatekeeperCompletion: + provider = resolve_provider() + match provider: + case GatekeeperProvider.OPENAI: + completion = complete_openai(prompt) + case GatekeeperProvider.ANTHROPIC: + completion = complete_anthropic(prompt) + case GatekeeperProvider.GEMINI: + completion = complete_gemini(prompt) + case GatekeeperProvider.OPENROUTER: + completion = complete_openrouter(prompt) + case _: # pragma: no cover + raise ValueError(f"Unsupported gatekeeper provider: {provider}") + + logger.info(f"Gatekeeper response: {completion.text}") + return completion diff --git a/src/linux_mcp_server/gatekeeper/openai_client.py b/src/linux_mcp_server/gatekeeper/openai_client.py new file mode 100644 index 00000000..da39999f --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/openai_client.py @@ -0,0 +1,113 @@ +"""OpenAI Responses and Chat Completions clients for the gatekeeper.""" + +from typing import Any + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS +from linux_mcp_server.gatekeeper.http_utils import GatekeeperHTTPError +from linux_mcp_server.gatekeeper.http_utils import get_openai_base_url +from linux_mcp_server.gatekeeper.http_utils import normalize_model_id +from linux_mcp_server.gatekeeper.http_utils import openai_auth_headers +from linux_mcp_server.gatekeeper.http_utils import openai_reasoning_block +from linux_mcp_server.gatekeeper.http_utils import post_json +from linux_mcp_server.gatekeeper.http_utils import prefers_openai_chat_completions +from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion +from linux_mcp_server.gatekeeper.schema import openai_response_format +from linux_mcp_server.gatekeeper.schema import openai_text_format + + +def _apply_chat_completions_extras(body: dict[str, Any]) -> dict[str, Any]: + """Merge template_kwargs into Chat Completions bodies (llama.cpp, etc.).""" + if CONFIG.gatekeeper.template_kwargs: + body["chat_template_kwargs"] = CONFIG.gatekeeper.template_kwargs + return body + + +def _build_responses_body(prompt: str) -> dict[str, Any]: + body: dict[str, Any] = { + "model": normalize_model_id(CONFIG.gatekeeper.model or ""), + "input": prompt, + "temperature": CONFIG.gatekeeper.temperature, + "store": False, + } + if CONFIG.gatekeeper.structured_output: + body["text"] = openai_text_format() + reasoning = openai_reasoning_block(CONFIG.gatekeeper.reasoning_effort) + if reasoning is not None: + body["reasoning"] = reasoning + return body + + +def _build_chat_completions_body(prompt: str) -> dict[str, Any]: + body: dict[str, Any] = { + "model": normalize_model_id(CONFIG.gatekeeper.model or ""), + "messages": [{"role": "user", "content": prompt}], + "temperature": CONFIG.gatekeeper.temperature, + } + if CONFIG.gatekeeper.structured_output: + body["response_format"] = openai_response_format() + reasoning_effort = CONFIG.gatekeeper.reasoning_effort + if reasoning_effort is not None: + body["reasoning_effort"] = reasoning_effort.value + return _apply_chat_completions_extras(body) + + +def _extract_responses_text(response: dict[str, Any]) -> str: + output_text = response.get("output_text") + if isinstance(output_text, str) and output_text.strip(): + return output_text.strip() + + chunks: list[str] = [] + for item in response.get("output", []): + if not isinstance(item, dict) or item.get("type") != "message": + continue + content = item.get("content", []) + if not isinstance(content, list): + continue + for part in content: + if isinstance(part, dict) and part.get("type") == "output_text": + text = part.get("text") + if isinstance(text, str): + chunks.append(text) + return "".join(chunks).strip() + + +def _extract_chat_completions_text(response: dict[str, Any]) -> str: + choices = response.get("choices", []) + if not choices: + return "" + message = choices[0].get("message", {}) + content = message.get("content") + return (content or "").strip() if isinstance(content, str) else "" + + +def complete_openai(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: + base_url = get_openai_base_url() + headers = { + **openai_auth_headers(), + "Content-Type": "application/json", + } + + # Try the Responses API first, falling back to Chat Completions if it's not available. + if not prefers_openai_chat_completions(base_url): + try: + response = post_json( + provider="openai", + url=f"{base_url}/responses", + headers=headers, + body=_build_responses_body(prompt), + timeout=timeout, + ) + return GatekeeperCompletion(text=_extract_responses_text(response)) + except GatekeeperHTTPError as exc: + if exc.status_code not in {404, 405}: + raise + + response = post_json( + provider="openai", + url=f"{base_url}/chat/completions", + headers=headers, + body=_build_chat_completions_body(prompt), + timeout=timeout, + ) + return GatekeeperCompletion(text=_extract_chat_completions_text(response)) diff --git a/src/linux_mcp_server/gatekeeper/openrouter_client.py b/src/linux_mcp_server/gatekeeper/openrouter_client.py new file mode 100644 index 00000000..27ae37f1 --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/openrouter_client.py @@ -0,0 +1,79 @@ +"""OpenRouter Chat Completions client for the gatekeeper.""" + +from typing import Any + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS +from linux_mcp_server.gatekeeper.http_utils import get_openrouter_base_url +from linux_mcp_server.gatekeeper.http_utils import normalize_openrouter_model_id +from linux_mcp_server.gatekeeper.http_utils import openrouter_auth_headers +from linux_mcp_server.gatekeeper.http_utils import openrouter_reasoning_block +from linux_mcp_server.gatekeeper.http_utils import post_json +from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion +from linux_mcp_server.gatekeeper.schema import openai_response_format + + +def _build_chat_completions_body(prompt: str) -> dict[str, Any]: + provider: dict[str, Any] = {"require_parameters": True} + if CONFIG.gatekeeper.quantization: + provider["quantizations"] = [CONFIG.gatekeeper.quantization] + + body: dict[str, Any] = { + "model": normalize_openrouter_model_id(CONFIG.gatekeeper.model or ""), + "messages": [{"role": "user", "content": prompt}], + "temperature": CONFIG.gatekeeper.temperature, + "provider": provider, + } + if CONFIG.gatekeeper.structured_output: + body["response_format"] = openai_response_format() + reasoning = openrouter_reasoning_block(CONFIG.gatekeeper.reasoning_effort) + if reasoning is not None: + body["reasoning"] = reasoning + if CONFIG.gatekeeper.template_kwargs: + body["chat_template_kwargs"] = CONFIG.gatekeeper.template_kwargs + return body + + +def _extract_chat_completions_text(response: dict[str, Any]) -> str: + choices = response.get("choices", []) + if not choices: + return "" + message = choices[0].get("message", {}) + content = message.get("content") + return (content or "").strip() if isinstance(content, str) else "" + + +def _extract_usage(response: dict[str, Any]) -> tuple[int, int, float | None]: + usage = response.get("usage", {}) + if not isinstance(usage, dict): + return 0, 0, None + prompt_tokens = usage.get("prompt_tokens", 0) + completion_tokens = usage.get("completion_tokens", 0) + cost = usage.get("cost") + return ( + int(prompt_tokens) if isinstance(prompt_tokens, int) else 0, + int(completion_tokens) if isinstance(completion_tokens, int) else 0, + float(cost) if isinstance(cost, (int, float)) else None, + ) + + +def complete_openrouter(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: + base_url = get_openrouter_base_url() + headers = { + **openrouter_auth_headers(), + "Content-Type": "application/json", + } + response = post_json( + provider="openrouter", + url=f"{base_url}/chat/completions", + headers=headers, + body=_build_chat_completions_body(prompt), + timeout=timeout, + ) + prompt_tokens, completion_tokens, usage_cost = _extract_usage(response) + return GatekeeperCompletion( + text=_extract_chat_completions_text(response), + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + usage_cost=usage_cost, + ) diff --git a/src/linux_mcp_server/gatekeeper/schema.py b/src/linux_mcp_server/gatekeeper/schema.py new file mode 100644 index 00000000..c7a4ff4d --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/schema.py @@ -0,0 +1,107 @@ +"""JSON schema helpers for gatekeeper structured output across LLM providers.""" + +from copy import deepcopy +from typing import Any + + +_GATEKEEPER_STATUS_VALUES = [ + "OK", + "BAD_DESCRIPTION", + "POLICY", + "MODIFIES_SYSTEM", + "UNCLEAR", + "DANGEROUS", + "MALICIOUS", +] + + +def _base_object_schema() -> dict[str, Any]: + return { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": _GATEKEEPER_STATUS_VALUES, + "description": "Gatekeeper verdict for the script.", + }, + "detail": { + "type": "string", + "description": "Short explanation when status is not OK.", + }, + }, + "required": ["status"], + "additionalProperties": False, + } + + +def _set_additional_properties_false(schema: dict[str, Any]) -> dict[str, Any]: + result = deepcopy(schema) + if result.get("type") == "object": + result["additionalProperties"] = False + properties = result.get("properties") + if isinstance(properties, dict): + for value in properties.values(): + if isinstance(value, dict): + _set_additional_properties_false(value) + elif result.get("type") == "array": + items = result.get("items") + if isinstance(items, dict): + _set_additional_properties_false(items) + return result + + +def openai_json_schema() -> dict[str, Any]: + """Strict JSON schema for OpenAI Responses / Chat Completions structured output.""" + return _set_additional_properties_false(_base_object_schema()) + + +def anthropic_json_schema() -> dict[str, Any]: + """JSON schema for Anthropic Messages API structured output.""" + return _base_object_schema() + + +def gemini_json_schema() -> dict[str, Any]: + """OpenAPI 3.0 subset schema for Gemini generateContent responseSchema.""" + schema = _base_object_schema() + # Gemini responseSchema does not use additionalProperties the same way; keep it simple. + schema.pop("additionalProperties", None) + return schema + + +def openai_response_format() -> dict[str, Any]: + return { + "type": "json_schema", + "json_schema": { + "name": "gatekeeper_result", + "strict": True, + "schema": openai_json_schema(), + }, + } + + +def openai_text_format() -> dict[str, Any]: + return { + "format": { + "type": "json_schema", + "name": "gatekeeper_result", + "strict": True, + "schema": openai_json_schema(), + } + } + + +def anthropic_output_config() -> dict[str, Any]: + return { + "format": { + "type": "json_schema", + "schema": anthropic_json_schema(), + } + } + + +def gemini_generation_config(*, temperature: float, structured_output: bool) -> dict[str, Any]: + config: dict[str, Any] = {"temperature": temperature} + if structured_output: + config["responseMimeType"] = "application/json" + config["responseSchema"] = gemini_json_schema() + return config diff --git a/tests/conftest.py b/tests/conftest.py index 1f8cbdf3..b9cf66e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,8 @@ # Register script tools on the in-process MCP instance used by ``mcp_client``. # Default CLI/config is FIXED-only; tests need validate_script / run_script / etc. os.environ.setdefault("LINUX_MCP_TOOLSET", "both") -os.environ.setdefault("LINUX_MCP_GATEKEEPER__MODEL", "test/gatekeeper-placeholder") +os.environ.setdefault("LINUX_MCP_GATEKEEPER__MODEL", "gemini-2.5-flash") +os.environ.setdefault("LINUX_MCP_GATEKEEPER__PROVIDER", "gemini") import pytest diff --git a/tests/gatekeeper/test_anthropic_client.py b/tests/gatekeeper/test_anthropic_client.py new file mode 100644 index 00000000..d9f13394 --- /dev/null +++ b/tests/gatekeeper/test_anthropic_client.py @@ -0,0 +1,53 @@ +import pytest + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperBackend +from linux_mcp_server.config import GatekeeperConfig +from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.gatekeeper import anthropic_client + + +class TestAnthropicClient: + @pytest.fixture + def gatekeeper_config(self, mocker): + mocker.patch.dict("os.environ", {"ANTHROPIC_API_KEY": "test-key"}, clear=False) + config = GatekeeperConfig( + provider=GatekeeperProvider.ANTHROPIC, + model="claude-sonnet-4-6", + structured_output=True, + temperature=0.0, + ) + mocker.patch.object(CONFIG, "gatekeeper", config) + return config + + def test_complete_anthropic_direct(self, gatekeeper_config, mocker): + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.anthropic_client.post_json", + return_value={"content": [{"type": "text", "text": '{"status": "OK", "detail": ""}'}]}, + ) + + result = anthropic_client.complete_anthropic("prompt") + + assert result == '{"status": "OK", "detail": ""}' + assert mock_post.call_args.kwargs["url"] == "https://api.anthropic.com/v1/messages" + body = mock_post.call_args.kwargs["body"] + assert body["model"] == "claude-sonnet-4-6" + assert body["output_config"]["format"]["type"] == "json_schema" + + def test_complete_anthropic_vertex(self, gatekeeper_config, mocker): + gatekeeper_config.backend = GatekeeperBackend.VERTEX + gatekeeper_config.model = "claude-sonnet-4-5@20250929" + mocker.patch("linux_mcp_server.gatekeeper.anthropic_client.get_gcp_project", return_value="test-project") + mocker.patch("linux_mcp_server.gatekeeper.anthropic_client.get_gcp_location", return_value="global") + mocker.patch("linux_mcp_server.gatekeeper.anthropic_client.get_gcp_access_token", return_value="gcp-token") + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.anthropic_client.post_json", + return_value={"content": [{"type": "text", "text": '{"status": "OK"}'}]}, + ) + + anthropic_client.complete_anthropic("prompt") + + body = mock_post.call_args.kwargs["body"] + assert "model" not in body + assert body["anthropic_version"] == "vertex-2023-10-16" + assert ":rawPredict" in mock_post.call_args.kwargs["url"] diff --git a/tests/gatekeeper/test_check_run_script.py b/tests/gatekeeper/test_check_run_script.py index e97b0d8f..06ecc6fb 100644 --- a/tests/gatekeeper/test_check_run_script.py +++ b/tests/gatekeeper/test_check_run_script.py @@ -1,17 +1,12 @@ import importlib +import time -from unittest.mock import AsyncMock -from unittest.mock import Mock - -import litellm import pytest -from litellm import Choices -from litellm import ModelResponse -from litellm import Usage - from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperBackend from linux_mcp_server.config import GatekeeperConfig +from linux_mcp_server.config import GatekeeperProvider from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper import GatekeeperResult from linux_mcp_server.gatekeeper import GatekeeperStatus @@ -19,10 +14,9 @@ from linux_mcp_server.gatekeeper.check_run_script import check_run_script_with_stats from linux_mcp_server.gatekeeper.check_run_script import GatekeeperException from linux_mcp_server.gatekeeper.check_run_script import get_model +from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion -# Workaround: with python-3.10, mocker.patch("linux_mcp_server.gatekeeper.check_run_script.X") -# doesn't work because it finds the imported check_run_script function rather than the module. check_run_script_module = importlib.import_module("linux_mcp_server.gatekeeper.check_run_script") @@ -54,7 +48,6 @@ def test_round_trip(self, status, detail, expected_description): parsed = GatekeeperResult.parse_from_description(result.description) assert parsed.status == status - # MALICIOUS descriptions hide the original detail if status == GatekeeperStatus.MALICIOUS: assert parsed.detail == "not allowed" else: @@ -67,35 +60,33 @@ def test_parse_from_description_unknown_prefix(self): class TestGetModel: def test_returns_configured_model(self, mocker): - mocker.patch.object(CONFIG.gatekeeper, "model", "test-model") - assert get_model() == "test-model" + mocker.patch.object(CONFIG.gatekeeper, "model", "gpt-5.4") + assert get_model() == "gpt-5.4" def test_raises_when_model_not_configured(self, mocker): mocker.patch.object(CONFIG.gatekeeper, "model", None) with pytest.raises(ValueError, match="To use run_script tools, you must set LINUX_MCP_GATEKEEPER__MODEL"): get_model() + def test_accepts_openrouter_model(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "model", "openrouter/anthropic/claude-3.5-sonnet") + assert get_model() == "openrouter/anthropic/claude-3.5-sonnet" + class TestCheckRunScript: @pytest.fixture - def mock_litellm(self, mocker): - mocker.patch.object(CONFIG.gatekeeper, "model", "test-model") - mock_acompletion = mocker.patch.object(check_run_script_module, "acompletion", new_callable=AsyncMock) - mock_get_params = mocker.patch.object(check_run_script_module, "get_supported_openai_params") - return mock_acompletion, mock_get_params - - def _make_response(self, content: str, usage=None, finish_reason="stop") -> ModelResponse: - message = Mock() - message.content = content - message.annotations = None - choice = Mock(spec=Choices) - choice.message = message - choice.finish_reason = finish_reason - response = Mock(spec=ModelResponse) - response.model = "openai/custom/model" - response.choices = [choice] - response.usage = usage or Usage(prompt_tokens=1000, completion_tokens=100) - return response + def mock_llm(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "model", "gpt-5.4") + mocker.patch.object(CONFIG.gatekeeper, "provider", GatekeeperProvider.OPENAI) + + def _completion(text: str) -> GatekeeperCompletion: + return GatekeeperCompletion(text=text) + + return mocker.patch.object( + check_run_script_module, + "complete_gatekeeper", + side_effect=lambda prompt: _completion('{"status": "OK", "detail": ""}'), + ) async def test_rejects_script_with_prompt_injection_attempts(self): tags = ["START_OF_SCRIPT", "END_OF_SCRIPT", "START_OF_DESCRIPTION", "END_OF_DESCRIPTION"] @@ -105,165 +96,152 @@ async def test_rejects_script_with_prompt_injection_attempts(self): assert result.status == GatekeeperStatus.MALICIOUS assert tag.lower() in result.detail - @pytest.mark.parametrize( - "structured_output,supported_params,expect_response_format", - [ - (None, ["response_format"], True), - (None, [""], False), - (None, None, False), - (False, ["response_format"], False), - (True, [""], True), - ], - ) - async def test_gatekeeper_structured_output( - self, mock_litellm, mocker, structured_output, supported_params, expect_response_format - ): - mock_acompletion, mock_get_params = mock_litellm - mock_get_params.return_value = supported_params - mock_acompletion.return_value = self._make_response('{"status": "OK", "detail": ""}') - mocker.patch.object(CONFIG.gatekeeper, "structured_output", structured_output) - - await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) - - call_kwargs = mock_acompletion.call_args.kwargs - if expect_response_format: - assert call_kwargs["response_format"] is GatekeeperResult - else: - assert "response_format" not in call_kwargs - - @pytest.mark.parametrize( - "gatekeeper_config,expected_kwargs", - [ - ( - GatekeeperConfig(model="openai/gpt-5.4", reasoning_effort=ReasoningEffort.LOW), - {"model": "openai/gpt-5.4", "reasoning_effort": "low", "temperature": 0.0}, - ), - ( - GatekeeperConfig(model="openrouter/openai/gpt-5.4", reasoning_effort=ReasoningEffort.NONE), - { - "model": "openrouter/openai/gpt-5.4", - "reasoning": {"enabled": False}, - "provider": {"require_parameters": True}, - "temperature": 0.0, - }, - ), - ( - GatekeeperConfig(model="openrouter/openai/gpt-5.4", reasoning_effort=ReasoningEffort.LOW), - { - "model": "openrouter/openai/gpt-5.4", - "reasoning": {"enabled": True, "effort": "low"}, - "provider": {"require_parameters": True}, - "temperature": 0.0, - }, - ), - ( - GatekeeperConfig(model="openai/gpt-5.4", template_kwargs={"enable_thinking": False}), - {"model": "openai/gpt-5.4", "chat_template_kwargs": {"enable_thinking": False}, "temperature": 0.0}, - ), - ( - GatekeeperConfig(model="openrouter/qwen/qwen3.5-9b", quantization="bf16"), - { - "model": "openrouter/qwen/qwen3.5-9b", - "provider": {"require_parameters": True, "quantizations": ["bf16"]}, - "temperature": 0.0, - }, - ), - ( - GatekeeperConfig(model="openai/gpt-5.4", temperature=1.0), - { - "model": "openai/gpt-5.4", - "temperature": 1.0, - }, - ), - ], - ) - async def test_gatekeeper_config_to_completion_parameters( - self, mock_litellm, mocker, gatekeeper_config, expected_kwargs - ): - mock_acompletion, mock_get_params = mock_litellm - mock_get_params.return_value = [] - mock_acompletion.return_value = self._make_response('{"status": "OK", "detail": ""}') - mocker.patch.object(CONFIG, "gatekeeper", gatekeeper_config) - - await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) - - # Add in fixed parameters - all_expected_kwargs = expected_kwargs | { - "max_tokens": 8000, - "timeout": 120, - } - - call_kwargs = mock_acompletion.call_args.kwargs - del call_kwargs["messages"] - assert call_kwargs == all_expected_kwargs + async def test_calls_gatekeeper_llm(self, mock_llm): + result = await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) - async def test_missing_detail_defaults_to_empty(self, mock_litellm): - mock_acompletion, mock_get_params = mock_litellm - mock_get_params.return_value = ["response_format"] - mock_acompletion.return_value = self._make_response('{"status": "OK"}') + assert result.status == GatekeeperStatus.OK + mock_llm.assert_called_once() + prompt = mock_llm.call_args.args[0] + assert "echo hi" in prompt + assert "test" in prompt + + async def test_missing_detail_defaults_to_empty(self, mocker): + mocker.patch.object( + check_run_script_module, + "complete_gatekeeper", + return_value=GatekeeperCompletion(text='{"status": "OK"}'), + ) result = await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) assert result.status == GatekeeperStatus.OK assert result.detail == "" - @pytest.mark.parametrize( - "response_text", - ["not valid json", '"just a string"', '{"status": "INVALID_STATUS"}'], - ) - async def test_parse_errors(self, mock_litellm, response_text): - mock_acompletion, mock_get_params = mock_litellm - mock_get_params.return_value = None # No response_format support - mock_acompletion.return_value = self._make_response(response_text) + @pytest.mark.parametrize("response_text", ["not valid json", '"just a string"', '{"status": "INVALID_STATUS"}']) + async def test_parse_errors(self, mocker, response_text): + mocker.patch.object( + check_run_script_module, + "complete_gatekeeper", + return_value=GatekeeperCompletion(text=response_text), + ) with pytest.raises(GatekeeperException, match=r"Failed to parse gatekeeper model output"): await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) - async def test_timeout(self, mock_litellm): - mock_acompletion, mock_get_params = mock_litellm - mock_get_params.return_value = ["response_format"] - mock_acompletion.side_effect = litellm.exceptions.Timeout( - "Timed out", model="custom/model", llm_provider="openai" + async def test_timeout(self, mocker): + def slow_complete(_prompt: str) -> GatekeeperCompletion: + time.sleep(10) + return GatekeeperCompletion(text='{"status": "OK"}') + + mocker.patch.object( + check_run_script_module, + "complete_gatekeeper", + new_callable=mocker.AsyncMock, + side_effect=slow_complete, ) + mocker.patch.object(check_run_script_module, "GATEKEEPER_TIMEOUT", 0.01) with pytest.raises(GatekeeperException, match=r"Timeout calling gatekeeper model"): await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) - async def test_max_tokens(self, mock_litellm): - mock_acompletion, mock_get_params = mock_litellm - mock_get_params.return_value = ["response_format"] - mock_acompletion.return_value = self._make_response('{"status": "OK"', finish_reason="length") - - with pytest.raises(GatekeeperException, match=r"Gatekeeper model output limit reached"): - await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) - - async def test_with_stats(self, mock_litellm): - - usage = Usage(prompt_tokens=1001, completion_tokens=201, cost=0.042) - - mock_acompletion, mock_get_params = mock_litellm - mock_get_params.return_value = ["response_format"] - mock_acompletion.return_value = self._make_response('{"status": "OK", "detail": ""}', usage) - + async def test_with_stats(self, mock_llm): result, stats = await check_run_script_with_stats( description="test", script_type="bash", script="echo hi", readonly=True ) assert result.status == GatekeeperStatus.OK assert result.detail == "" + assert stats.latency > 0 - assert stats.prompt_tokens == 1001 - assert stats.completion_tokens == 201 - assert stats.cost == 0.042 - - async def test_custom_cost(self, mock_litellm, mocker): + async def test_custom_cost(self, mocker): mocker.patch.object(CONFIG.gatekeeper, "cost", (1e-6, 4e-6)) - usage = Usage(prompt_tokens=1001, completion_tokens=201) + mocker.patch.object( + check_run_script_module, + "complete_gatekeeper", + return_value=GatekeeperCompletion( + text='{"status": "OK", "detail": ""}', prompt_tokens=100, completion_tokens=50 + ), + ) + + _, stats = await check_run_script_with_stats( + description="test", script_type="bash", script="echo hi", readonly=True + ) - mock_acompletion, mock_get_params = mock_litellm - mock_get_params.return_value = ["response_format"] - mock_acompletion.return_value = self._make_response('{"status": "OK", "detail": ""}', usage) + assert stats.cost == pytest.approx(100 * 1e-6 + 50 * 4e-6) + + async def test_openrouter_usage_cost(self, mocker): + mocker.patch.object( + check_run_script_module, + "complete_gatekeeper", + return_value=GatekeeperCompletion( + text='{"status": "OK", "detail": ""}', + prompt_tokens=10, + completion_tokens=5, + usage_cost=0.42, + ), + ) _, stats = await check_run_script_with_stats( description="test", script_type="bash", script="echo hi", readonly=True ) - assert stats.cost == 1001 * 1e-6 + 201 * 4e-6 + assert stats.prompt_tokens == 10 + assert stats.completion_tokens == 5 + assert stats.cost == 0.42 + + +class TestGatekeeperConfigIntegration: + @pytest.fixture + def mock_openai_post(self, mocker): + mocker.patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}, clear=False) + return mocker.patch( + "linux_mcp_server.gatekeeper.openai_client.post_json", + return_value={"output_text": '{"status": "OK", "detail": ""}'}, + ) + + async def test_openai_provider_config(self, mocker, mock_openai_post): + mocker.patch.object( + CONFIG, + "gatekeeper", + GatekeeperConfig( + provider=GatekeeperProvider.OPENAI, + model="gpt-5.4", + reasoning_effort=ReasoningEffort.LOW, + structured_output=True, + temperature=0.0, + ), + ) + + await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) + + body = mock_openai_post.call_args.kwargs["body"] + assert body["model"] == "gpt-5.4" + assert body["reasoning"] == {"effort": "low"} + assert body["temperature"] == 0.0 + assert "text" in body + + async def test_openai_vertex_backend_uses_custom_base_url(self, mocker): + mocker.patch.dict("os.environ", {}, clear=False) + mocker.patch( + "linux_mcp_server.gatekeeper.gcp_auth.get_gcp_access_token", + return_value="gcp-token", + ) + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openai_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + mocker.patch.object( + CONFIG, + "gatekeeper", + GatekeeperConfig( + provider=GatekeeperProvider.OPENAI, + backend=GatekeeperBackend.VERTEX, + model="gpt-oss-120b-maas", + base_url="https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi", + structured_output=False, + temperature=0.0, + ), + ) + + await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) + + assert mock_post.call_args.kwargs["url"].endswith("/chat/completions") + assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer gcp-token" diff --git a/tests/gatekeeper/test_gcp_auth.py b/tests/gatekeeper/test_gcp_auth.py new file mode 100644 index 00000000..33bb93da --- /dev/null +++ b/tests/gatekeeper/test_gcp_auth.py @@ -0,0 +1,34 @@ +import pytest + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.gatekeeper.gcp_auth import GCPAuthError +from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_location +from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project + + +class TestGCPAuth: + def test_get_gcp_project_from_config(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "project", "from-config") + mocker.patch.dict("os.environ", {}, clear=True) + assert get_gcp_project() == "from-config" + + def test_get_gcp_project_from_env(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "project", None) + mocker.patch.dict("os.environ", {"VERTEXAI_PROJECT": "from-env"}, clear=True) + assert get_gcp_project() == "from-env" + + def test_get_gcp_project_missing(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "project", None) + mocker.patch.dict("os.environ", {}, clear=True) + with pytest.raises(GCPAuthError, match="Vertex backend requires a GCP project"): + get_gcp_project() + + def test_get_gcp_location_from_env(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "location", None) + mocker.patch.dict("os.environ", {"VERTEXAI_LOCATION": "us-central1"}, clear=True) + assert get_gcp_location() == "us-central1" + + def test_get_gcp_location_default(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "location", None) + mocker.patch.dict("os.environ", {}, clear=True) + assert get_gcp_location() == "global" diff --git a/tests/gatekeeper/test_gemini_client.py b/tests/gatekeeper/test_gemini_client.py new file mode 100644 index 00000000..19739dbd --- /dev/null +++ b/tests/gatekeeper/test_gemini_client.py @@ -0,0 +1,55 @@ +import pytest + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperBackend +from linux_mcp_server.config import GatekeeperConfig +from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.config import ReasoningEffort +from linux_mcp_server.gatekeeper import gemini_client + + +class TestGeminiClient: + @pytest.fixture + def gatekeeper_config(self, mocker): + mocker.patch.dict("os.environ", {"GOOGLE_API_KEY": "test-key"}, clear=False) + config = GatekeeperConfig( + provider=GatekeeperProvider.GEMINI, + model="gemini-2.0-flash", + reasoning_effort=ReasoningEffort.LOW, + structured_output=True, + temperature=0.0, + ) + mocker.patch.object(CONFIG, "gatekeeper", config) + return config + + def test_complete_gemini_google_ai(self, gatekeeper_config, mocker): + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.gemini_client.post_json", + return_value={"candidates": [{"content": {"parts": [{"text": '{"status": "OK"}'}]}}]}, + ) + + result = gemini_client.complete_gemini("prompt") + + assert result == '{"status": "OK"}' + url = mock_post.call_args.kwargs["url"] + assert "generativelanguage.googleapis.com" in url + assert "key=test-key" in url + body = mock_post.call_args.kwargs["body"] + assert body["generationConfig"]["responseMimeType"] == "application/json" + assert body["generationConfig"]["thinkingConfig"] == {"thinkingLevel": "LOW"} + + def test_complete_gemini_vertex(self, gatekeeper_config, mocker): + gatekeeper_config.backend = GatekeeperBackend.VERTEX + gatekeeper_config.model = "gemini-3.1-pro-preview" + mocker.patch("linux_mcp_server.gatekeeper.gemini_client.get_gcp_project", return_value="test-project") + mocker.patch("linux_mcp_server.gatekeeper.gemini_client.get_gcp_location", return_value="global") + mocker.patch("linux_mcp_server.gatekeeper.gemini_client.get_gcp_access_token", return_value="gcp-token") + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.gemini_client.post_json", + return_value={"candidates": [{"content": {"parts": [{"text": '{"status": "OK"}'}]}}]}, + ) + + gemini_client.complete_gemini("prompt") + + assert ":generateContent" in mock_post.call_args.kwargs["url"] + assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer gcp-token" diff --git a/tests/gatekeeper/test_http_utils.py b/tests/gatekeeper/test_http_utils.py new file mode 100644 index 00000000..99ea605c --- /dev/null +++ b/tests/gatekeeper/test_http_utils.py @@ -0,0 +1,85 @@ +import pytest + +from linux_mcp_server.config import ReasoningEffort +from linux_mcp_server.gatekeeper.http_utils import anthropic_thinking_block +from linux_mcp_server.gatekeeper.http_utils import gemini_thinking_level +from linux_mcp_server.gatekeeper.http_utils import normalize_model_id +from linux_mcp_server.gatekeeper.http_utils import normalize_openrouter_model_id +from linux_mcp_server.gatekeeper.http_utils import openai_reasoning_block +from linux_mcp_server.gatekeeper.http_utils import openrouter_reasoning_block +from linux_mcp_server.gatekeeper.http_utils import prefers_openai_chat_completions + + +@pytest.mark.parametrize( + "model,expected", + [ + ("openai/gpt-5.4", "gpt-5.4"), + ("anthropic/claude-sonnet-4-6", "claude-sonnet-4-6"), + ("vertex_ai/gemini-3.1-pro-preview", "gemini-3.1-pro-preview"), + ("gpt-oss-120b-maas", "gpt-oss-120b-maas"), + ], +) +def test_normalize_model_id(model, expected): + assert normalize_model_id(model) == expected + + +@pytest.mark.parametrize( + "base_url,expected", + [ + ("https://api.openai.com/v1", False), + ("http://localhost:11434/v1", False), + ("https://example.com/v1", False), + ( + "https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi", + True, + ), + ], +) +def test_prefers_openai_chat_completions(base_url, expected): + assert prefers_openai_chat_completions(base_url) is expected + + +def test_openai_reasoning_block_none(): + assert openai_reasoning_block(None) is None + assert openai_reasoning_block(ReasoningEffort.DEFAULT) is None + + +def test_openai_reasoning_block_low(): + assert openai_reasoning_block(ReasoningEffort.LOW) == {"effort": "low"} + + +@pytest.mark.parametrize( + "effort,expected", + [ + (ReasoningEffort.NONE, {"enabled": False}), + (ReasoningEffort.LOW, {"enabled": True, "effort": "low"}), + (ReasoningEffort.HIGH, {"enabled": True, "effort": "high"}), + ], +) +def test_openrouter_reasoning_block(effort, expected): + assert openrouter_reasoning_block(effort) == expected + + +def test_openrouter_reasoning_block_default(): + assert openrouter_reasoning_block(None) is None + assert openrouter_reasoning_block(ReasoningEffort.DEFAULT) is None + + +@pytest.mark.parametrize( + "model,expected", + [ + ("openrouter/google/gemma-4-26b-a4b-it", "google/gemma-4-26b-a4b-it"), + ("openai/gpt-oss-120b", "openai/gpt-oss-120b"), + ], +) +def test_normalize_openrouter_model_id(model, expected): + assert normalize_openrouter_model_id(model) == expected + + +def test_anthropic_thinking_block_low(): + block = anthropic_thinking_block(ReasoningEffort.LOW) + assert block == {"type": "enabled", "budget_tokens": 4096} + + +def test_gemini_thinking_level_medium(): + assert gemini_thinking_level(ReasoningEffort.MEDIUM) == "MEDIUM" diff --git a/tests/gatekeeper/test_llm.py b/tests/gatekeeper/test_llm.py new file mode 100644 index 00000000..1fce5c2c --- /dev/null +++ b/tests/gatekeeper/test_llm.py @@ -0,0 +1,55 @@ +import importlib + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.gatekeeper.llm import complete_gatekeeper +from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion +from linux_mcp_server.gatekeeper.llm import resolve_provider + + +llm_module = importlib.import_module("linux_mcp_server.gatekeeper.llm") + + +class TestResolveProvider: + def test_explicit_provider(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "provider", GatekeeperProvider.ANTHROPIC) + mocker.patch.object(CONFIG.gatekeeper, "model", "claude-sonnet-4-6") + assert resolve_provider() == GatekeeperProvider.ANTHROPIC + + def test_infer_openai_from_model_prefix(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "provider", None) + mocker.patch.object(CONFIG.gatekeeper, "model", "openai/gpt-5.4") + assert resolve_provider() == GatekeeperProvider.OPENAI + + def test_infer_gemini_from_model_prefix(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "provider", None) + mocker.patch.object(CONFIG.gatekeeper, "model", "gemini-2.0-flash") + assert resolve_provider() == GatekeeperProvider.GEMINI + + def test_infer_openrouter_from_model_prefix(self, mocker): + mocker.patch.object(CONFIG.gatekeeper, "provider", None) + mocker.patch.object(CONFIG.gatekeeper, "model", "openrouter/anthropic/claude-3.5-sonnet") + assert resolve_provider() == GatekeeperProvider.OPENROUTER + + +class TestCompleteGatekeeper: + def test_routes_to_openai(self, mocker): + mocker.patch.object(llm_module, "resolve_provider", return_value=GatekeeperProvider.OPENAI) + mock_complete = mocker.patch( + "linux_mcp_server.gatekeeper.openai_client.complete_openai", + return_value='{"status": "OK"}', + ) + result = complete_gatekeeper("prompt") + assert result.text == '{"status": "OK"}' + mock_complete.assert_called_once_with("prompt") + + def test_routes_to_openrouter(self, mocker): + mocker.patch.object(llm_module, "resolve_provider", return_value=GatekeeperProvider.OPENROUTER) + expected = GatekeeperCompletion(text='{"status": "OK"}', prompt_tokens=1, completion_tokens=2, usage_cost=0.5) + mock_complete = mocker.patch( + "linux_mcp_server.gatekeeper.openrouter_client.complete_openrouter", + return_value=expected, + ) + result = complete_gatekeeper("prompt") + assert result == expected + mock_complete.assert_called_once_with("prompt") diff --git a/tests/gatekeeper/test_openai_client.py b/tests/gatekeeper/test_openai_client.py new file mode 100644 index 00000000..32af4d99 --- /dev/null +++ b/tests/gatekeeper/test_openai_client.py @@ -0,0 +1,122 @@ +import pytest + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperBackend +from linux_mcp_server.config import GatekeeperConfig +from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.config import ReasoningEffort +from linux_mcp_server.gatekeeper import openai_client +from linux_mcp_server.gatekeeper.http_utils import GatekeeperHTTPError + + +class TestOpenAIClient: + @pytest.fixture + def gatekeeper_config(self, mocker): + mocker.patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}, clear=False) + config = GatekeeperConfig( + provider=GatekeeperProvider.OPENAI, + model="gpt-5.4", + reasoning_effort=ReasoningEffort.LOW, + structured_output=True, + temperature=0.0, + ) + mocker.patch.object(CONFIG, "gatekeeper", config) + return config + + def test_complete_openai_uses_responses_api(self, gatekeeper_config, mocker): + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openai_client.post_json", + return_value={"output_text": '{"status": "OK", "detail": ""}'}, + ) + + result = openai_client.complete_openai("prompt") + + assert result == '{"status": "OK", "detail": ""}' + assert mock_post.call_args.kwargs["url"] == "https://api.openai.com/v1/responses" + body = mock_post.call_args.kwargs["body"] + assert body["model"] == "gpt-5.4" + assert body["reasoning"] == {"effort": "low"} + assert body["text"]["format"]["type"] == "json_schema" + + def test_complete_openai_uses_responses_api_for_ollama(self, gatekeeper_config, mocker): + gatekeeper_config.base_url = "http://localhost:11434/v1" + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openai_client.post_json", + return_value={"output_text": '{"status": "OK", "detail": ""}'}, + ) + + result = openai_client.complete_openai("prompt") + + assert result == '{"status": "OK", "detail": ""}' + assert mock_post.call_args.kwargs["url"] == "http://localhost:11434/v1/responses" + + def test_complete_openai_falls_back_to_chat_completions_on_404(self, gatekeeper_config, mocker): + gatekeeper_config.base_url = "https://models.example.com/v1" + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openai_client.post_json", + side_effect=[ + GatekeeperHTTPError("openai", 404, "not found"), + {"choices": [{"message": {"content": '{"status": "OK", "detail": ""}'}}]}, + ], + ) + + result = openai_client.complete_openai("prompt") + + assert result == '{"status": "OK", "detail": ""}' + assert mock_post.call_args_list[0].kwargs["url"] == "https://models.example.com/v1/responses" + assert mock_post.call_args_list[1].kwargs["url"] == "https://models.example.com/v1/chat/completions" + body = mock_post.call_args_list[1].kwargs["body"] + assert body["response_format"]["type"] == "json_schema" + assert body["reasoning_effort"] == "low" + + def test_complete_openai_vertex_uses_gcp_token(self, gatekeeper_config, mocker): + gatekeeper_config.backend = GatekeeperBackend.VERTEX + gatekeeper_config.base_url = ( + "https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi" + ) + mocker.patch( + "linux_mcp_server.gatekeeper.gcp_auth.get_gcp_access_token", + return_value="gcp-token", + ) + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openai_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + + openai_client.complete_openai("prompt") + + assert mock_post.call_count == 1 + assert mock_post.call_args.kwargs["url"].endswith("/chat/completions") + headers = mock_post.call_args.kwargs["headers"] + assert headers["Authorization"] == "Bearer gcp-token" + + def test_structured_output_disabled(self, gatekeeper_config, mocker): + gatekeeper_config.structured_output = False + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openai_client.post_json", + return_value={"output_text": '{"status": "OK"}'}, + ) + + openai_client.complete_openai("prompt") + + body = mock_post.call_args.kwargs["body"] + assert "text" not in body + + def test_template_kwargs_on_chat_completions(self, gatekeeper_config, mocker): + gatekeeper_config.base_url = ( + "https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi" + ) + gatekeeper_config.template_kwargs = {"enable_thinking": False} + mocker.patch( + "linux_mcp_server.gatekeeper.gcp_auth.get_gcp_access_token", + return_value="gcp-token", + ) + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openai_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + + openai_client.complete_openai("prompt") + + body = mock_post.call_args.kwargs["body"] + assert body["chat_template_kwargs"] == {"enable_thinking": False} diff --git a/tests/gatekeeper/test_openrouter_client.py b/tests/gatekeeper/test_openrouter_client.py new file mode 100644 index 00000000..5c6a8d7c --- /dev/null +++ b/tests/gatekeeper/test_openrouter_client.py @@ -0,0 +1,117 @@ +import pytest + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperConfig +from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.config import ReasoningEffort +from linux_mcp_server.gatekeeper import openrouter_client + + +class TestOpenRouterClient: + @pytest.fixture + def gatekeeper_config(self, mocker): + mocker.patch.dict("os.environ", {"OPENROUTER_API_KEY": "test-key"}, clear=False) + config = GatekeeperConfig( + provider=GatekeeperProvider.OPENROUTER, + model="openai/gpt-oss-120b", + reasoning_effort=ReasoningEffort.LOW, + structured_output=True, + temperature=0.0, + ) + mocker.patch.object(CONFIG, "gatekeeper", config) + return config + + def test_complete_openrouter_request_body(self, gatekeeper_config, mocker): + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openrouter_client.post_json", + return_value={ + "choices": [{"message": {"content": '{"status": "OK", "detail": ""}'}}], + "usage": {"prompt_tokens": 10, "completion_tokens": 5, "cost": 0.001}, + }, + ) + + completion = openrouter_client.complete_openrouter("prompt") + + assert completion.text == '{"status": "OK", "detail": ""}' + assert completion.prompt_tokens == 10 + assert completion.completion_tokens == 5 + assert completion.usage_cost == 0.001 + assert mock_post.call_args.kwargs["url"] == "https://openrouter.ai/api/v1/chat/completions" + headers = mock_post.call_args.kwargs["headers"] + assert headers["Authorization"] == "Bearer test-key" + body = mock_post.call_args.kwargs["body"] + assert body["model"] == "openai/gpt-oss-120b" + assert body["reasoning"] == {"enabled": True, "effort": "low"} + assert body["provider"] == {"require_parameters": True} + assert body["response_format"]["type"] == "json_schema" + + def test_complete_openrouter_quantization(self, gatekeeper_config, mocker): + gatekeeper_config.quantization = "fp4" + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openrouter_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + + openrouter_client.complete_openrouter("prompt") + + body = mock_post.call_args.kwargs["body"] + assert body["provider"] == {"require_parameters": True, "quantizations": ["fp4"]} + + def test_complete_openrouter_reasoning_none(self, gatekeeper_config, mocker): + gatekeeper_config.reasoning_effort = ReasoningEffort.NONE + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openrouter_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + + openrouter_client.complete_openrouter("prompt") + + body = mock_post.call_args.kwargs["body"] + assert body["reasoning"] == {"enabled": False} + + def test_complete_openrouter_legacy_model_prefix(self, gatekeeper_config, mocker): + gatekeeper_config.model = "openrouter/google/gemma-4-26b-a4b-it" + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openrouter_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + + openrouter_client.complete_openrouter("prompt") + + body = mock_post.call_args.kwargs["body"] + assert body["model"] == "google/gemma-4-26b-a4b-it" + + def test_complete_openrouter_custom_base_url(self, gatekeeper_config, mocker): + gatekeeper_config.base_url = "https://openrouter.example.com/api/v1" + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openrouter_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + + openrouter_client.complete_openrouter("prompt") + + assert mock_post.call_args.kwargs["url"] == "https://openrouter.example.com/api/v1/chat/completions" + + def test_structured_output_disabled(self, gatekeeper_config, mocker): + gatekeeper_config.structured_output = False + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openrouter_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + + openrouter_client.complete_openrouter("prompt") + + body = mock_post.call_args.kwargs["body"] + assert "response_format" not in body + + def test_template_kwargs(self, gatekeeper_config, mocker): + gatekeeper_config.template_kwargs = {"enable_thinking": False} + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.openrouter_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + + openrouter_client.complete_openrouter("prompt") + + body = mock_post.call_args.kwargs["body"] + assert body["chat_template_kwargs"] == {"enable_thinking": False} diff --git a/tests/gatekeeper/test_schema.py b/tests/gatekeeper/test_schema.py new file mode 100644 index 00000000..e13aa969 --- /dev/null +++ b/tests/gatekeeper/test_schema.py @@ -0,0 +1,32 @@ +from linux_mcp_server.gatekeeper.schema import _GATEKEEPER_STATUS_VALUES +from linux_mcp_server.gatekeeper.schema import anthropic_json_schema +from linux_mcp_server.gatekeeper.schema import gemini_json_schema +from linux_mcp_server.gatekeeper.schema import openai_json_schema +from linux_mcp_server.gatekeeper.schema import openai_response_format +from linux_mcp_server.gatekeeper.schema import openai_text_format + + +class TestGatekeeperSchemas: + def test_openai_schema_is_strict(self): + schema = openai_json_schema() + assert schema["additionalProperties"] is False + assert set(schema["properties"]) == {"status", "detail"} + assert schema["properties"]["status"]["enum"] == _GATEKEEPER_STATUS_VALUES + + def test_openai_response_format(self): + response_format = openai_response_format() + assert response_format["type"] == "json_schema" + assert response_format["json_schema"]["strict"] is True + + def test_openai_text_format(self): + text_format = openai_text_format() + assert text_format["format"]["type"] == "json_schema" + assert text_format["format"]["name"] == "gatekeeper_result" + + def test_anthropic_schema_has_status_enum(self): + schema = anthropic_json_schema() + assert "status" in schema["properties"] + + def test_gemini_schema_omits_additional_properties(self): + schema = gemini_json_schema() + assert "additionalProperties" not in schema diff --git a/tests/test_config.py b/tests/test_config.py index 0e3c4f5f..19ecdc55 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -10,6 +10,8 @@ from pydantic import ValidationError from linux_mcp_server.config import Config +from linux_mcp_server.config import GatekeeperBackend +from linux_mcp_server.config import GatekeeperProvider class TestConfig: @@ -299,29 +301,31 @@ def test_new_env_var_works_without_old(self, mock_getuser, monkeypatch): class TestGatekeeperConfig: + def test_provider_and_backend(self, mock_getuser, monkeypatch): + monkeypatch.setenv("LINUX_MCP_GATEKEEPER__PROVIDER", "gemini") + monkeypatch.setenv("LINUX_MCP_GATEKEEPER__BACKEND", "vertex") + monkeypatch.setenv("LINUX_MCP_GATEKEEPER__MODEL", "gemini-3.1-pro-preview") + + config = Config() + + assert config.gatekeeper.provider == GatekeeperProvider.GEMINI + assert config.gatekeeper.backend == GatekeeperBackend.VERTEX + assert config.gatekeeper.model == "gemini-3.1-pro-preview" + + def test_structured_output_defaults_true(self, mock_getuser, monkeypatch): + config = Config() + assert config.gatekeeper.structured_output is True + def test_template_kwargs(self, mock_getuser, monkeypatch): - """Test setting template_kwargs with a JSON object.""" monkeypatch.setenv( "LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS", '{ "enable_thinking": true, "reasoning_effort": "low" }' ) config = Config() - assert config.gatekeeper.template_kwargs == {"enable_thinking": True, "reasoning_effort": "low"} - - def test_template_kwargs_one_by_one(self, mock_getuser, monkeypatch): - """Test setting template_kwargs, key-by-key""" - # This ends up with strings, which is not what we want, so we'll just document - # setting it as a JSON object - monkeypatch.setenv("LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS__ENABLE_THINKING", "true") - monkeypatch.setenv("LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS__REASONING_EFFORT", "low") - monkeypatch.setenv("LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS__UNSET", "") - - config = Config() - assert config.gatekeeper.template_kwargs == {"enable_thinking": "true", "reasoning_effort": "low"} + assert config.gatekeeper.template_kwargs == {"enable_thinking": True, "reasoning_effort": "low"} def test_template_kwargs_unset(self, mock_getuser, monkeypatch): - """Test default value for template_kwargs""" config = Config() assert config.gatekeeper.template_kwargs == {} @@ -330,6 +334,17 @@ def test_cost(self, monkeypatch): config = Config() assert config.gatekeeper.cost == (1e-6, 4e-6) + def test_openrouter_provider_and_quantization(self, mock_getuser, monkeypatch): + monkeypatch.setenv("LINUX_MCP_GATEKEEPER__PROVIDER", "openrouter") + monkeypatch.setenv("LINUX_MCP_GATEKEEPER__MODEL", "openai/gpt-oss-120b") + monkeypatch.setenv("LINUX_MCP_GATEKEEPER__QUANTIZATION", "fp4") + + config = Config() + + assert config.gatekeeper.provider == GatekeeperProvider.OPENROUTER + assert config.gatekeeper.model == "openai/gpt-oss-120b" + assert config.gatekeeper.quantization == "fp4" + @pytest.mark.parametrize( "value", ["not_a_float", "not_a_float:not_a_float", "1e-6"], diff --git a/tests/tools/test_run_script.py b/tests/tools/test_run_script.py index 89dc9cac..8818a681 100644 --- a/tests/tools/test_run_script.py +++ b/tests/tools/test_run_script.py @@ -64,7 +64,7 @@ def patch_execute_command(mocker) -> Any: @pytest.fixture def patch_check_run_script(mocker) -> Any: - """Mock the LLM gatekeeper so tests do not call litellm.""" + """Mock the LLM gatekeeper so tests do not call the provider HTTP APIs.""" return mocker.patch.object(run_script_mod, "check_run_script", autospec=True) diff --git a/uv.lock b/uv.lock index 62d25f1f..aa5f3d7a 100644 --- a/uv.lock +++ b/uv.lock @@ -25,157 +25,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, ] -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11'" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, - { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, - { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, - { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, - { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, - { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, - { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, - { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, - { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, - { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, - { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, - { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, - { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, - { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, - { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, -] - [[package]] name = "annotated-types" version = "0.7.0" @@ -219,15 +68,15 @@ wheels = [ [[package]] name = "asyncssh" -version = "2.23.1" +version = "2.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/95/212d3d394f2a6ccb3f95056d3b9a7ce13c2f58503cbd6a38d037ef48cb13/asyncssh-2.23.1.tar.gz", hash = "sha256:d9dc3bc0206f3e4b5d80d1c0e6a24af2b4ad4beb556884c41fb2ad1c7ca3f44f", size = 542883, upload-time = "2026-06-07T14:15:18.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/fd/c34fe7e30838b4b9cc91903da26a62c6d33b673c731b3d951fcd70ab1889/asyncssh-2.23.0.tar.gz", hash = "sha256:8c54760953c1f2cf282591bcba5c8c70efc48d645bbf26bd2307a9c66a0ed1a7", size = 542154, upload-time = "2026-05-09T03:15:01.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/94/9aa81bde40627af70388d634152e7c53e7533788e662b8093047501a1473/asyncssh-2.23.1-py3-none-any.whl", hash = "sha256:f68e55476d41253d785bcac9a90834ae5fdea0f417bd6d7182608bda248de88e", size = 376054, upload-time = "2026-06-07T14:15:17.375Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b5/b1a3979f4840d1271ca8e0978dbccfb18ad2d33b4ece85cf77122fb46e5f/asyncssh-2.23.0-py3-none-any.whl", hash = "sha256:14108bfdaae17457f0c1841e883ad934271bbfdd46458aa4c4d0973451940ad0", size = 375687, upload-time = "2026-05-09T03:15:00.221Z" }, ] [package.optional-dependencies] @@ -819,15 +668,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -936,208 +776,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/76/b310d52fa0e30d39bd937eb58ec2c1f1ea1b5f519f0575e9dd9612f01deb/fastmcp-3.2.4-py3-none-any.whl", hash = "sha256:e6c9c429171041455e47ab94bb3f83c4657622a0ec28922f6940053959bd58a9", size = 728599, upload-time = "2026-04-14T01:42:26.85Z" }, ] -[[package]] -name = "fastuuid" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/b2/731a6696e37cd20eed353f69a09f37a984a43c9713764ee3f7ad5f57f7f9/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6e6243d40f6c793c3e2ee14c13769e341b90be5ef0c23c82fa6515a96145181a", size = 516760, upload-time = "2025-10-19T22:25:21.509Z" }, - { url = "https://files.pythonhosted.org/packages/c5/79/c73c47be2a3b8734d16e628982653517f80bbe0570e27185d91af6096507/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:13ec4f2c3b04271f62be2e1ce7e95ad2dd1cf97e94503a3760db739afbd48f00", size = 264748, upload-time = "2025-10-19T22:41:52.873Z" }, - { url = "https://files.pythonhosted.org/packages/24/c5/84c1eea05977c8ba5173555b0133e3558dc628bcf868d6bf1689ff14aedc/fastuuid-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b2fdd48b5e4236df145a149d7125badb28e0a383372add3fbaac9a6b7a394470", size = 254537, upload-time = "2025-10-19T22:33:55.603Z" }, - { url = "https://files.pythonhosted.org/packages/0e/23/4e362367b7fa17dbed646922f216b9921efb486e7abe02147e4b917359f8/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f74631b8322d2780ebcf2d2d75d58045c3e9378625ec51865fe0b5620800c39d", size = 278994, upload-time = "2025-10-19T22:26:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/b2/72/3985be633b5a428e9eaec4287ed4b873b7c4c53a9639a8b416637223c4cd/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cffc144dc93eb604b87b179837f2ce2af44871a7b323f2bfed40e8acb40ba8", size = 280003, upload-time = "2025-10-19T22:23:45.415Z" }, - { url = "https://files.pythonhosted.org/packages/b3/6d/6ef192a6df34e2266d5c9deb39cd3eea986df650cbcfeaf171aa52a059c3/fastuuid-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a771f135ab4523eb786e95493803942a5d1fc1610915f131b363f55af53b219", size = 303583, upload-time = "2025-10-19T22:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/9d/11/8a2ea753c68d4fece29d5d7c6f3f903948cc6e82d1823bc9f7f7c0355db3/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4edc56b877d960b4eda2c4232f953a61490c3134da94f3c28af129fb9c62a4f6", size = 460955, upload-time = "2025-10-19T22:36:25.196Z" }, - { url = "https://files.pythonhosted.org/packages/23/42/7a32c93b6ce12642d9a152ee4753a078f372c9ebb893bc489d838dd4afd5/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bcc96ee819c282e7c09b2eed2b9bd13084e3b749fdb2faf58c318d498df2efbe", size = 480763, upload-time = "2025-10-19T22:24:28.451Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e9/a5f6f686b46e3ed4ed3b93770111c233baac87dd6586a411b4988018ef1d/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a3c0bca61eacc1843ea97b288d6789fbad7400d16db24e36a66c28c268cfe3d", size = 452613, upload-time = "2025-10-19T22:25:06.827Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c9/18abc73c9c5b7fc0e476c1733b678783b2e8a35b0be9babd423571d44e98/fastuuid-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7f2f3efade4937fae4e77efae1af571902263de7b78a0aee1a1653795a093b2a", size = 155045, upload-time = "2025-10-19T22:28:32.732Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8a/d9e33f4eb4d4f6d9f2c5c7d7e96b5cdbb535c93f3b1ad6acce97ee9d4bf8/fastuuid-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae64ba730d179f439b0736208b4c279b8bc9c089b102aec23f86512ea458c8a4", size = 156122, upload-time = "2025-10-19T22:23:15.59Z" }, - { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, - { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, - { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, - { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, - { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, - { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, - { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, - { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, - { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, - { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, - { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, - { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, - { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, - { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, - { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, - { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, - { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, - { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, - { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, - { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, - { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, - { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, - { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, - { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, -] - -[[package]] -name = "filelock" -version = "3.25.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, -] - -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, - { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, - { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, - { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, - { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, - { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, - { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, - { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, - { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, - { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, - { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, - { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, - { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, - { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, - { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, - { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, - { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, -] - -[[package]] -name = "fsspec" -version = "2026.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, -] - [[package]] name = "ghp-import" version = "2.1.0" @@ -1150,28 +788,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] -[[package]] -name = "google-api-core" -version = "2.30.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" }, -] - -[package.optional-dependencies] -grpc = [ - { name = "grpcio" }, - { name = "grpcio-status" }, -] - [[package]] name = "google-auth" version = "2.49.2" @@ -1185,185 +801,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" }, ] -[package.optional-dependencies] -requests = [ - { name = "requests" }, -] - -[[package]] -name = "google-cloud-aiplatform" -version = "1.157.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "docstring-parser" }, - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "google-cloud-bigquery" }, - { name = "google-cloud-resource-manager" }, - { name = "google-cloud-storage" }, - { name = "google-genai" }, - { name = "packaging" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/d9/e2a5f5a8535bbc8f68729796f3fc2d68d59a72818fb44f6544edbc2592e4/google_cloud_aiplatform-1.157.0.tar.gz", hash = "sha256:ce8413ed3584c4896f7656b663214c24e91c2c89426f1c91fbd1d220ffda23af", size = 11064992, upload-time = "2026-06-10T00:19:33.643Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/82/3ec2ba56dc1fa71ef783348a0c519721879dbc8f1e568534e6d4b4856ccd/google_cloud_aiplatform-1.157.0-py2.py3-none-any.whl", hash = "sha256:0ca499ac5648988916fc089f9e94bd99667eefba13f6936475247f4a0bf86634", size = 9200777, upload-time = "2026-06-10T00:19:30.181Z" }, -] - -[[package]] -name = "google-cloud-bigquery" -version = "3.41.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "google-cloud-core" }, - { name = "google-resumable-media" }, - { name = "packaging" }, - { name = "python-dateutil" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/13/6515c7aab55a4a0cf708ffd309fb9af5bab54c13e32dc22c5acd6497193c/google_cloud_bigquery-3.41.0.tar.gz", hash = "sha256:2217e488b47ed576360c9b2cc07d59d883a54b83167c0ef37f915c26b01a06fe", size = 513434, upload-time = "2026-03-30T22:50:55.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/33/1d3902efadef9194566d499d61507e1f038454e0b55499d2d7f8ab2a4fee/google_cloud_bigquery-3.41.0-py3-none-any.whl", hash = "sha256:2a5b5a737b401cbd824a6e5eac7554100b878668d908e6548836b5d8aaa4dcaa", size = 262343, upload-time = "2026-03-30T22:48:45.444Z" }, -] - -[[package]] -name = "google-cloud-core" -version = "2.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core" }, - { name = "google-auth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/24/6ca08b0a03c7b0c620427503ab00353a4ae806b848b93bcea18b6b76fde6/google_cloud_core-2.5.1.tar.gz", hash = "sha256:3dc94bdec9d05a31d9f355045ed0f369fbc0d8c665076c734f065d729800f811", size = 36078, upload-time = "2026-03-30T22:50:08.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/d9/5bb050cb32826466aa9b25f79e2ca2879fe66cb76782d4ed798dd7506151/google_cloud_core-2.5.1-py3-none-any.whl", hash = "sha256:ea62cdf502c20e3e14be8a32c05ed02113d7bef454e40ff3fab6fe1ec9f1f4e7", size = 29452, upload-time = "2026-03-30T22:48:31.567Z" }, -] - -[[package]] -name = "google-cloud-resource-manager" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpc-google-iam-v1" }, - { name = "grpcio" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b2/1a/13060cabf553d52d151d2afc26b39561e82853380d499dd525a0d422d9f0/google_cloud_resource_manager-1.17.0.tar.gz", hash = "sha256:0f486b62e2c58ff992a3a50fa0f4a96eef7750aa6c971bb373398ccb91828660", size = 464971, upload-time = "2026-03-26T22:17:29.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/f7/661d7a9023e877a226b5683429c3662f75a29ef45cb1464cf39adb689218/google_cloud_resource_manager-1.17.0-py3-none-any.whl", hash = "sha256:e479baf4b014a57f298e01b8279e3290b032e3476d69c8e5e1427af8f82739a5", size = 404403, upload-time = "2026-03-26T22:15:26.57Z" }, -] - -[[package]] -name = "google-cloud-storage" -version = "3.10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core" }, - { name = "google-auth" }, - { name = "google-cloud-core" }, - { name = "google-crc32c" }, - { name = "google-resumable-media" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4c/47/205eb8e9a1739b5345843e5a425775cbdc472cc38e7eda082ba5b8d02450/google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286", size = 17309950, upload-time = "2026-03-23T09:35:23.409Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/ff/ca9ab2417fa913d75aae38bf40bf856bb2749a604b2e0f701b37cfcd23cc/google_cloud_storage-3.10.1-py3-none-any.whl", hash = "sha256:a72f656759b7b99bda700f901adcb3425a828d4a29f911bc26b3ea79c5b1217f", size = 324453, upload-time = "2026-03-23T09:35:21.368Z" }, -] - -[[package]] -name = "google-crc32c" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, - { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, - { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, - { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, - { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, - { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, - { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, - { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, - { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, - { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, - { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, - { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, - { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, - { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, - { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, -] - -[[package]] -name = "google-genai" -version = "1.72.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "google-auth", extra = ["requests"] }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "sniffio" }, - { name = "tenacity" }, - { name = "typing-extensions" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3c/20/2aff5ea3cd7459f85101d119c136d9ca4369fcda3dcf0cfee89b305611a4/google_genai-1.72.0.tar.gz", hash = "sha256:abe7d3aecfafb464b904e3a09c81b626fb425e160e123e71a5125a7021cea7b2", size = 522844, upload-time = "2026-04-09T21:35:46.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/3d/9f70246114cdf56a2615a40428ced08bc844f5a26247fe812b2f0dd4eaca/google_genai-1.72.0-py3-none-any.whl", hash = "sha256:ea861e4c6946e3185c24b40d95503e088fc230a73a71fec0ef78164b369a8489", size = 764230, upload-time = "2026-04-09T21:35:44.587Z" }, -] - -[[package]] -name = "google-resumable-media" -version = "2.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-crc32c" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3f/d1/b1ea14b93b6b78f57fc580125de44e9f593ab88dd2460f1a8a8d18f74754/google_resumable_media-2.8.2.tar.gz", hash = "sha256:f3354a182ebd193ae3f42e3ef95e6c9b10f128320de23ac7637236713b1acd70", size = 2164510, upload-time = "2026-03-30T23:34:25.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f8/50bfaf4658431ff9de45c5c3935af7ab01157a4903c603cd0eee6e78e087/google_resumable_media-2.8.2-py3-none-any.whl", hash = "sha256:82b6d8ccd11765268cdd2a2123f417ec806b8eef3000a9a38dfe3033da5fb220", size = 81511, upload-time = "2026-03-30T23:34:09.671Z" }, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.74.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, -] - -[package.optional-dependencies] -grpc = [ - { name = "grpcio" }, -] - [[package]] name = "griffelib" version = "2.0.0" @@ -1373,95 +810,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] -[[package]] -name = "grpc-google-iam-v1" -version = "0.14.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos", extra = ["grpc"] }, - { name = "grpcio" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/4f/d098419ad0bfc06c9ce440575f05aa22d8973b6c276e86ac7890093d3c37/grpc_google_iam_v1-0.14.4.tar.gz", hash = "sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038", size = 23706, upload-time = "2026-04-01T01:57:49.813Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/22/c2dd50c09bf679bd38173656cd4402d2511e563b33bc88f90009cf50613c/grpc_google_iam_v1-0.14.4-py3-none-any.whl", hash = "sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964", size = 32675, upload-time = "2026-04-01T01:57:47.69Z" }, -] - -[[package]] -name = "grpcio" -version = "1.80.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/cd/bb7b7e54084a344c03d68144450da7ddd5564e51a298ae1662de65f48e2d/grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c", size = 6050363, upload-time = "2026-03-30T08:46:20.894Z" }, - { url = "https://files.pythonhosted.org/packages/16/02/1417f5c3460dea65f7a2e3c14e8b31e77f7ffb730e9bfadd89eda7a9f477/grpcio-1.80.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7b641fc3f1dc647bfd80bd713addc68f6d145956f64677e56d9ebafc0bd72388", size = 12026037, upload-time = "2026-03-30T08:46:25.144Z" }, - { url = "https://files.pythonhosted.org/packages/43/98/c910254eedf2cae368d78336a2de0678e66a7317d27c02522392f949b5c6/grpcio-1.80.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33eb763f18f006dc7fee1e69831d38d23f5eccd15b2e0f92a13ee1d9242e5e02", size = 6602306, upload-time = "2026-03-30T08:46:27.593Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f8/88ca4e78c077b2b2113d95da1e1ab43efd43d723c9a0397d26529c2c1a56/grpcio-1.80.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:52d143637e3872633fc7dd7c3c6a1c84e396b359f3a72e215f8bf69fd82084fc", size = 7301535, upload-time = "2026-03-30T08:46:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/f9/96/f28660fe2fe0f153288bf4a04e4910b7309d442395135c88ed4f5b3b8b40/grpcio-1.80.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c51bf8ac4575af2e0678bccfb07e47321fc7acb5049b4482832c5c195e04e13a", size = 6808669, upload-time = "2026-03-30T08:46:31.984Z" }, - { url = "https://files.pythonhosted.org/packages/47/eb/3f68a5e955779c00aeef23850e019c1c1d0e032d90633ba49c01ad5a96e0/grpcio-1.80.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:50a9871536d71c4fba24ee856abc03a87764570f0c457dd8db0b4018f379fed9", size = 7409489, upload-time = "2026-03-30T08:46:34.684Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a7/d2f681a4bfb881be40659a309771f3bdfbfdb1190619442816c3f0ffc079/grpcio-1.80.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a72d84ad0514db063e21887fbacd1fd7acb4d494a564cae22227cd45c7fbf199", size = 8423167, upload-time = "2026-03-30T08:46:36.833Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/29b4589c204959aa35ce5708400a05bba72181807c45c47b3ec000c39333/grpcio-1.80.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7691a6788ad9196872f95716df5bc643ebba13c97140b7a5ee5c8e75d1dea81", size = 7846761, upload-time = "2026-03-30T08:46:40.091Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d2/ed143e097230ee121ac5848f6ff14372dba91289b10b536d54fb1b7cbae7/grpcio-1.80.0-cp310-cp310-win32.whl", hash = "sha256:46c2390b59d67f84e882694d489f5b45707c657832d7934859ceb8c33f467069", size = 4156534, upload-time = "2026-03-30T08:46:42.026Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c9/df8279bb49b29409995e95efa85b72973d62f8aeff89abee58c91f393710/grpcio-1.80.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc053420fc75749c961e2a4c906398d7c15725d36ccc04ae6d16093167223b58", size = 4889869, upload-time = "2026-03-30T08:46:44.219Z" }, - { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, - { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, - { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, - { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, - { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, - { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, - { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, - { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, - { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, - { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, - { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, - { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, - { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, - { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, - { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, - { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, - { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, - { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, - { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, - { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, - { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, - { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, - { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, - { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, -] - -[[package]] -name = "grpcio-status" -version = "1.80.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "grpcio" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/ed/105f619bdd00cb47a49aa2feea6232ea2bbb04199d52a22cc6a7d603b5cb/grpcio_status-1.80.0.tar.gz", hash = "sha256:df73802a4c89a3ea88aa2aff971e886fccce162bc2e6511408b3d67a144381cd", size = 13901, upload-time = "2026-03-30T08:54:34.784Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/80/58cd2dfc19a07d022abe44bde7c365627f6c7cb6f692ada6c65ca437d09a/grpcio_status-1.80.0-py3-none-any.whl", hash = "sha256:4b56990363af50dbf2c2ebb80f1967185c07d87aa25aa2bea45ddb75fc181dbe", size = 14638, upload-time = "2026-03-30T08:54:01.569Z" }, -] - [[package]] name = "gssapi" version = "1.11.1" @@ -1494,38 +842,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] -[[package]] -name = "hf-xet" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/08/23c84a26716382c89151b5b447b4beb19e3345f3a93d3b73009a71a57ad3/hf_xet-1.4.2.tar.gz", hash = "sha256:b7457b6b482d9e0743bd116363239b1fa904a5e65deede350fbc0c4ea67c71ea", size = 672357, upload-time = "2026-03-13T06:58:51.077Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/06/e8cf74c3c48e5485c7acc5a990d0d8516cdfb5fdf80f799174f1287cc1b5/hf_xet-1.4.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ac8202ae1e664b2c15cdfc7298cbb25e80301ae596d602ef7870099a126fcad4", size = 3796125, upload-time = "2026-03-13T06:58:33.177Z" }, - { url = "https://files.pythonhosted.org/packages/66/d4/b73ebab01cbf60777323b7de9ef05550790451eb5172a220d6b9845385ec/hf_xet-1.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d2f8ee39fa9fba9af929f8c0d0482f8ee6e209179ad14a909b6ad78ffcb7c81", size = 3555985, upload-time = "2026-03-13T06:58:31.797Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e7/ded6d1bd041c3f2bca9e913a0091adfe32371988e047dd3a68a2463c15a2/hf_xet-1.4.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4642a6cf249c09da8c1f87fe50b24b2a3450b235bf8adb55700b52f0ea6e2eb6", size = 4212085, upload-time = "2026-03-13T06:58:24.323Z" }, - { url = "https://files.pythonhosted.org/packages/97/c1/a0a44d1f98934f7bdf17f7a915b934f9fca44bb826628c553589900f6df8/hf_xet-1.4.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:769431385e746c92dc05492dde6f687d304584b89c33d79def8367ace06cb555", size = 3988266, upload-time = "2026-03-13T06:58:22.887Z" }, - { url = "https://files.pythonhosted.org/packages/7a/82/be713b439060e7d1f1d93543c8053d4ef2fe7e6922c5b31642eaa26f3c4b/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c9dd1c1bc4cc56168f81939b0e05b4c36dd2d28c13dc1364b17af89aa0082496", size = 4188513, upload-time = "2026-03-13T06:58:40.858Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/cbd4188b22abd80ebd0edbb2b3e87f2633e958983519980815fb8314eae5/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fca58a2ae4e6f6755cc971ac6fcdf777ea9284d7e540e350bb000813b9a3008d", size = 4428287, upload-time = "2026-03-13T06:58:42.601Z" }, - { url = "https://files.pythonhosted.org/packages/b2/4e/84e45b25e2e3e903ed3db68d7eafa96dae9a1d1f6d0e7fc85120347a852f/hf_xet-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:163aab46854ccae0ab6a786f8edecbbfbaa38fcaa0184db6feceebf7000c93c0", size = 3665574, upload-time = "2026-03-13T06:58:53.881Z" }, - { url = "https://files.pythonhosted.org/packages/ee/71/c5ac2b9a7ae39c14e91973035286e73911c31980fe44e7b1d03730c00adc/hf_xet-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:09b138422ecbe50fd0c84d4da5ff537d27d487d3607183cd10e3e53f05188e82", size = 3528760, upload-time = "2026-03-13T06:58:52.187Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0f/fcd2504015eab26358d8f0f232a1aed6b8d363a011adef83fe130bff88f7/hf_xet-1.4.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:949dcf88b484bb9d9276ca83f6599e4aa03d493c08fc168c124ad10b2e6f75d7", size = 3796493, upload-time = "2026-03-13T06:58:39.267Z" }, - { url = "https://files.pythonhosted.org/packages/82/56/19c25105ff81731ca6d55a188b5de2aa99d7a2644c7aa9de1810d5d3b726/hf_xet-1.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41659966020d59eb9559c57de2cde8128b706a26a64c60f0531fa2318f409418", size = 3555797, upload-time = "2026-03-13T06:58:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/8933c073186849b5e06762aa89847991d913d10a95d1603eb7f2c3834086/hf_xet-1.4.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c588e21d80010119458dd5d02a69093f0d115d84e3467efe71ffb2c67c19146", size = 4212127, upload-time = "2026-03-13T06:58:30.539Z" }, - { url = "https://files.pythonhosted.org/packages/eb/01/f89ebba4e369b4ed699dcb60d3152753870996f41c6d22d3d7cac01310e1/hf_xet-1.4.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a296744d771a8621ad1d50c098d7ab975d599800dae6d48528ba3944e5001ba0", size = 3987788, upload-time = "2026-03-13T06:58:29.139Z" }, - { url = "https://files.pythonhosted.org/packages/84/4d/8a53e5ffbc2cc33bbf755382ac1552c6d9af13f623ed125fe67cc3e6772f/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f563f7efe49588b7d0629d18d36f46d1658fe7e08dce3fa3d6526e1c98315e2d", size = 4188315, upload-time = "2026-03-13T06:58:48.017Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b8/b7a1c1b5592254bd67050632ebbc1b42cc48588bf4757cb03c2ef87e704a/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5b2e0132c56d7ee1bf55bdb638c4b62e7106f6ac74f0b786fed499d5548c5570", size = 4428306, upload-time = "2026-03-13T06:58:49.502Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0c/40779e45b20e11c7c5821a94135e0207080d6b3d76e7b78ccb413c6f839b/hf_xet-1.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2f45c712c2fa1215713db10df6ac84b49d0e1c393465440e9cb1de73ecf7bbf6", size = 3665826, upload-time = "2026-03-13T06:58:59.88Z" }, - { url = "https://files.pythonhosted.org/packages/51/4c/e2688c8ad1760d7c30f7c429c79f35f825932581bc7c9ec811436d2f21a0/hf_xet-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6d53df40616f7168abfccff100d232e9d460583b9d86fa4912c24845f192f2b8", size = 3529113, upload-time = "2026-03-13T06:58:58.491Z" }, - { url = "https://files.pythonhosted.org/packages/b4/86/b40b83a2ff03ef05c4478d2672b1fc2b9683ff870e2b25f4f3af240f2e7b/hf_xet-1.4.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:71f02d6e4cdd07f344f6844845d78518cc7186bd2bc52d37c3b73dc26a3b0bc5", size = 3800339, upload-time = "2026-03-13T06:58:36.245Z" }, - { url = "https://files.pythonhosted.org/packages/64/2e/af4475c32b4378b0e92a587adb1aa3ec53e3450fd3e5fe0372a874531c00/hf_xet-1.4.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e9b38d876e94d4bdcf650778d6ebbaa791dd28de08db9736c43faff06ede1b5a", size = 3559664, upload-time = "2026-03-13T06:58:34.787Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4c/781267da3188db679e601de18112021a5cb16506fe86b246e22c5401a9c4/hf_xet-1.4.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:77e8c180b7ef12d8a96739a4e1e558847002afe9ea63b6f6358b2271a8bdda1c", size = 4217422, upload-time = "2026-03-13T06:58:27.472Z" }, - { url = "https://files.pythonhosted.org/packages/68/47/d6cf4a39ecf6c7705f887a46f6ef5c8455b44ad9eb0d391aa7e8a2ff7fea/hf_xet-1.4.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3b3c6a882016b94b6c210957502ff7877802d0dbda8ad142c8595db8b944271", size = 3992847, upload-time = "2026-03-13T06:58:25.989Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ef/e80815061abff54697239803948abc665c6b1d237102c174f4f7a9a5ffc5/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d9a634cc929cfbaf2e1a50c0e532ae8c78fa98618426769480c58501e8c8ac2", size = 4193843, upload-time = "2026-03-13T06:58:44.59Z" }, - { url = "https://files.pythonhosted.org/packages/54/75/07f6aa680575d9646c4167db6407c41340cbe2357f5654c4e72a1b01ca14/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b0932eb8b10317ea78b7da6bab172b17be03bbcd7809383d8d5abd6a2233e04", size = 4432751, upload-time = "2026-03-13T06:58:46.533Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/193eabd7e7d4b903c4aa983a215509c6114915a5a237525ec562baddb868/hf_xet-1.4.2-cp37-abi3-win_amd64.whl", hash = "sha256:ad185719fb2e8ac26f88c8100562dbf9dbdcc3d9d2add00faa94b5f106aea53f", size = 3671149, upload-time = "2026-03-13T06:58:57.07Z" }, - { url = "https://files.pythonhosted.org/packages/b4/7e/ccf239da366b37ba7f0b36095450efae4a64980bdc7ec2f51354205fdf39/hf_xet-1.4.2-cp37-abi3-win_arm64.whl", hash = "sha256:32c012286b581f783653e718c1862aea5b9eb140631685bb0c5e7012c8719a87", size = 3533426, upload-time = "2026-03-13T06:58:55.46Z" }, -] - [[package]] name = "httpcore" version = "1.0.9" @@ -1563,26 +879,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] -[[package]] -name = "huggingface-hub" -version = "1.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "httpx" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "tqdm" }, - { name = "typer" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/15/eafc1c57bf0f8afffb243dcd4c0cceb785e956acc17bba4d9bf2ae21fc9c/huggingface_hub-1.7.2.tar.gz", hash = "sha256:7f7e294e9bbb822e025bdb2ada025fa4344d978175a7f78e824d86e35f7ab43b", size = 724684, upload-time = "2026-03-20T10:36:08.767Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/de/3ad061a05f74728927ded48c90b73521b9a9328c85d841bdefb30e01fb85/huggingface_hub-1.7.2-py3-none-any.whl", hash = "sha256:288f33a0a17b2a73a1359e2a5fd28d1becb2c121748c6173ab8643fb342c850e", size = 618036, upload-time = "2026-03-20T10:36:06.824Z" }, -] - [[package]] name = "idna" version = "3.11" @@ -1786,103 +1082,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "jiter" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, - { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, - { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, - { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, - { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, - { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, - { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, - { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, - { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, - { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, -] - [[package]] name = "jsonref" version = "1.1.0" @@ -1958,14 +1157,14 @@ dependencies = [ { name = "asyncssh", extra = ["bcrypt"] }, { name = "fakeredis" }, { name = "fastmcp" }, - { name = "litellm" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "requests" }, ] [package.optional-dependencies] gcp = [ - { name = "google-cloud-aiplatform" }, + { name = "google-auth" }, ] gssapi = [ { name = "gssapi" }, @@ -2008,11 +1207,11 @@ requires-dist = [ { name = "asyncssh", extras = ["bcrypt"], specifier = ">=2.22.0" }, { name = "fakeredis", specifier = "<2.35" }, { name = "fastmcp", specifier = ">=3.2.4" }, - { name = "google-cloud-aiplatform", marker = "extra == 'gcp'", specifier = ">=1.147.0" }, + { name = "google-auth", marker = "extra == 'gcp'", specifier = ">=2.40.0" }, { name = "gssapi", marker = "extra == 'gssapi'", specifier = ">=1.11.1" }, - { name = "litellm", specifier = ">=1.80.16" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic-settings", specifier = ">=2.12.0" }, + { name = "requests", specifier = ">=2.32.0" }, ] provides-extras = ["gcp", "gssapi"] @@ -2046,29 +1245,6 @@ test = [ { name = "pytest-randomly" }, ] -[[package]] -name = "litellm" -version = "1.83.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "click" }, - { name = "fastuuid" }, - { name = "httpx" }, - { name = "importlib-metadata" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "tiktoken" }, - { name = "tokenizers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/92/6ce9737554994ca8e536e5f4f6a87cc7c4774b656c9eb9add071caf7d54b/litellm-1.83.0.tar.gz", hash = "sha256:860bebc76c4bb27b4cf90b4a77acd66dba25aced37e3db98750de8a1766bfb7a", size = 17333062, upload-time = "2026-03-31T05:08:25.331Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/2c/a670cc050fcd6f45c6199eb99e259c73aea92edba8d5c2fc1b3686d36217/litellm-1.83.0-py3-none-any.whl", hash = "sha256:88c536d339248f3987571493015784671ba3f193a328e1ea6780dbebaa2094a8", size = 15610306, upload-time = "2026-03-31T05:08:21.987Z" }, -] - [[package]] name = "markdown" version = "3.10.2" @@ -2359,144 +1535,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] -[[package]] -name = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, - { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, - { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, - { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, - { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, - { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, - { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, - { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, - { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, - { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, - { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, - { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, - { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, - { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, - { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, - { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, - { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, - { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, - { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, - { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, - { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, - { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, - { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, - { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, - { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, - { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, - { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, - { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, - { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, - { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, -] - [[package]] name = "nodeenv" version = "1.10.0" @@ -2506,25 +1544,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] -[[package]] -name = "openai" -version = "2.29.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/15/203d537e58986b5673e7f232453a2a2f110f22757b15921cbdeea392e520/openai-2.29.0.tar.gz", hash = "sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec", size = 671128, upload-time = "2026-03-17T17:53:49.599Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533, upload-time = "2026-03-17T17:53:47.348Z" }, -] - [[package]] name = "openapi-pydantic" version = "0.5.1" @@ -2646,147 +1665,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] -[[package]] -name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, - { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, - { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, - { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, - { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, - { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, - { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, - { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, -] - -[[package]] -name = "proto-plus" -version = "1.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" }, -] - -[[package]] -name = "protobuf" -version = "6.33.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, - { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, - { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, - { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, - { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, -] - [[package]] name = "ptyprocess" version = "0.7.0" @@ -3088,16 +1966,16 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "1.4.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] [[package]] @@ -3301,127 +2179,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] -[[package]] -name = "regex" -version = "2026.2.28" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/b8/845a927e078f5e5cc55d29f57becbfde0003d52806544531ab3f2da4503c/regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", size = 488461, upload-time = "2026-02-28T02:15:48.405Z" }, - { url = "https://files.pythonhosted.org/packages/32/f9/8a0034716684e38a729210ded6222249f29978b24b684f448162ef21f204/regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", size = 290774, upload-time = "2026-02-28T02:15:51.738Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ba/b27feefffbb199528dd32667cd172ed484d9c197618c575f01217fbe6103/regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", size = 288737, upload-time = "2026-02-28T02:15:53.534Z" }, - { url = "https://files.pythonhosted.org/packages/18/c5/65379448ca3cbfe774fcc33774dc8295b1ee97dc3237ae3d3c7b27423c9d/regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", size = 782675, upload-time = "2026-02-28T02:15:55.488Z" }, - { url = "https://files.pythonhosted.org/packages/aa/30/6fa55bef48090f900fbd4649333791fc3e6467380b9e775e741beeb3231f/regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", size = 850514, upload-time = "2026-02-28T02:15:57.509Z" }, - { url = "https://files.pythonhosted.org/packages/a9/28/9ca180fb3787a54150209754ac06a42409913571fa94994f340b3bba4e1e/regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", size = 896612, upload-time = "2026-02-28T02:15:59.682Z" }, - { url = "https://files.pythonhosted.org/packages/46/b5/f30d7d3936d6deecc3ea7bea4f7d3c5ee5124e7c8de372226e436b330a55/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", size = 791691, upload-time = "2026-02-28T02:16:01.752Z" }, - { url = "https://files.pythonhosted.org/packages/f5/34/96631bcf446a56ba0b2a7f684358a76855dfe315b7c2f89b35388494ede0/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", size = 783111, upload-time = "2026-02-28T02:16:03.651Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/f95cb7a85fe284d41cd2f3625e0f2ae30172b55dfd2af1d9b4eaef6259d7/regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", size = 767512, upload-time = "2026-02-28T02:16:05.616Z" }, - { url = "https://files.pythonhosted.org/packages/3d/af/a650f64a79c02a97f73f64d4e7fc4cc1984e64affab14075e7c1f9a2db34/regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", size = 773920, upload-time = "2026-02-28T02:16:08.325Z" }, - { url = "https://files.pythonhosted.org/packages/72/f8/3f9c2c2af37aedb3f5a1e7227f81bea065028785260d9cacc488e43e6997/regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", size = 846681, upload-time = "2026-02-28T02:16:10.381Z" }, - { url = "https://files.pythonhosted.org/packages/54/12/8db04a334571359f4d127d8f89550917ec6561a2fddfd69cd91402b47482/regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", size = 755565, upload-time = "2026-02-28T02:16:11.972Z" }, - { url = "https://files.pythonhosted.org/packages/da/bc/91c22f384d79324121b134c267a86ca90d11f8016aafb1dc5bee05890ee3/regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", size = 835789, upload-time = "2026-02-28T02:16:14.036Z" }, - { url = "https://files.pythonhosted.org/packages/46/a7/4cc94fd3af01dcfdf5a9ed75c8e15fd80fcd62cc46da7592b1749e9c35db/regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", size = 780094, upload-time = "2026-02-28T02:16:15.468Z" }, - { url = "https://files.pythonhosted.org/packages/3c/21/e5a38f420af3c77cab4a65f0c3a55ec02ac9babf04479cfd282d356988a6/regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", size = 266025, upload-time = "2026-02-28T02:16:16.828Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0a/205c4c1466a36e04d90afcd01d8908bac327673050c7fe316b2416d99d3d/regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", size = 277965, upload-time = "2026-02-28T02:16:18.752Z" }, - { url = "https://files.pythonhosted.org/packages/c3/4d/29b58172f954b6ec2c5ed28529a65e9026ab96b4b7016bcd3858f1c31d3c/regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", size = 270336, upload-time = "2026-02-28T02:16:20.735Z" }, - { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, - { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, - { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, - { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, - { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, - { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, - { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, - { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, - { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, - { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, - { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, - { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, - { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, - { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, - { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, - { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, - { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, - { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, - { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, - { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, - { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, - { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, - { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, - { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, - { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, - { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, - { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, - { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, - { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, - { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, - { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, - { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, - { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, - { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, - { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, - { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, - { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, - { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, - { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, - { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, - { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, - { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, - { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, - { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, - { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, - { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, - { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, - { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, - { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, - { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, - { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, - { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, - { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, - { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, - { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, -] - [[package]] name = "requests" version = "2.32.5" @@ -3632,15 +2389,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, ] -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, -] - [[package]] name = "shiv" version = "1.0.8" @@ -3664,15 +2412,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - [[package]] name = "sortedcontainers" version = "2.4.0" @@ -3722,106 +2461,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] -[[package]] -name = "tenacity" -version = "9.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, -] - -[[package]] -name = "tiktoken" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, - { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, - { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, - { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, - { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, - { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, -] - -[[package]] -name = "tokenizers" -version = "0.22.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, - { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, - { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, - { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, - { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, - { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, - { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, - { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, - { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, - { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, - { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, -] - [[package]] name = "tomli" version = "2.4.0" @@ -3876,18 +2515,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, -] - [[package]] name = "traitlets" version = "5.14.3" @@ -3897,21 +2524,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] -[[package]] -name = "typer" -version = "0.24.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -4177,146 +2789,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] -[[package]] -name = "yarl" -version = "1.23.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, - { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, - { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, - { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, - { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, - { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, - { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, - { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, - { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, - { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, - { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, - { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, - { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, - { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, - { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, - { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, - { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, - { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, - { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, - { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, - { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, - { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, - { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, - { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, - { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, - { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, - { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, -] - [[package]] name = "zipp" version = "3.23.0" From 38913ed5d13aad6f3fa38803d0501b2eb47b3567 Mon Sep 17 00:00:00 2001 From: Link Dupont Date: Mon, 15 Jun 2026 10:56:44 -0400 Subject: [PATCH 2/7] feat(gatekeeper): add Vertex AI support and refactor configuration Add first-class support for the Vertex AI provider in the gatekeeper system. The configuration structure has been refactored to accommodate separate settings for each provider type. --- docs/config-reference.md | 19 +++-- docs/guarded-command-execution.md | 13 ++- eval/gatekeeper/README.md | 4 + eval/gatekeeper/standard-evals.sh | 43 ++++------ src/linux_mcp_server/config.py | 52 ++++++------ .../gatekeeper/anthropic_client.py | 38 +-------- src/linux_mcp_server/gatekeeper/gcp_auth.py | 14 +++- .../gatekeeper/gemini_client.py | 38 ++------- src/linux_mcp_server/gatekeeper/http_utils.py | 39 +++++++-- src/linux_mcp_server/gatekeeper/llm.py | 7 +- .../gatekeeper/openai_client.py | 19 +++-- .../gatekeeper/openrouter_client.py | 18 +++- .../gatekeeper/vertex_ai_client.py | 84 +++++++++++++++++++ tests/gatekeeper/test_anthropic_client.py | 21 +---- tests/gatekeeper/test_check_run_script.py | 14 ++-- tests/gatekeeper/test_gcp_auth.py | 13 +-- tests/gatekeeper/test_gemini_client.py | 19 +---- tests/gatekeeper/test_http_utils.py | 33 ++++++++ tests/gatekeeper/test_llm.py | 15 +++- tests/gatekeeper/test_openai_client.py | 50 ++++------- tests/gatekeeper/test_openrouter_client.py | 7 +- tests/gatekeeper/test_vertex_ai_client.py | 82 ++++++++++++++++++ tests/test_config.py | 27 +++--- 23 files changed, 410 insertions(+), 259 deletions(-) create mode 100644 src/linux_mcp_server/gatekeeper/vertex_ai_client.py create mode 100644 tests/gatekeeper/test_vertex_ai_client.py diff --git a/docs/config-reference.md b/docs/config-reference.md index f5a285ca..7f13474d 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -58,18 +58,21 @@ These are used when `LINUX_MCP_TOOLSET` is set to `run_script` or `both`. | Option / Env Var | Default | Description | | ---------------- | ------- | ----------- | | `--always-confirm-scripts` / `--no-always-confirm-scripts`
`LINUX_MCP_ALWAYS_CONFIRM_SCRIPTS` | `False` | All scripts must be confirmed by the user | -| `--gatekeeper.provider`
`LINUX_MCP_GATEKEEPER__PROVIDER` | `openai` (inferred from model if unset) | LLM provider: `openai`, `anthropic`, `gemini`, or `openrouter` | -| `--gatekeeper.backend`
`LINUX_MCP_GATEKEEPER__BACKEND` | `direct` | API backend: `direct` or `vertex` (GCP/Vertex AI) | +| `--gatekeeper.provider`
`LINUX_MCP_GATEKEEPER__PROVIDER` | `openai` (inferred from model if unset) | LLM provider: `openai`, `anthropic`, `gemini`, `openrouter`, or `vertex_ai` | | `--gatekeeper.model`
`LINUX_MCP_GATEKEEPER__MODEL` | _(none)_ | Required: provider-native model ID (e.g. `gpt-5.4`, `claude-sonnet-4-6`, `gemini-2.0-flash`, `openai/gpt-oss-120b` for OpenRouter) | -| `--gatekeeper.quantization`
`LINUX_MCP_GATEKEEPER__QUANTIZATION` | _(none)_ | OpenRouter only: filter providers by quantization level (e.g. `fp4`, `bf16`) | -| `--gatekeeper.base_url`
`LINUX_MCP_GATEKEEPER__BASE_URL` / `OPENAI_API_BASE` | `https://api.openai.com/v1` | OpenAI-compatible API base URL (OpenAI provider only) | -| `--gatekeeper.project`
`LINUX_MCP_GATEKEEPER__PROJECT` / `VERTEXAI_PROJECT` | _(none)_ | GCP project for Vertex backends | -| `--gatekeeper.location`
`LINUX_MCP_GATEKEEPER__LOCATION` / `VERTEXAI_LOCATION` | `global` | GCP region for Vertex backends | | `--gatekeeper.reasoning_effort`
`LINUX_MCP_GATEKEEPER__REASONING_EFFORT` | _(model specific)_ | Reasoning effort (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`). Not all values are supported for all models. | | `--gatekeeper.structured_output`
`LINUX_MCP_GATEKEEPER__STRUCTURED_OUTPUT` | `True` | Whether to use structured JSON output from the model | | `--gatekeeper.temperature`
`LINUX_MCP_GATEKEEPER__TEMPERATURE` | 0.0 | Temperature to use for the model | -| `--gatekeeper.template_kwargs`
`LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS` | _(none)_ | _Not usually needed_ - Extra chat-template arguments for OpenAI-compatible servers (e.g. llama.cpp `enable_thinking`), sent as `chat_template_kwargs` on Chat Completions requests. JSON object, e.g. `{ "enable_thinking": false }` | -| Provider credentials | _(none)_ | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY` / `GEMINI_API_KEY`, or `OPENROUTER_API_KEY` for direct backends; `GOOGLE_APPLICATION_CREDENTIALS` for Vertex backends | +| `--gatekeeper.cost`
`LINUX_MCP_GATEKEEPER__COST` | _(none)_ | Gatekeeper cost for accounting (`input$/token:output$/token`) | +| `--gatekeeper.openai.base_url`
`LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL` / `OPENAI_API_BASE` | `https://api.openai.com/v1` | OpenAI provider: API base URL | +| `--gatekeeper.openai.template_kwargs`
`LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS` | _(none)_ | OpenAI provider: extra chat-template arguments (e.g. llama.cpp `enable_thinking`), sent as `chat_template_kwargs` | +| `--gatekeeper.openrouter.base_url`
`LINUX_MCP_GATEKEEPER__OPENROUTER__BASE_URL` | `https://openrouter.ai/api/v1` | OpenRouter provider: API base URL | +| `--gatekeeper.openrouter.quantization`
`LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION` | _(none)_ | OpenRouter provider: filter providers by quantization level (e.g. `fp4`, `bf16`) | +| `--gatekeeper.openrouter.template_kwargs`
`LINUX_MCP_GATEKEEPER__OPENROUTER__TEMPLATE_KWARGS` | _(none)_ | OpenRouter provider: extra chat-template arguments | +| `--gatekeeper.vertex_ai.project`
`LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT` / `VERTEXAI_PROJECT` | _(none)_ | Vertex AI provider: GCP project | +| `--gatekeeper.vertex_ai.location`
`LINUX_MCP_GATEKEEPER__VERTEX_AI__LOCATION` / `VERTEXAI_LOCATION` | `global` | Vertex AI provider: GCP region | +| `--gatekeeper.vertex_ai.base_url`
`LINUX_MCP_GATEKEEPER__VERTEX_AI__BASE_URL` | _(computed)_ | Vertex AI provider: override OpenAI-compatible MaaS endpoint URL (default derived from project/location) | +| Provider credentials | _(none)_ | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY` / `GEMINI_API_KEY`, or `OPENROUTER_API_KEY` for direct providers; `GOOGLE_APPLICATION_CREDENTIALS` for `vertex_ai` | ## Logging Configuration diff --git a/docs/guarded-command-execution.md b/docs/guarded-command-execution.md index 61d2345d..5fb5c623 100644 --- a/docs/guarded-command-execution.md +++ b/docs/guarded-command-execution.md @@ -108,7 +108,7 @@ LINUX_MCP_TOOLSET=run_script Set `LINUX_MCP_GATEKEEPER__PROVIDER` and `LINUX_MCP_GATEKEEPER__MODEL` to configure the gatekeeper. Set the matching API credentials for your provider (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, -`GOOGLE_API_KEY`, or `OPENROUTER_API_KEY`). For Vertex AI backends, install the `gcp` extra and configure +`GOOGLE_API_KEY`, or `OPENROUTER_API_KEY`). For the `vertex_ai` provider, install the `gcp` extra and configure `GOOGLE_APPLICATION_CREDENTIALS`, `VERTEXAI_PROJECT`, and `VERTEXAI_LOCATION`. Example (OpenAI): @@ -132,10 +132,19 @@ Example (OpenRouter): ```sh LINUX_MCP_GATEKEEPER__PROVIDER=openrouter LINUX_MCP_GATEKEEPER__MODEL=openai/gpt-oss-120b -LINUX_MCP_GATEKEEPER__QUANTIZATION=fp4 +LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION=fp4 OPENROUTER_API_KEY=<....> ``` +Example (Vertex AI): + +```sh +LINUX_MCP_GATEKEEPER__PROVIDER=vertex_ai +LINUX_MCP_GATEKEEPER__MODEL=gemini-3.1-pro-preview +LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT=my-gcp-project +GOOGLE_APPLICATION_CREDENTIALS= +``` + **Configure your client's tool policy** diff --git a/eval/gatekeeper/README.md b/eval/gatekeeper/README.md index fa5c0ddd..c3511932 100644 --- a/eval/gatekeeper/README.md +++ b/eval/gatekeeper/README.md @@ -75,6 +75,10 @@ uv run eval/gatekeeper/run-eval.py testcases/selinux-port-denial.yaml -o results export OPENROUTER_API_KEY="..." ./eval/gatekeeper/standard-evals.sh --no-save gpt-oss-120b:low,fp4@openrouter +# Run via standard-evals.sh (Vertex AI example) +export VERTEXAI_PROJECT="my-gcp-project" +./eval/gatekeeper/standard-evals.sh --no-save gemini-3.1-pro-preview:low@vertex_ai + # Run all test case files in testcases/ uv run eval/gatekeeper/run-eval.py --all -o results.yaml diff --git a/eval/gatekeeper/standard-evals.sh b/eval/gatekeeper/standard-evals.sh index 89d2fd90..89f90725 100755 --- a/eval/gatekeeper/standard-evals.sh +++ b/eval/gatekeeper/standard-evals.sh @@ -263,25 +263,23 @@ case "$provider" in anthropic) : "${ANTHROPIC_API_KEY:?'api key must be set'}" LINUX_MCP_GATEKEEPER__PROVIDER=anthropic - LINUX_MCP_GATEKEEPER__BACKEND=direct LINUX_MCP_GATEKEEPER__MODEL="$model" ;; llama_cpp) OPENAI_API_KEY=tasty LINUX_MCP_GATEKEEPER__PROVIDER=openai - LINUX_MCP_GATEKEEPER__BACKEND=direct - LINUX_MCP_GATEKEEPER__BASE_URL=http://localhost:8080/v1 + LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL=http://localhost:8080/v1 # gemma-4 and qwen3.5 support enable_thinking via chat_template_kwargs; granite-4 has no control case "$reasoning" in none) - LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS='{ "enable_thinking": false }' + LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS='{ "enable_thinking": false }' ;; default) - LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS='{ "enable_thinking": true }' + LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS='{ "enable_thinking": true }' ;; esac export OPENAI_API_KEY - export LINUX_MCP_GATEKEEPER__BASE_URL + export LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL max_parallel=1 quant_tag="${quantization^^}" case "$model" in @@ -317,37 +315,25 @@ case "$provider" in : "${VERTEXAI_PROJECT:?'project must be set'}" uv_args+=("--extra" "gcp") max_parallel=50 - LINUX_MCP_GATEKEEPER__BACKEND=vertex - vertex_location="${VERTEXAI_LOCATION:-global}" - vertex_openapi_base="https://aiplatform.googleapis.com/v1/projects/${VERTEXAI_PROJECT}/locations/${vertex_location}/endpoints/openapi" + LINUX_MCP_GATEKEEPER__PROVIDER=vertex_ai + LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT="${VERTEXAI_PROJECT}" case $model in - claude-*) - LINUX_MCP_GATEKEEPER__PROVIDER=anthropic - LINUX_MCP_GATEKEEPER__MODEL="$model" - ;; - gemini-*) - LINUX_MCP_GATEKEEPER__PROVIDER=gemini + claude-*|gemini-*) LINUX_MCP_GATEKEEPER__MODEL="$model" ;; gemma-4-26b-a4b-it) if [[ $reasoning == "none" ]] ; then LINUX_MCP_GATEKEEPER__REASONING_EFFORT= fi - LINUX_MCP_GATEKEEPER__PROVIDER=openai LINUX_MCP_GATEKEEPER__MODEL="${model}-maas" - LINUX_MCP_GATEKEEPER__BASE_URL="${vertex_openapi_base}" LINUX_MCP_GATEKEEPER__COST=0.15e-6:0.60e-6 ;; gpt-oss-20b) - LINUX_MCP_GATEKEEPER__PROVIDER=openai LINUX_MCP_GATEKEEPER__MODEL="${model}-maas" - LINUX_MCP_GATEKEEPER__BASE_URL="${vertex_openapi_base}" LINUX_MCP_GATEKEEPER__COST="0.07e-6:0.25e-6" ;; gpt-oss-120b) - LINUX_MCP_GATEKEEPER__PROVIDER=openai LINUX_MCP_GATEKEEPER__MODEL="${model}-maas" - LINUX_MCP_GATEKEEPER__BASE_URL="${vertex_openapi_base}" LINUX_MCP_GATEKEEPER__COST="0.09e-6:0.36e-6" ;; esac @@ -357,7 +343,6 @@ case "$provider" in SSL_CERT_FILE=${SSL_CERT_FILE:-/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem} export SSL_CERT_FILE LINUX_MCP_GATEKEEPER__PROVIDER=openai - LINUX_MCP_GATEKEEPER__BACKEND=direct case $model in granite-*) LINUX_MCP_GATEKEEPER__MODEL="ibm-granite/$model" @@ -366,15 +351,14 @@ case "$provider" in LINUX_MCP_GATEKEEPER__MODEL="openai/$model" ;; esac - LINUX_MCP_GATEKEEPER__BASE_URL="$(get_MC_base_url "$model")" + LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL="$(get_MC_base_url "$model")" max_parallel=5 ;; openrouter) : "${OPENROUTER_API_KEY:?'api key must be set'}" LINUX_MCP_GATEKEEPER__PROVIDER=openrouter - LINUX_MCP_GATEKEEPER__BACKEND=direct if [[ -n $quantization ]] ; then - LINUX_MCP_GATEKEEPER__QUANTIZATION=$quantization + LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION=$quantization fi case $model in claude-*) @@ -399,11 +383,12 @@ case "$provider" in ;; esac -export LINUX_MCP_GATEKEEPER__PROVIDER LINUX_MCP_GATEKEEPER__BACKEND LINUX_MCP_GATEKEEPER__MODEL +export LINUX_MCP_GATEKEEPER__PROVIDER LINUX_MCP_GATEKEEPER__MODEL export LINUX_MCP_GATEKEEPER__COST LINUX_MCP_GATEKEEPER__REASONING_EFFORT -[[ -n "${LINUX_MCP_GATEKEEPER__QUANTIZATION:-}" ]] && export LINUX_MCP_GATEKEEPER__QUANTIZATION -[[ -n "${LINUX_MCP_GATEKEEPER__BASE_URL:-}" ]] && export LINUX_MCP_GATEKEEPER__BASE_URL -[[ -n "${LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS:-}" ]] && export LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS +[[ -n "${LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION:-}" ]] && export LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION +[[ -n "${LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL:-}" ]] && export LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL +[[ -n "${LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS:-}" ]] && export LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS +[[ -n "${LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT:-}" ]] && export LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT variant_suffix="" if [[ -n "$variant" ]] ; then diff --git a/src/linux_mcp_server/config.py b/src/linux_mcp_server/config.py index eca506f2..502f774d 100644 --- a/src/linux_mcp_server/config.py +++ b/src/linux_mcp_server/config.py @@ -55,13 +55,7 @@ class GatekeeperProvider(StrEnum): ANTHROPIC = "anthropic" GEMINI = "gemini" OPENROUTER = "openrouter" - - -class GatekeeperBackend(StrEnum): - """API backend for the gatekeeper provider.""" - - DIRECT = "direct" - VERTEX = "vertex" + VERTEX_AI = "vertex_ai" class AuthProvider(StrEnum): @@ -129,39 +123,43 @@ def parse_cost(v: Any) -> Any: return v -class GatekeeperConfig(BaseSettings): - """Gatekeeper Model configuration""" +class OpenAIGatekeeperConfig(BaseSettings): + """OpenAI gatekeeper provider configuration.""" - provider: GatekeeperProvider | None = None - backend: GatekeeperBackend = GatekeeperBackend.DIRECT - model: str | None = None + base_url: str | None = None + template_kwargs: dict[str, Any] = Field(default_factory=dict) - # Model quantization for OpenRouter provider routing (e.g. fp8, bf16) - quantization: str | None = None - # OpenAI-compatible API base URL (OpenAI provider only) +class OpenRouterGatekeeperConfig(BaseSettings): + """OpenRouter gatekeeper provider configuration.""" + base_url: str | None = None + quantization: str | None = None + template_kwargs: dict[str, Any] = Field(default_factory=dict) + + +class VertexAIGatekeeperConfig(BaseSettings): + """Vertex AI gatekeeper provider configuration.""" - # GCP project and region for Vertex backends project: str | None = None location: str | None = None + base_url: str | None = None - # reasoning effort - reasoning_effort: ReasoningEffort | None = None - # Whether we should use structured output - structured_output: bool = True - - # Extra chat-template arguments for OpenAI-compatible servers (e.g. llama.cpp enable_thinking). - # Passed as chat_template_kwargs on Chat Completions requests. - template_kwargs: dict[str, Any] = Field(default_factory=dict) +class GatekeeperConfig(BaseSettings): + """Gatekeeper Model configuration""" - # Temperature for gatekeeper model + provider: GatekeeperProvider | None = None + model: str | None = None + reasoning_effort: ReasoningEffort | None = None + structured_output: bool = True temperature: float = 0.0 - - # Gatekeeper cost for accounting (input $/token, output $/token) cost: Annotated[tuple[float, float] | None, BeforeValidator(parse_cost)] = None + openai: OpenAIGatekeeperConfig | None = None + openrouter: OpenRouterGatekeeperConfig | None = None + vertex_ai: VertexAIGatekeeperConfig | None = None + class Config(BaseSettings): # The '_' is required in the env_prefix, otherwise, pydantic would diff --git a/src/linux_mcp_server/gatekeeper/anthropic_client.py b/src/linux_mcp_server/gatekeeper/anthropic_client.py index 9d28fafe..95ce4bb5 100644 --- a/src/linux_mcp_server/gatekeeper/anthropic_client.py +++ b/src/linux_mcp_server/gatekeeper/anthropic_client.py @@ -3,15 +3,10 @@ from typing import Any from linux_mcp_server.config import CONFIG -from linux_mcp_server.config import GatekeeperBackend -from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_access_token -from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_location -from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_API_URL from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_API_VERSION from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_DEFAULT_MAX_TOKENS from linux_mcp_server.gatekeeper.http_utils import anthropic_thinking_block -from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_VERTEX_VERSION from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS from linux_mcp_server.gatekeeper.http_utils import get_anthropic_api_key from linux_mcp_server.gatekeeper.http_utils import normalize_model_id @@ -20,7 +15,7 @@ from linux_mcp_server.gatekeeper.schema import anthropic_output_config -def _build_messages_body(prompt: str, *, include_model: bool) -> dict[str, Any]: +def build_messages_body(prompt: str, *, include_model: bool) -> dict[str, Any]: body: dict[str, Any] = { "max_tokens": ANTHROPIC_DEFAULT_MAX_TOKENS, "messages": [{"role": "user", "content": prompt}], @@ -36,14 +31,7 @@ def _build_messages_body(prompt: str, *, include_model: bool) -> dict[str, Any]: return body -def _vertex_url(model: str) -> str: - project = get_gcp_project() - location = get_gcp_location() - host = "aiplatform.googleapis.com" if location == "global" else f"{location}-aiplatform.googleapis.com" - return f"https://{host}/v1/projects/{project}/locations/{location}/publishers/anthropic/models/{model}:rawPredict" - - -def _extract_messages_text(response: dict[str, Any]) -> str: +def extract_messages_text(response: dict[str, Any]) -> str: for item in response.get("content", []): if isinstance(item, dict) and item.get("type") == "text": text = item.get("text") @@ -53,24 +41,6 @@ def _extract_messages_text(response: dict[str, Any]) -> str: def complete_anthropic(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: - model = normalize_model_id(CONFIG.gatekeeper.model or "") - - if CONFIG.gatekeeper.backend == GatekeeperBackend.VERTEX: - body = _build_messages_body(prompt, include_model=False) - body["anthropic_version"] = ANTHROPIC_VERTEX_VERSION - headers = { - "Authorization": f"Bearer {get_gcp_access_token()}", - "Content-Type": "application/json", - } - response = post_json( - provider="anthropic", - url=_vertex_url(model), - headers=headers, - body=body, - timeout=timeout, - ) - return GatekeeperCompletion(text=_extract_messages_text(response)) - headers = { "x-api-key": get_anthropic_api_key(), "anthropic-version": ANTHROPIC_API_VERSION, @@ -80,7 +50,7 @@ def complete_anthropic(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) - provider="anthropic", url=ANTHROPIC_API_URL, headers=headers, - body=_build_messages_body(prompt, include_model=True), + body=build_messages_body(prompt, include_model=True), timeout=timeout, ) - return GatekeeperCompletion(text=_extract_messages_text(response)) + return GatekeeperCompletion(text=extract_messages_text(response)) diff --git a/src/linux_mcp_server/gatekeeper/gcp_auth.py b/src/linux_mcp_server/gatekeeper/gcp_auth.py index a7657f36..c6389246 100644 --- a/src/linux_mcp_server/gatekeeper/gcp_auth.py +++ b/src/linux_mcp_server/gatekeeper/gcp_auth.py @@ -10,17 +10,23 @@ class GCPAuthError(RuntimeError): def get_gcp_project() -> str: - project = CONFIG.gatekeeper.project or os.environ.get("VERTEXAI_PROJECT") or os.environ.get("GOOGLE_CLOUD_PROJECT") + vertex_ai = CONFIG.gatekeeper.vertex_ai + project = ( + (vertex_ai.project if vertex_ai else None) + or os.environ.get("VERTEXAI_PROJECT") + or os.environ.get("GOOGLE_CLOUD_PROJECT") + ) if not project: raise GCPAuthError( - "Vertex backend requires a GCP project. Set VERTEXAI_PROJECT, GOOGLE_CLOUD_PROJECT, " - "or LINUX_MCP_GATEKEEPER__PROJECT." + "Vertex AI provider requires a GCP project. Set VERTEXAI_PROJECT, GOOGLE_CLOUD_PROJECT, " + "or LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT." ) return project def get_gcp_location() -> str: - return CONFIG.gatekeeper.location or os.environ.get("VERTEXAI_LOCATION") or "global" + vertex_ai = CONFIG.gatekeeper.vertex_ai + return (vertex_ai.location if vertex_ai else None) or os.environ.get("VERTEXAI_LOCATION") or "global" def get_gcp_access_token() -> str: diff --git a/src/linux_mcp_server/gatekeeper/gemini_client.py b/src/linux_mcp_server/gatekeeper/gemini_client.py index 47fe5274..30cb3136 100644 --- a/src/linux_mcp_server/gatekeeper/gemini_client.py +++ b/src/linux_mcp_server/gatekeeper/gemini_client.py @@ -3,10 +3,6 @@ from typing import Any from linux_mcp_server.config import CONFIG -from linux_mcp_server.config import GatekeeperBackend -from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_access_token -from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_location -from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS from linux_mcp_server.gatekeeper.http_utils import gemini_thinking_level from linux_mcp_server.gatekeeper.http_utils import get_google_api_key @@ -17,7 +13,7 @@ from linux_mcp_server.gatekeeper.schema import gemini_generation_config -def _build_body(prompt: str) -> dict[str, Any]: +def build_gemini_body(prompt: str) -> dict[str, Any]: generation_config = gemini_generation_config( temperature=CONFIG.gatekeeper.temperature, structured_output=CONFIG.gatekeeper.structured_output, @@ -31,14 +27,7 @@ def _build_body(prompt: str) -> dict[str, Any]: } -def _vertex_url(model: str) -> str: - project = get_gcp_project() - location = get_gcp_location() - host = "aiplatform.googleapis.com" if location == "global" else f"{location}-aiplatform.googleapis.com" - return f"https://{host}/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent" - - -def _extract_text(response: dict[str, Any]) -> str: +def extract_gemini_text(response: dict[str, Any]) -> str: candidates = response.get("candidates", []) if not candidates: return "" @@ -52,29 +41,12 @@ def _extract_text(response: dict[str, Any]) -> str: def complete_gemini(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: model = normalize_model_id(CONFIG.gatekeeper.model or "") - body = _build_body(prompt) - - if CONFIG.gatekeeper.backend == GatekeeperBackend.VERTEX: - headers = { - "Authorization": f"Bearer {get_gcp_access_token()}", - "Content-Type": "application/json", - } - response = post_json( - provider="gemini", - url=_vertex_url(model), - headers=headers, - body=body, - timeout=timeout, - ) - return GatekeeperCompletion(text=_extract_text(response)) - api_key = get_google_api_key() - headers = {"Content-Type": "application/json"} response = post_json( provider="gemini", url=f"{GOOGLE_AI_BASE_URL}/models/{model}:generateContent?key={api_key}", - headers=headers, - body=body, + headers={"Content-Type": "application/json"}, + body=build_gemini_body(prompt), timeout=timeout, ) - return GatekeeperCompletion(text=_extract_text(response)) + return GatekeeperCompletion(text=extract_gemini_text(response)) diff --git a/src/linux_mcp_server/gatekeeper/http_utils.py b/src/linux_mcp_server/gatekeeper/http_utils.py index 2763bc3a..4b03150f 100644 --- a/src/linux_mcp_server/gatekeeper/http_utils.py +++ b/src/linux_mcp_server/gatekeeper/http_utils.py @@ -3,12 +3,12 @@ import os from typing import Any +from typing import Literal from urllib.parse import urlparse import requests from linux_mcp_server.config import CONFIG -from linux_mcp_server.config import GatekeeperBackend from linux_mcp_server.config import ReasoningEffort @@ -51,7 +51,8 @@ def post_json( def get_openai_base_url() -> str: - return (CONFIG.gatekeeper.base_url or os.environ.get("OPENAI_API_BASE") or OPENAI_DEFAULT_BASE_URL).rstrip("/") + configured = CONFIG.gatekeeper.openai.base_url if CONFIG.gatekeeper.openai else None + return (configured or os.environ.get("OPENAI_API_BASE") or OPENAI_DEFAULT_BASE_URL).rstrip("/") def prefers_openai_chat_completions(base_url: str) -> bool: @@ -74,7 +75,35 @@ def normalize_openrouter_model_id(model: str) -> str: def get_openrouter_base_url() -> str: - return (CONFIG.gatekeeper.base_url or OPENROUTER_DEFAULT_BASE_URL).rstrip("/") + configured = CONFIG.gatekeeper.openrouter.base_url if CONFIG.gatekeeper.openrouter else None + return (configured or OPENROUTER_DEFAULT_BASE_URL).rstrip("/") + + +def vertex_api_style(model: str) -> Literal["anthropic", "gemini", "openai_compatible"]: + normalized = normalize_model_id(model) + if normalized.startswith("claude"): + return "anthropic" + if normalized.startswith("gemini"): + return "gemini" + return "openai_compatible" + + +def get_vertex_openapi_base_url() -> str: + cfg = CONFIG.gatekeeper.vertex_ai + if cfg and cfg.base_url: + return cfg.base_url.rstrip("/") + from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_location + from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project + + project = get_gcp_project() + location = get_gcp_location() + return f"https://aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/endpoints/openapi" + + +def vertex_auth_headers() -> dict[str, str]: + from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_access_token + + return {"Authorization": f"Bearer {get_gcp_access_token()}"} def openai_reasoning_block(reasoning_effort: ReasoningEffort | None) -> dict[str, Any] | None: @@ -153,8 +182,4 @@ def get_google_api_key() -> str: def openai_auth_headers() -> dict[str, str]: - if CONFIG.gatekeeper.backend == GatekeeperBackend.VERTEX: - from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_access_token - - return {"Authorization": f"Bearer {get_gcp_access_token()}"} return {"Authorization": f"Bearer {get_openai_api_key()}"} diff --git a/src/linux_mcp_server/gatekeeper/llm.py b/src/linux_mcp_server/gatekeeper/llm.py index 8c2307be..0a795aaa 100644 --- a/src/linux_mcp_server/gatekeeper/llm.py +++ b/src/linux_mcp_server/gatekeeper/llm.py @@ -10,6 +10,7 @@ from linux_mcp_server.gatekeeper.gemini_client import complete_gemini from linux_mcp_server.gatekeeper.openai_client import complete_openai from linux_mcp_server.gatekeeper.openrouter_client import complete_openrouter +from linux_mcp_server.gatekeeper.vertex_ai_client import complete_vertex_ai logger = logging.getLogger("linux-mcp-server") @@ -27,10 +28,8 @@ def _infer_provider_from_model(model: str) -> GatekeeperProvider: return GatekeeperProvider.OPENROUTER if model.startswith("anthropic/") or model.startswith("claude"): return GatekeeperProvider.ANTHROPIC - if model.startswith("vertex_ai/gemini") or model.startswith("gemini"): + if model.startswith("gemini"): return GatekeeperProvider.GEMINI - if model.startswith("vertex_ai/anthropic"): - return GatekeeperProvider.ANTHROPIC return GatekeeperProvider.OPENAI @@ -53,6 +52,8 @@ def complete_gatekeeper(prompt: str) -> GatekeeperCompletion: completion = complete_gemini(prompt) case GatekeeperProvider.OPENROUTER: completion = complete_openrouter(prompt) + case GatekeeperProvider.VERTEX_AI: + completion = complete_vertex_ai(prompt) case _: # pragma: no cover raise ValueError(f"Unsupported gatekeeper provider: {provider}") diff --git a/src/linux_mcp_server/gatekeeper/openai_client.py b/src/linux_mcp_server/gatekeeper/openai_client.py index da39999f..988f1397 100644 --- a/src/linux_mcp_server/gatekeeper/openai_client.py +++ b/src/linux_mcp_server/gatekeeper/openai_client.py @@ -16,10 +16,17 @@ from linux_mcp_server.gatekeeper.schema import openai_text_format +def _openai_template_kwargs() -> dict[str, Any]: + if CONFIG.gatekeeper.openai is None: + return {} + return CONFIG.gatekeeper.openai.template_kwargs + + def _apply_chat_completions_extras(body: dict[str, Any]) -> dict[str, Any]: """Merge template_kwargs into Chat Completions bodies (llama.cpp, etc.).""" - if CONFIG.gatekeeper.template_kwargs: - body["chat_template_kwargs"] = CONFIG.gatekeeper.template_kwargs + template_kwargs = _openai_template_kwargs() + if template_kwargs: + body["chat_template_kwargs"] = template_kwargs return body @@ -38,7 +45,7 @@ def _build_responses_body(prompt: str) -> dict[str, Any]: return body -def _build_chat_completions_body(prompt: str) -> dict[str, Any]: +def build_chat_completions_body(prompt: str) -> dict[str, Any]: body: dict[str, Any] = { "model": normalize_model_id(CONFIG.gatekeeper.model or ""), "messages": [{"role": "user", "content": prompt}], @@ -72,7 +79,7 @@ def _extract_responses_text(response: dict[str, Any]) -> str: return "".join(chunks).strip() -def _extract_chat_completions_text(response: dict[str, Any]) -> str: +def extract_chat_completions_text(response: dict[str, Any]) -> str: choices = response.get("choices", []) if not choices: return "" @@ -107,7 +114,7 @@ def complete_openai(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> G provider="openai", url=f"{base_url}/chat/completions", headers=headers, - body=_build_chat_completions_body(prompt), + body=build_chat_completions_body(prompt), timeout=timeout, ) - return GatekeeperCompletion(text=_extract_chat_completions_text(response)) + return GatekeeperCompletion(text=extract_chat_completions_text(response)) diff --git a/src/linux_mcp_server/gatekeeper/openrouter_client.py b/src/linux_mcp_server/gatekeeper/openrouter_client.py index 27ae37f1..972ef7bd 100644 --- a/src/linux_mcp_server/gatekeeper/openrouter_client.py +++ b/src/linux_mcp_server/gatekeeper/openrouter_client.py @@ -13,10 +13,20 @@ from linux_mcp_server.gatekeeper.schema import openai_response_format +def _openrouter_config() -> dict[str, Any]: + if CONFIG.gatekeeper.openrouter is None: + return {"quantization": None, "template_kwargs": {}} + return { + "quantization": CONFIG.gatekeeper.openrouter.quantization, + "template_kwargs": CONFIG.gatekeeper.openrouter.template_kwargs, + } + + def _build_chat_completions_body(prompt: str) -> dict[str, Any]: + openrouter = _openrouter_config() provider: dict[str, Any] = {"require_parameters": True} - if CONFIG.gatekeeper.quantization: - provider["quantizations"] = [CONFIG.gatekeeper.quantization] + if openrouter["quantization"]: + provider["quantizations"] = [openrouter["quantization"]] body: dict[str, Any] = { "model": normalize_openrouter_model_id(CONFIG.gatekeeper.model or ""), @@ -29,8 +39,8 @@ def _build_chat_completions_body(prompt: str) -> dict[str, Any]: reasoning = openrouter_reasoning_block(CONFIG.gatekeeper.reasoning_effort) if reasoning is not None: body["reasoning"] = reasoning - if CONFIG.gatekeeper.template_kwargs: - body["chat_template_kwargs"] = CONFIG.gatekeeper.template_kwargs + if openrouter["template_kwargs"]: + body["chat_template_kwargs"] = openrouter["template_kwargs"] return body diff --git a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py new file mode 100644 index 00000000..5884201e --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py @@ -0,0 +1,84 @@ +"""Vertex AI gatekeeper client with model-based API routing.""" + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.gatekeeper.anthropic_client import build_messages_body +from linux_mcp_server.gatekeeper.anthropic_client import extract_messages_text +from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_location +from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project +from linux_mcp_server.gatekeeper.gemini_client import build_gemini_body +from linux_mcp_server.gatekeeper.gemini_client import extract_gemini_text +from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_VERTEX_VERSION +from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS +from linux_mcp_server.gatekeeper.http_utils import get_vertex_openapi_base_url +from linux_mcp_server.gatekeeper.http_utils import normalize_model_id +from linux_mcp_server.gatekeeper.http_utils import post_json +from linux_mcp_server.gatekeeper.http_utils import vertex_api_style +from linux_mcp_server.gatekeeper.http_utils import vertex_auth_headers +from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion +from linux_mcp_server.gatekeeper.openai_client import build_chat_completions_body +from linux_mcp_server.gatekeeper.openai_client import extract_chat_completions_text + + +def _anthropic_vertex_url(model: str) -> str: + project = get_gcp_project() + location = get_gcp_location() + host = "aiplatform.googleapis.com" if location == "global" else f"{location}-aiplatform.googleapis.com" + return f"https://{host}/v1/projects/{project}/locations/{location}/publishers/anthropic/models/{model}:rawPredict" + + +def _gemini_vertex_url(model: str) -> str: + project = get_gcp_project() + location = get_gcp_location() + host = "aiplatform.googleapis.com" if location == "global" else f"{location}-aiplatform.googleapis.com" + return f"https://{host}/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent" + + +def _complete_anthropic_on_vertex(prompt: str, *, timeout: int) -> GatekeeperCompletion: + model = normalize_model_id(CONFIG.gatekeeper.model or "") + body = build_messages_body(prompt, include_model=False) + body["anthropic_version"] = ANTHROPIC_VERTEX_VERSION + response = post_json( + provider="anthropic", + url=_anthropic_vertex_url(model), + headers={**vertex_auth_headers(), "Content-Type": "application/json"}, + body=body, + timeout=timeout, + ) + return GatekeeperCompletion(text=extract_messages_text(response)) + + +def _complete_gemini_on_vertex(prompt: str, *, timeout: int) -> GatekeeperCompletion: + model = normalize_model_id(CONFIG.gatekeeper.model or "") + response = post_json( + provider="gemini", + url=_gemini_vertex_url(model), + headers={**vertex_auth_headers(), "Content-Type": "application/json"}, + body=build_gemini_body(prompt), + timeout=timeout, + ) + return GatekeeperCompletion(text=extract_gemini_text(response)) + + +def _complete_openai_compatible_on_vertex(prompt: str, *, timeout: int) -> GatekeeperCompletion: + base_url = get_vertex_openapi_base_url() + response = post_json( + provider="openai", + url=f"{base_url}/chat/completions", + headers={**vertex_auth_headers(), "Content-Type": "application/json"}, + body=build_chat_completions_body(prompt), + timeout=timeout, + ) + return GatekeeperCompletion(text=extract_chat_completions_text(response)) + + +def complete_vertex_ai(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: + model = CONFIG.gatekeeper.model or "" + match vertex_api_style(model): + case "anthropic": + return _complete_anthropic_on_vertex(prompt, timeout=timeout) + case "gemini": + return _complete_gemini_on_vertex(prompt, timeout=timeout) + case "openai_compatible": + return _complete_openai_compatible_on_vertex(prompt, timeout=timeout) + case _: # pragma: no cover + raise ValueError(f"Unsupported Vertex AI API style for model: {model}") diff --git a/tests/gatekeeper/test_anthropic_client.py b/tests/gatekeeper/test_anthropic_client.py index d9f13394..5b29a43b 100644 --- a/tests/gatekeeper/test_anthropic_client.py +++ b/tests/gatekeeper/test_anthropic_client.py @@ -1,7 +1,6 @@ import pytest from linux_mcp_server.config import CONFIG -from linux_mcp_server.config import GatekeeperBackend from linux_mcp_server.config import GatekeeperConfig from linux_mcp_server.config import GatekeeperProvider from linux_mcp_server.gatekeeper import anthropic_client @@ -28,26 +27,8 @@ def test_complete_anthropic_direct(self, gatekeeper_config, mocker): result = anthropic_client.complete_anthropic("prompt") - assert result == '{"status": "OK", "detail": ""}' + assert result.text == '{"status": "OK", "detail": ""}' assert mock_post.call_args.kwargs["url"] == "https://api.anthropic.com/v1/messages" body = mock_post.call_args.kwargs["body"] assert body["model"] == "claude-sonnet-4-6" assert body["output_config"]["format"]["type"] == "json_schema" - - def test_complete_anthropic_vertex(self, gatekeeper_config, mocker): - gatekeeper_config.backend = GatekeeperBackend.VERTEX - gatekeeper_config.model = "claude-sonnet-4-5@20250929" - mocker.patch("linux_mcp_server.gatekeeper.anthropic_client.get_gcp_project", return_value="test-project") - mocker.patch("linux_mcp_server.gatekeeper.anthropic_client.get_gcp_location", return_value="global") - mocker.patch("linux_mcp_server.gatekeeper.anthropic_client.get_gcp_access_token", return_value="gcp-token") - mock_post = mocker.patch( - "linux_mcp_server.gatekeeper.anthropic_client.post_json", - return_value={"content": [{"type": "text", "text": '{"status": "OK"}'}]}, - ) - - anthropic_client.complete_anthropic("prompt") - - body = mock_post.call_args.kwargs["body"] - assert "model" not in body - assert body["anthropic_version"] == "vertex-2023-10-16" - assert ":rawPredict" in mock_post.call_args.kwargs["url"] diff --git a/tests/gatekeeper/test_check_run_script.py b/tests/gatekeeper/test_check_run_script.py index 06ecc6fb..8c0400c5 100644 --- a/tests/gatekeeper/test_check_run_script.py +++ b/tests/gatekeeper/test_check_run_script.py @@ -4,10 +4,10 @@ import pytest from linux_mcp_server.config import CONFIG -from linux_mcp_server.config import GatekeeperBackend from linux_mcp_server.config import GatekeeperConfig from linux_mcp_server.config import GatekeeperProvider from linux_mcp_server.config import ReasoningEffort +from linux_mcp_server.config import VertexAIGatekeeperConfig from linux_mcp_server.gatekeeper import GatekeeperResult from linux_mcp_server.gatekeeper import GatekeeperStatus from linux_mcp_server.gatekeeper.check_run_script import check_run_script @@ -218,24 +218,26 @@ async def test_openai_provider_config(self, mocker, mock_openai_post): assert body["temperature"] == 0.0 assert "text" in body - async def test_openai_vertex_backend_uses_custom_base_url(self, mocker): + async def test_vertex_ai_provider_uses_openapi_base_url(self, mocker): mocker.patch.dict("os.environ", {}, clear=False) mocker.patch( "linux_mcp_server.gatekeeper.gcp_auth.get_gcp_access_token", return_value="gcp-token", ) mock_post = mocker.patch( - "linux_mcp_server.gatekeeper.openai_client.post_json", + "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) mocker.patch.object( CONFIG, "gatekeeper", GatekeeperConfig( - provider=GatekeeperProvider.OPENAI, - backend=GatekeeperBackend.VERTEX, + provider=GatekeeperProvider.VERTEX_AI, model="gpt-oss-120b-maas", - base_url="https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi", + vertex_ai=VertexAIGatekeeperConfig( + project="p", + base_url="https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi", + ), structured_output=False, temperature=0.0, ), diff --git a/tests/gatekeeper/test_gcp_auth.py b/tests/gatekeeper/test_gcp_auth.py index 33bb93da..6e567c9e 100644 --- a/tests/gatekeeper/test_gcp_auth.py +++ b/tests/gatekeeper/test_gcp_auth.py @@ -1,6 +1,7 @@ import pytest from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import VertexAIGatekeeperConfig from linux_mcp_server.gatekeeper.gcp_auth import GCPAuthError from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_location from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project @@ -8,27 +9,27 @@ class TestGCPAuth: def test_get_gcp_project_from_config(self, mocker): - mocker.patch.object(CONFIG.gatekeeper, "project", "from-config") + mocker.patch.object(CONFIG.gatekeeper, "vertex_ai", VertexAIGatekeeperConfig(project="from-config")) mocker.patch.dict("os.environ", {}, clear=True) assert get_gcp_project() == "from-config" def test_get_gcp_project_from_env(self, mocker): - mocker.patch.object(CONFIG.gatekeeper, "project", None) + mocker.patch.object(CONFIG.gatekeeper, "vertex_ai", None) mocker.patch.dict("os.environ", {"VERTEXAI_PROJECT": "from-env"}, clear=True) assert get_gcp_project() == "from-env" def test_get_gcp_project_missing(self, mocker): - mocker.patch.object(CONFIG.gatekeeper, "project", None) + mocker.patch.object(CONFIG.gatekeeper, "vertex_ai", None) mocker.patch.dict("os.environ", {}, clear=True) - with pytest.raises(GCPAuthError, match="Vertex backend requires a GCP project"): + with pytest.raises(GCPAuthError, match="Vertex AI provider requires a GCP project"): get_gcp_project() def test_get_gcp_location_from_env(self, mocker): - mocker.patch.object(CONFIG.gatekeeper, "location", None) + mocker.patch.object(CONFIG.gatekeeper, "vertex_ai", None) mocker.patch.dict("os.environ", {"VERTEXAI_LOCATION": "us-central1"}, clear=True) assert get_gcp_location() == "us-central1" def test_get_gcp_location_default(self, mocker): - mocker.patch.object(CONFIG.gatekeeper, "location", None) + mocker.patch.object(CONFIG.gatekeeper, "vertex_ai", None) mocker.patch.dict("os.environ", {}, clear=True) assert get_gcp_location() == "global" diff --git a/tests/gatekeeper/test_gemini_client.py b/tests/gatekeeper/test_gemini_client.py index 19739dbd..6151a9e6 100644 --- a/tests/gatekeeper/test_gemini_client.py +++ b/tests/gatekeeper/test_gemini_client.py @@ -1,7 +1,6 @@ import pytest from linux_mcp_server.config import CONFIG -from linux_mcp_server.config import GatekeeperBackend from linux_mcp_server.config import GatekeeperConfig from linux_mcp_server.config import GatekeeperProvider from linux_mcp_server.config import ReasoningEffort @@ -30,26 +29,10 @@ def test_complete_gemini_google_ai(self, gatekeeper_config, mocker): result = gemini_client.complete_gemini("prompt") - assert result == '{"status": "OK"}' + assert result.text == '{"status": "OK"}' url = mock_post.call_args.kwargs["url"] assert "generativelanguage.googleapis.com" in url assert "key=test-key" in url body = mock_post.call_args.kwargs["body"] assert body["generationConfig"]["responseMimeType"] == "application/json" assert body["generationConfig"]["thinkingConfig"] == {"thinkingLevel": "LOW"} - - def test_complete_gemini_vertex(self, gatekeeper_config, mocker): - gatekeeper_config.backend = GatekeeperBackend.VERTEX - gatekeeper_config.model = "gemini-3.1-pro-preview" - mocker.patch("linux_mcp_server.gatekeeper.gemini_client.get_gcp_project", return_value="test-project") - mocker.patch("linux_mcp_server.gatekeeper.gemini_client.get_gcp_location", return_value="global") - mocker.patch("linux_mcp_server.gatekeeper.gemini_client.get_gcp_access_token", return_value="gcp-token") - mock_post = mocker.patch( - "linux_mcp_server.gatekeeper.gemini_client.post_json", - return_value={"candidates": [{"content": {"parts": [{"text": '{"status": "OK"}'}]}}]}, - ) - - gemini_client.complete_gemini("prompt") - - assert ":generateContent" in mock_post.call_args.kwargs["url"] - assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer gcp-token" diff --git a/tests/gatekeeper/test_http_utils.py b/tests/gatekeeper/test_http_utils.py index 99ea605c..b4c9bb37 100644 --- a/tests/gatekeeper/test_http_utils.py +++ b/tests/gatekeeper/test_http_utils.py @@ -1,13 +1,17 @@ import pytest +from linux_mcp_server.config import CONFIG from linux_mcp_server.config import ReasoningEffort +from linux_mcp_server.config import VertexAIGatekeeperConfig from linux_mcp_server.gatekeeper.http_utils import anthropic_thinking_block from linux_mcp_server.gatekeeper.http_utils import gemini_thinking_level +from linux_mcp_server.gatekeeper.http_utils import get_vertex_openapi_base_url from linux_mcp_server.gatekeeper.http_utils import normalize_model_id from linux_mcp_server.gatekeeper.http_utils import normalize_openrouter_model_id from linux_mcp_server.gatekeeper.http_utils import openai_reasoning_block from linux_mcp_server.gatekeeper.http_utils import openrouter_reasoning_block from linux_mcp_server.gatekeeper.http_utils import prefers_openai_chat_completions +from linux_mcp_server.gatekeeper.http_utils import vertex_api_style @pytest.mark.parametrize( @@ -83,3 +87,32 @@ def test_anthropic_thinking_block_low(): def test_gemini_thinking_level_medium(): assert gemini_thinking_level(ReasoningEffort.MEDIUM) == "MEDIUM" + + +@pytest.mark.parametrize( + "model,expected", + [ + ("claude-sonnet-4-6", "anthropic"), + ("vertex_ai/gemini-3.1-pro-preview", "gemini"), + ("gpt-oss-120b-maas", "openai_compatible"), + ], +) +def test_vertex_api_style(model, expected): + assert vertex_api_style(model) == expected + + +def test_get_vertex_openapi_base_url_from_config(mocker): + mocker.patch.object( + CONFIG.gatekeeper, + "vertex_ai", + VertexAIGatekeeperConfig(base_url="https://custom.example.com/openapi"), + ) + assert get_vertex_openapi_base_url() == "https://custom.example.com/openapi" + + +def test_get_vertex_openapi_base_url_computed(mocker): + mocker.patch.object(CONFIG.gatekeeper, "vertex_ai", VertexAIGatekeeperConfig(project="my-project")) + mocker.patch("linux_mcp_server.gatekeeper.gcp_auth.get_gcp_project", return_value="my-project") + mocker.patch("linux_mcp_server.gatekeeper.gcp_auth.get_gcp_location", return_value="global") + url = get_vertex_openapi_base_url() + assert url == "https://aiplatform.googleapis.com/v1/projects/my-project/locations/global/endpoints/openapi" diff --git a/tests/gatekeeper/test_llm.py b/tests/gatekeeper/test_llm.py index 1fce5c2c..dcc3d6d6 100644 --- a/tests/gatekeeper/test_llm.py +++ b/tests/gatekeeper/test_llm.py @@ -35,12 +35,13 @@ def test_infer_openrouter_from_model_prefix(self, mocker): class TestCompleteGatekeeper: def test_routes_to_openai(self, mocker): mocker.patch.object(llm_module, "resolve_provider", return_value=GatekeeperProvider.OPENAI) + expected = GatekeeperCompletion(text='{"status": "OK"}') mock_complete = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.complete_openai", - return_value='{"status": "OK"}', + return_value=expected, ) result = complete_gatekeeper("prompt") - assert result.text == '{"status": "OK"}' + assert result == expected mock_complete.assert_called_once_with("prompt") def test_routes_to_openrouter(self, mocker): @@ -53,3 +54,13 @@ def test_routes_to_openrouter(self, mocker): result = complete_gatekeeper("prompt") assert result == expected mock_complete.assert_called_once_with("prompt") + + def test_routes_to_vertex_ai(self, mocker): + mocker.patch.object(llm_module, "resolve_provider", return_value=GatekeeperProvider.VERTEX_AI) + mock_complete = mocker.patch( + "linux_mcp_server.gatekeeper.vertex_ai_client.complete_vertex_ai", + return_value=GatekeeperCompletion(text='{"status": "OK"}'), + ) + result = complete_gatekeeper("prompt") + assert result.text == '{"status": "OK"}' + mock_complete.assert_called_once_with("prompt") diff --git a/tests/gatekeeper/test_openai_client.py b/tests/gatekeeper/test_openai_client.py index 32af4d99..08a6dbd6 100644 --- a/tests/gatekeeper/test_openai_client.py +++ b/tests/gatekeeper/test_openai_client.py @@ -1,9 +1,9 @@ import pytest from linux_mcp_server.config import CONFIG -from linux_mcp_server.config import GatekeeperBackend from linux_mcp_server.config import GatekeeperConfig from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.config import OpenAIGatekeeperConfig from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper import openai_client from linux_mcp_server.gatekeeper.http_utils import GatekeeperHTTPError @@ -31,7 +31,7 @@ def test_complete_openai_uses_responses_api(self, gatekeeper_config, mocker): result = openai_client.complete_openai("prompt") - assert result == '{"status": "OK", "detail": ""}' + assert result.text == '{"status": "OK", "detail": ""}' assert mock_post.call_args.kwargs["url"] == "https://api.openai.com/v1/responses" body = mock_post.call_args.kwargs["body"] assert body["model"] == "gpt-5.4" @@ -39,7 +39,7 @@ def test_complete_openai_uses_responses_api(self, gatekeeper_config, mocker): assert body["text"]["format"]["type"] == "json_schema" def test_complete_openai_uses_responses_api_for_ollama(self, gatekeeper_config, mocker): - gatekeeper_config.base_url = "http://localhost:11434/v1" + gatekeeper_config.openai = OpenAIGatekeeperConfig(base_url="http://localhost:11434/v1") mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", return_value={"output_text": '{"status": "OK", "detail": ""}'}, @@ -47,11 +47,11 @@ def test_complete_openai_uses_responses_api_for_ollama(self, gatekeeper_config, result = openai_client.complete_openai("prompt") - assert result == '{"status": "OK", "detail": ""}' + assert result.text == '{"status": "OK", "detail": ""}' assert mock_post.call_args.kwargs["url"] == "http://localhost:11434/v1/responses" def test_complete_openai_falls_back_to_chat_completions_on_404(self, gatekeeper_config, mocker): - gatekeeper_config.base_url = "https://models.example.com/v1" + gatekeeper_config.openai = OpenAIGatekeeperConfig(base_url="https://models.example.com/v1") mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", side_effect=[ @@ -62,34 +62,13 @@ def test_complete_openai_falls_back_to_chat_completions_on_404(self, gatekeeper_ result = openai_client.complete_openai("prompt") - assert result == '{"status": "OK", "detail": ""}' + assert result.text == '{"status": "OK", "detail": ""}' assert mock_post.call_args_list[0].kwargs["url"] == "https://models.example.com/v1/responses" assert mock_post.call_args_list[1].kwargs["url"] == "https://models.example.com/v1/chat/completions" body = mock_post.call_args_list[1].kwargs["body"] assert body["response_format"]["type"] == "json_schema" assert body["reasoning_effort"] == "low" - def test_complete_openai_vertex_uses_gcp_token(self, gatekeeper_config, mocker): - gatekeeper_config.backend = GatekeeperBackend.VERTEX - gatekeeper_config.base_url = ( - "https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi" - ) - mocker.patch( - "linux_mcp_server.gatekeeper.gcp_auth.get_gcp_access_token", - return_value="gcp-token", - ) - mock_post = mocker.patch( - "linux_mcp_server.gatekeeper.openai_client.post_json", - return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, - ) - - openai_client.complete_openai("prompt") - - assert mock_post.call_count == 1 - assert mock_post.call_args.kwargs["url"].endswith("/chat/completions") - headers = mock_post.call_args.kwargs["headers"] - assert headers["Authorization"] == "Bearer gcp-token" - def test_structured_output_disabled(self, gatekeeper_config, mocker): gatekeeper_config.structured_output = False mock_post = mocker.patch( @@ -103,20 +82,19 @@ def test_structured_output_disabled(self, gatekeeper_config, mocker): assert "text" not in body def test_template_kwargs_on_chat_completions(self, gatekeeper_config, mocker): - gatekeeper_config.base_url = ( - "https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi" - ) - gatekeeper_config.template_kwargs = {"enable_thinking": False} - mocker.patch( - "linux_mcp_server.gatekeeper.gcp_auth.get_gcp_access_token", - return_value="gcp-token", + gatekeeper_config.openai = OpenAIGatekeeperConfig( + base_url="https://models.example.com/v1", + template_kwargs={"enable_thinking": False}, ) mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", - return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + side_effect=[ + GatekeeperHTTPError("openai", 404, "not found"), + {"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ], ) openai_client.complete_openai("prompt") - body = mock_post.call_args.kwargs["body"] + body = mock_post.call_args_list[1].kwargs["body"] assert body["chat_template_kwargs"] == {"enable_thinking": False} diff --git a/tests/gatekeeper/test_openrouter_client.py b/tests/gatekeeper/test_openrouter_client.py index 5c6a8d7c..6d499e1e 100644 --- a/tests/gatekeeper/test_openrouter_client.py +++ b/tests/gatekeeper/test_openrouter_client.py @@ -3,6 +3,7 @@ from linux_mcp_server.config import CONFIG from linux_mcp_server.config import GatekeeperConfig from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.config import OpenRouterGatekeeperConfig from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper import openrouter_client @@ -46,7 +47,7 @@ def test_complete_openrouter_request_body(self, gatekeeper_config, mocker): assert body["response_format"]["type"] == "json_schema" def test_complete_openrouter_quantization(self, gatekeeper_config, mocker): - gatekeeper_config.quantization = "fp4" + gatekeeper_config.openrouter = OpenRouterGatekeeperConfig(quantization="fp4") mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openrouter_client.post_json", return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, @@ -82,7 +83,7 @@ def test_complete_openrouter_legacy_model_prefix(self, gatekeeper_config, mocker assert body["model"] == "google/gemma-4-26b-a4b-it" def test_complete_openrouter_custom_base_url(self, gatekeeper_config, mocker): - gatekeeper_config.base_url = "https://openrouter.example.com/api/v1" + gatekeeper_config.openrouter = OpenRouterGatekeeperConfig(base_url="https://openrouter.example.com/api/v1") mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openrouter_client.post_json", return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, @@ -105,7 +106,7 @@ def test_structured_output_disabled(self, gatekeeper_config, mocker): assert "response_format" not in body def test_template_kwargs(self, gatekeeper_config, mocker): - gatekeeper_config.template_kwargs = {"enable_thinking": False} + gatekeeper_config.openrouter = OpenRouterGatekeeperConfig(template_kwargs={"enable_thinking": False}) mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openrouter_client.post_json", return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, diff --git a/tests/gatekeeper/test_vertex_ai_client.py b/tests/gatekeeper/test_vertex_ai_client.py new file mode 100644 index 00000000..865db31c --- /dev/null +++ b/tests/gatekeeper/test_vertex_ai_client.py @@ -0,0 +1,82 @@ +import pytest + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperConfig +from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.config import VertexAIGatekeeperConfig +from linux_mcp_server.gatekeeper import vertex_ai_client + + +class TestVertexAIClient: + @pytest.fixture + def gatekeeper_config(self, mocker): + config = GatekeeperConfig( + provider=GatekeeperProvider.VERTEX_AI, + model="claude-sonnet-4-6", + structured_output=True, + temperature=0.0, + vertex_ai=VertexAIGatekeeperConfig(project="test-project", location="global"), + ) + mocker.patch.object(CONFIG, "gatekeeper", config) + mocker.patch("linux_mcp_server.gatekeeper.vertex_ai_client.get_gcp_project", return_value="test-project") + mocker.patch("linux_mcp_server.gatekeeper.vertex_ai_client.get_gcp_location", return_value="global") + mocker.patch( + "linux_mcp_server.gatekeeper.gcp_auth.get_gcp_access_token", + return_value="gcp-token", + ) + return config + + def test_complete_anthropic_on_vertex(self, gatekeeper_config, mocker): + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", + return_value={"content": [{"type": "text", "text": '{"status": "OK"}'}]}, + ) + + result = vertex_ai_client.complete_vertex_ai("prompt") + + assert result.text == '{"status": "OK"}' + body = mock_post.call_args.kwargs["body"] + assert "model" not in body + assert body["anthropic_version"] == "vertex-2023-10-16" + assert ":rawPredict" in mock_post.call_args.kwargs["url"] + + def test_complete_gemini_on_vertex(self, gatekeeper_config, mocker): + gatekeeper_config.model = "gemini-3.1-pro-preview" + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", + return_value={"candidates": [{"content": {"parts": [{"text": '{"status": "OK"}'}]}}]}, + ) + + result = vertex_ai_client.complete_vertex_ai("prompt") + + assert result.text == '{"status": "OK"}' + assert ":generateContent" in mock_post.call_args.kwargs["url"] + assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer gcp-token" + + def test_complete_openai_compatible_on_vertex(self, gatekeeper_config, mocker): + gatekeeper_config.model = "gpt-oss-120b-maas" + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + + result = vertex_ai_client.complete_vertex_ai("prompt") + + assert result.text == '{"status": "OK"}' + assert mock_post.call_args.kwargs["url"].endswith("/chat/completions") + assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer gcp-token" + + def test_custom_openapi_base_url(self, gatekeeper_config, mocker): + gatekeeper_config.model = "gpt-oss-120b-maas" + gatekeeper_config.vertex_ai = VertexAIGatekeeperConfig( + project="test-project", + base_url="https://custom.example.com/v1/projects/p/locations/global/endpoints/openapi", + ) + mock_post = mocker.patch( + "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", + return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + ) + + vertex_ai_client.complete_vertex_ai("prompt") + + assert mock_post.call_args.kwargs["url"].startswith("https://custom.example.com") diff --git a/tests/test_config.py b/tests/test_config.py index 19ecdc55..66a422f8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -10,7 +10,6 @@ from pydantic import ValidationError from linux_mcp_server.config import Config -from linux_mcp_server.config import GatekeeperBackend from linux_mcp_server.config import GatekeeperProvider @@ -301,16 +300,19 @@ def test_new_env_var_works_without_old(self, mock_getuser, monkeypatch): class TestGatekeeperConfig: - def test_provider_and_backend(self, mock_getuser, monkeypatch): - monkeypatch.setenv("LINUX_MCP_GATEKEEPER__PROVIDER", "gemini") - monkeypatch.setenv("LINUX_MCP_GATEKEEPER__BACKEND", "vertex") + def test_vertex_ai_provider(self, mock_getuser, monkeypatch): + monkeypatch.setenv("LINUX_MCP_GATEKEEPER__PROVIDER", "vertex_ai") monkeypatch.setenv("LINUX_MCP_GATEKEEPER__MODEL", "gemini-3.1-pro-preview") + monkeypatch.setenv("LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT", "my-project") + monkeypatch.setenv("LINUX_MCP_GATEKEEPER__VERTEX_AI__LOCATION", "global") config = Config() - assert config.gatekeeper.provider == GatekeeperProvider.GEMINI - assert config.gatekeeper.backend == GatekeeperBackend.VERTEX + assert config.gatekeeper.provider == GatekeeperProvider.VERTEX_AI assert config.gatekeeper.model == "gemini-3.1-pro-preview" + assert config.gatekeeper.vertex_ai is not None + assert config.gatekeeper.vertex_ai.project == "my-project" + assert config.gatekeeper.vertex_ai.location == "global" def test_structured_output_defaults_true(self, mock_getuser, monkeypatch): config = Config() @@ -318,16 +320,18 @@ def test_structured_output_defaults_true(self, mock_getuser, monkeypatch): def test_template_kwargs(self, mock_getuser, monkeypatch): monkeypatch.setenv( - "LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS", '{ "enable_thinking": true, "reasoning_effort": "low" }' + "LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS", + '{ "enable_thinking": true, "reasoning_effort": "low" }', ) config = Config() - assert config.gatekeeper.template_kwargs == {"enable_thinking": True, "reasoning_effort": "low"} + assert config.gatekeeper.openai is not None + assert config.gatekeeper.openai.template_kwargs == {"enable_thinking": True, "reasoning_effort": "low"} def test_template_kwargs_unset(self, mock_getuser, monkeypatch): config = Config() - assert config.gatekeeper.template_kwargs == {} + assert config.gatekeeper.openai is None def test_cost(self, monkeypatch): monkeypatch.setenv("LINUX_MCP_GATEKEEPER__COST", "1e-6:4e-6") @@ -337,13 +341,14 @@ def test_cost(self, monkeypatch): def test_openrouter_provider_and_quantization(self, mock_getuser, monkeypatch): monkeypatch.setenv("LINUX_MCP_GATEKEEPER__PROVIDER", "openrouter") monkeypatch.setenv("LINUX_MCP_GATEKEEPER__MODEL", "openai/gpt-oss-120b") - monkeypatch.setenv("LINUX_MCP_GATEKEEPER__QUANTIZATION", "fp4") + monkeypatch.setenv("LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION", "fp4") config = Config() assert config.gatekeeper.provider == GatekeeperProvider.OPENROUTER assert config.gatekeeper.model == "openai/gpt-oss-120b" - assert config.gatekeeper.quantization == "fp4" + assert config.gatekeeper.openrouter is not None + assert config.gatekeeper.openrouter.quantization == "fp4" @pytest.mark.parametrize( "value", From 424325c03feca53412b86663747af1af414f8886 Mon Sep 17 00:00:00 2001 From: Link Dupont Date: Mon, 15 Jun 2026 11:34:05 -0400 Subject: [PATCH 3/7] refactor: reorganize provider-specific funcions Move provider-specific methods out of http_utils into their relevant client file. --- .../gatekeeper/anthropic_client.py | 42 ++++- .../gatekeeper/gemini_client.py | 35 ++++- src/linux_mcp_server/gatekeeper/http_utils.py | 145 ------------------ src/linux_mcp_server/gatekeeper/llm.py | 10 +- .../gatekeeper/openai_client.py | 49 ++++-- .../gatekeeper/openrouter_client.py | 50 ++++-- .../gatekeeper/vertex_ai_client.py | 50 ++++-- src/linux_mcp_server/models.py | 7 + tests/gatekeeper/test_anthropic_client.py | 7 + tests/gatekeeper/test_gemini_client.py | 5 + tests/gatekeeper/test_http_utils.py | 102 ------------ tests/gatekeeper/test_openai_client.py | 27 ++++ tests/gatekeeper/test_openrouter_client.py | 30 ++++ tests/gatekeeper/test_vertex_ai_client.py | 31 ++++ 14 files changed, 290 insertions(+), 300 deletions(-) diff --git a/src/linux_mcp_server/gatekeeper/anthropic_client.py b/src/linux_mcp_server/gatekeeper/anthropic_client.py index 95ce4bb5..cca74c72 100644 --- a/src/linux_mcp_server/gatekeeper/anthropic_client.py +++ b/src/linux_mcp_server/gatekeeper/anthropic_client.py @@ -1,18 +1,44 @@ """Anthropic Messages API client for the gatekeeper.""" +import os + from typing import Any from linux_mcp_server.config import CONFIG -from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_API_URL -from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_API_VERSION -from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_DEFAULT_MAX_TOKENS -from linux_mcp_server.gatekeeper.http_utils import anthropic_thinking_block +from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS -from linux_mcp_server.gatekeeper.http_utils import get_anthropic_api_key from linux_mcp_server.gatekeeper.http_utils import normalize_model_id from linux_mcp_server.gatekeeper.http_utils import post_json -from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion from linux_mcp_server.gatekeeper.schema import anthropic_output_config +from linux_mcp_server.models import GatekeeperCompletion + + +ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages" +ANTHROPIC_API_VERSION = "2023-06-01" +ANTHROPIC_DEFAULT_MAX_TOKENS = 4096 + + +def _get_anthropic_api_key() -> str: + api_key = os.environ.get("ANTHROPIC_API_KEY") + if not api_key: + raise ValueError("ANTHROPIC_API_KEY is required for Anthropic gatekeeper provider.") + return api_key + + +def _anthropic_thinking_block(reasoning_effort: ReasoningEffort | None) -> dict[str, Any] | None: + if reasoning_effort is None or reasoning_effort in {ReasoningEffort.NONE, ReasoningEffort.DEFAULT}: + return None + budget_by_effort = { + ReasoningEffort.MINIMAL: 1024, + ReasoningEffort.LOW: 4096, + ReasoningEffort.MEDIUM: 8192, + ReasoningEffort.HIGH: 16384, + ReasoningEffort.XHIGH: 32768, + } + budget = budget_by_effort.get(reasoning_effort) + if budget is None: + return None + return {"type": "enabled", "budget_tokens": budget} def build_messages_body(prompt: str, *, include_model: bool) -> dict[str, Any]: @@ -25,7 +51,7 @@ def build_messages_body(prompt: str, *, include_model: bool) -> dict[str, Any]: body["model"] = normalize_model_id(CONFIG.gatekeeper.model or "") if CONFIG.gatekeeper.structured_output: body["output_config"] = anthropic_output_config() - thinking = anthropic_thinking_block(CONFIG.gatekeeper.reasoning_effort) + thinking = _anthropic_thinking_block(CONFIG.gatekeeper.reasoning_effort) if thinking is not None: body["thinking"] = thinking return body @@ -42,7 +68,7 @@ def extract_messages_text(response: dict[str, Any]) -> str: def complete_anthropic(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: headers = { - "x-api-key": get_anthropic_api_key(), + "x-api-key": _get_anthropic_api_key(), "anthropic-version": ANTHROPIC_API_VERSION, "Content-Type": "application/json", } diff --git a/src/linux_mcp_server/gatekeeper/gemini_client.py b/src/linux_mcp_server/gatekeeper/gemini_client.py index 30cb3136..34de487a 100644 --- a/src/linux_mcp_server/gatekeeper/gemini_client.py +++ b/src/linux_mcp_server/gatekeeper/gemini_client.py @@ -1,16 +1,39 @@ """Gemini generateContent client for the gatekeeper.""" +import os + from typing import Any from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS -from linux_mcp_server.gatekeeper.http_utils import gemini_thinking_level -from linux_mcp_server.gatekeeper.http_utils import get_google_api_key -from linux_mcp_server.gatekeeper.http_utils import GOOGLE_AI_BASE_URL from linux_mcp_server.gatekeeper.http_utils import normalize_model_id from linux_mcp_server.gatekeeper.http_utils import post_json -from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion from linux_mcp_server.gatekeeper.schema import gemini_generation_config +from linux_mcp_server.models import GatekeeperCompletion + + +GOOGLE_AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" + + +def _get_google_api_key() -> str: + api_key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY") + if not api_key: + raise ValueError("GOOGLE_API_KEY or GEMINI_API_KEY is required for Gemini direct backend.") + return api_key + + +def _gemini_thinking_level(reasoning_effort: ReasoningEffort | None) -> str | None: + if reasoning_effort is None or reasoning_effort in {ReasoningEffort.NONE, ReasoningEffort.DEFAULT}: + return None + mapping = { + ReasoningEffort.MINIMAL: "MINIMAL", + ReasoningEffort.LOW: "LOW", + ReasoningEffort.MEDIUM: "MEDIUM", + ReasoningEffort.HIGH: "HIGH", + ReasoningEffort.XHIGH: "HIGH", + } + return mapping.get(reasoning_effort) def build_gemini_body(prompt: str) -> dict[str, Any]: @@ -18,7 +41,7 @@ def build_gemini_body(prompt: str) -> dict[str, Any]: temperature=CONFIG.gatekeeper.temperature, structured_output=CONFIG.gatekeeper.structured_output, ) - thinking_level = gemini_thinking_level(CONFIG.gatekeeper.reasoning_effort) + thinking_level = _gemini_thinking_level(CONFIG.gatekeeper.reasoning_effort) if thinking_level is not None: generation_config["thinkingConfig"] = {"thinkingLevel": thinking_level} return { @@ -41,7 +64,7 @@ def extract_gemini_text(response: dict[str, Any]) -> str: def complete_gemini(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: model = normalize_model_id(CONFIG.gatekeeper.model or "") - api_key = get_google_api_key() + api_key = _get_google_api_key() response = post_json( provider="gemini", url=f"{GOOGLE_AI_BASE_URL}/models/{model}:generateContent?key={api_key}", diff --git a/src/linux_mcp_server/gatekeeper/http_utils.py b/src/linux_mcp_server/gatekeeper/http_utils.py index 4b03150f..9fefd75a 100644 --- a/src/linux_mcp_server/gatekeeper/http_utils.py +++ b/src/linux_mcp_server/gatekeeper/http_utils.py @@ -1,29 +1,12 @@ """Shared helpers for gatekeeper HTTP clients.""" -import os - from typing import Any -from typing import Literal -from urllib.parse import urlparse import requests -from linux_mcp_server.config import CONFIG -from linux_mcp_server.config import ReasoningEffort - DEFAULT_TIMEOUT_SECONDS = 120 -OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1" -OPENROUTER_DEFAULT_BASE_URL = "https://openrouter.ai/api/v1" - -ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages" -ANTHROPIC_API_VERSION = "2023-06-01" -ANTHROPIC_VERTEX_VERSION = "vertex-2023-10-16" -ANTHROPIC_DEFAULT_MAX_TOKENS = 4096 - -GOOGLE_AI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" - class GatekeeperHTTPError(RuntimeError): """Raised when an LLM provider returns an error response.""" @@ -50,136 +33,8 @@ def post_json( return response.json() -def get_openai_base_url() -> str: - configured = CONFIG.gatekeeper.openai.base_url if CONFIG.gatekeeper.openai else None - return (configured or os.environ.get("OPENAI_API_BASE") or OPENAI_DEFAULT_BASE_URL).rstrip("/") - - -def prefers_openai_chat_completions(base_url: str) -> bool: - """Hosts known to expose only Chat Completions, not the Responses API.""" - path = urlparse(base_url).path or "" - return "/endpoints/openapi" in path - - def normalize_model_id(model: str) -> str: for prefix in ("openai/", "anthropic/", "vertex_ai/", "gemini/"): if model.startswith(prefix): return model[len(prefix) :] return model - - -def normalize_openrouter_model_id(model: str) -> str: - if model.startswith("openrouter/"): - return model[len("openrouter/") :] - return model - - -def get_openrouter_base_url() -> str: - configured = CONFIG.gatekeeper.openrouter.base_url if CONFIG.gatekeeper.openrouter else None - return (configured or OPENROUTER_DEFAULT_BASE_URL).rstrip("/") - - -def vertex_api_style(model: str) -> Literal["anthropic", "gemini", "openai_compatible"]: - normalized = normalize_model_id(model) - if normalized.startswith("claude"): - return "anthropic" - if normalized.startswith("gemini"): - return "gemini" - return "openai_compatible" - - -def get_vertex_openapi_base_url() -> str: - cfg = CONFIG.gatekeeper.vertex_ai - if cfg and cfg.base_url: - return cfg.base_url.rstrip("/") - from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_location - from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project - - project = get_gcp_project() - location = get_gcp_location() - return f"https://aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/endpoints/openapi" - - -def vertex_auth_headers() -> dict[str, str]: - from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_access_token - - return {"Authorization": f"Bearer {get_gcp_access_token()}"} - - -def openai_reasoning_block(reasoning_effort: ReasoningEffort | None) -> dict[str, Any] | None: - if reasoning_effort is None or reasoning_effort == ReasoningEffort.DEFAULT: - return None - return {"effort": reasoning_effort.value} - - -def openrouter_reasoning_block(reasoning_effort: ReasoningEffort | None) -> dict[str, Any] | None: - if reasoning_effort is None or reasoning_effort == ReasoningEffort.DEFAULT: - return None - if reasoning_effort == ReasoningEffort.NONE: - return {"enabled": False} - return {"enabled": True, "effort": reasoning_effort.value} - - -def anthropic_thinking_block(reasoning_effort: ReasoningEffort | None) -> dict[str, Any] | None: - if reasoning_effort is None or reasoning_effort in {ReasoningEffort.NONE, ReasoningEffort.DEFAULT}: - return None - budget_by_effort = { - ReasoningEffort.MINIMAL: 1024, - ReasoningEffort.LOW: 4096, - ReasoningEffort.MEDIUM: 8192, - ReasoningEffort.HIGH: 16384, - ReasoningEffort.XHIGH: 32768, - } - budget = budget_by_effort.get(reasoning_effort) - if budget is None: - return None - return {"type": "enabled", "budget_tokens": budget} - - -def gemini_thinking_level(reasoning_effort: ReasoningEffort | None) -> str | None: - if reasoning_effort is None or reasoning_effort in {ReasoningEffort.NONE, ReasoningEffort.DEFAULT}: - return None - mapping = { - ReasoningEffort.MINIMAL: "MINIMAL", - ReasoningEffort.LOW: "LOW", - ReasoningEffort.MEDIUM: "MEDIUM", - ReasoningEffort.HIGH: "HIGH", - ReasoningEffort.XHIGH: "HIGH", - } - return mapping.get(reasoning_effort) - - -def get_openai_api_key() -> str: - api_key = os.environ.get("OPENAI_API_KEY") - if not api_key: - raise ValueError("OPENAI_API_KEY is required for OpenAI gatekeeper provider.") - return api_key - - -def get_openrouter_api_key() -> str: - api_key = os.environ.get("OPENROUTER_API_KEY") - if not api_key: - raise ValueError("OPENROUTER_API_KEY is required for OpenRouter gatekeeper provider.") - return api_key - - -def openrouter_auth_headers() -> dict[str, str]: - return {"Authorization": f"Bearer {get_openrouter_api_key()}"} - - -def get_anthropic_api_key() -> str: - api_key = os.environ.get("ANTHROPIC_API_KEY") - if not api_key: - raise ValueError("ANTHROPIC_API_KEY is required for Anthropic gatekeeper provider.") - return api_key - - -def get_google_api_key() -> str: - api_key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY") - if not api_key: - raise ValueError("GOOGLE_API_KEY or GEMINI_API_KEY is required for Gemini direct backend.") - return api_key - - -def openai_auth_headers() -> dict[str, str]: - return {"Authorization": f"Bearer {get_openai_api_key()}"} diff --git a/src/linux_mcp_server/gatekeeper/llm.py b/src/linux_mcp_server/gatekeeper/llm.py index 0a795aaa..56b8ca08 100644 --- a/src/linux_mcp_server/gatekeeper/llm.py +++ b/src/linux_mcp_server/gatekeeper/llm.py @@ -2,8 +2,6 @@ import logging -from pydantic import BaseModel - from linux_mcp_server.config import CONFIG from linux_mcp_server.config import GatekeeperProvider from linux_mcp_server.gatekeeper.anthropic_client import complete_anthropic @@ -11,18 +9,12 @@ from linux_mcp_server.gatekeeper.openai_client import complete_openai from linux_mcp_server.gatekeeper.openrouter_client import complete_openrouter from linux_mcp_server.gatekeeper.vertex_ai_client import complete_vertex_ai +from linux_mcp_server.models import GatekeeperCompletion logger = logging.getLogger("linux-mcp-server") -class GatekeeperCompletion(BaseModel): - text: str - prompt_tokens: int = 0 - completion_tokens: int = 0 - usage_cost: float | None = None - - def _infer_provider_from_model(model: str) -> GatekeeperProvider: if model.startswith("openrouter/"): return GatekeeperProvider.OPENROUTER diff --git a/src/linux_mcp_server/gatekeeper/openai_client.py b/src/linux_mcp_server/gatekeeper/openai_client.py index 988f1397..15f338b5 100644 --- a/src/linux_mcp_server/gatekeeper/openai_client.py +++ b/src/linux_mcp_server/gatekeeper/openai_client.py @@ -1,19 +1,50 @@ """OpenAI Responses and Chat Completions clients for the gatekeeper.""" +import os + from typing import Any +from urllib.parse import urlparse from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS from linux_mcp_server.gatekeeper.http_utils import GatekeeperHTTPError -from linux_mcp_server.gatekeeper.http_utils import get_openai_base_url from linux_mcp_server.gatekeeper.http_utils import normalize_model_id -from linux_mcp_server.gatekeeper.http_utils import openai_auth_headers -from linux_mcp_server.gatekeeper.http_utils import openai_reasoning_block from linux_mcp_server.gatekeeper.http_utils import post_json -from linux_mcp_server.gatekeeper.http_utils import prefers_openai_chat_completions -from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion from linux_mcp_server.gatekeeper.schema import openai_response_format from linux_mcp_server.gatekeeper.schema import openai_text_format +from linux_mcp_server.models import GatekeeperCompletion + + +OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1" + + +def _openai_reasoning_block(reasoning_effort: ReasoningEffort | None) -> dict[str, Any] | None: + if reasoning_effort is None or reasoning_effort == ReasoningEffort.DEFAULT: + return None + return {"effort": reasoning_effort.value} + + +def _get_openai_api_key() -> str: + api_key = os.environ.get("OPENAI_API_KEY") + if not api_key: + raise ValueError("OPENAI_API_KEY is required for OpenAI gatekeeper provider.") + return api_key + + +def _openai_auth_headers() -> dict[str, str]: + return {"Authorization": f"Bearer {_get_openai_api_key()}"} + + +def _get_openai_base_url() -> str: + configured = CONFIG.gatekeeper.openai.base_url if CONFIG.gatekeeper.openai else None + return (configured or os.environ.get("OPENAI_API_BASE") or OPENAI_DEFAULT_BASE_URL).rstrip("/") + + +def _prefers_openai_chat_completions(base_url: str) -> bool: + """Hosts known to expose only Chat Completions, not the Responses API.""" + path = urlparse(base_url).path or "" + return "/endpoints/openapi" in path def _openai_template_kwargs() -> dict[str, Any]: @@ -39,7 +70,7 @@ def _build_responses_body(prompt: str) -> dict[str, Any]: } if CONFIG.gatekeeper.structured_output: body["text"] = openai_text_format() - reasoning = openai_reasoning_block(CONFIG.gatekeeper.reasoning_effort) + reasoning = _openai_reasoning_block(CONFIG.gatekeeper.reasoning_effort) if reasoning is not None: body["reasoning"] = reasoning return body @@ -89,14 +120,14 @@ def extract_chat_completions_text(response: dict[str, Any]) -> str: def complete_openai(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: - base_url = get_openai_base_url() + base_url = _get_openai_base_url() headers = { - **openai_auth_headers(), + **_openai_auth_headers(), "Content-Type": "application/json", } # Try the Responses API first, falling back to Chat Completions if it's not available. - if not prefers_openai_chat_completions(base_url): + if not _prefers_openai_chat_completions(base_url): try: response = post_json( provider="openai", diff --git a/src/linux_mcp_server/gatekeeper/openrouter_client.py b/src/linux_mcp_server/gatekeeper/openrouter_client.py index 972ef7bd..0e705a28 100644 --- a/src/linux_mcp_server/gatekeeper/openrouter_client.py +++ b/src/linux_mcp_server/gatekeeper/openrouter_client.py @@ -1,16 +1,48 @@ """OpenRouter Chat Completions client for the gatekeeper.""" +import os + from typing import Any from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS -from linux_mcp_server.gatekeeper.http_utils import get_openrouter_base_url -from linux_mcp_server.gatekeeper.http_utils import normalize_openrouter_model_id -from linux_mcp_server.gatekeeper.http_utils import openrouter_auth_headers -from linux_mcp_server.gatekeeper.http_utils import openrouter_reasoning_block from linux_mcp_server.gatekeeper.http_utils import post_json -from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion from linux_mcp_server.gatekeeper.schema import openai_response_format +from linux_mcp_server.models import GatekeeperCompletion + + +OPENROUTER_DEFAULT_BASE_URL = "https://openrouter.ai/api/v1" + + +def _normalize_openrouter_model_id(model: str) -> str: + if model.startswith("openrouter/"): + return model[len("openrouter/") :] + return model + + +def _get_openrouter_base_url() -> str: + configured = CONFIG.gatekeeper.openrouter.base_url if CONFIG.gatekeeper.openrouter else None + return (configured or OPENROUTER_DEFAULT_BASE_URL).rstrip("/") + + +def _get_openrouter_api_key() -> str: + api_key = os.environ.get("OPENROUTER_API_KEY") + if not api_key: + raise ValueError("OPENROUTER_API_KEY is required for OpenRouter gatekeeper provider.") + return api_key + + +def _openrouter_auth_headers() -> dict[str, str]: + return {"Authorization": f"Bearer {_get_openrouter_api_key()}"} + + +def _openrouter_reasoning_block(reasoning_effort: ReasoningEffort | None) -> dict[str, Any] | None: + if reasoning_effort is None or reasoning_effort == ReasoningEffort.DEFAULT: + return None + if reasoning_effort == ReasoningEffort.NONE: + return {"enabled": False} + return {"enabled": True, "effort": reasoning_effort.value} def _openrouter_config() -> dict[str, Any]: @@ -29,14 +61,14 @@ def _build_chat_completions_body(prompt: str) -> dict[str, Any]: provider["quantizations"] = [openrouter["quantization"]] body: dict[str, Any] = { - "model": normalize_openrouter_model_id(CONFIG.gatekeeper.model or ""), + "model": _normalize_openrouter_model_id(CONFIG.gatekeeper.model or ""), "messages": [{"role": "user", "content": prompt}], "temperature": CONFIG.gatekeeper.temperature, "provider": provider, } if CONFIG.gatekeeper.structured_output: body["response_format"] = openai_response_format() - reasoning = openrouter_reasoning_block(CONFIG.gatekeeper.reasoning_effort) + reasoning = _openrouter_reasoning_block(CONFIG.gatekeeper.reasoning_effort) if reasoning is not None: body["reasoning"] = reasoning if openrouter["template_kwargs"]: @@ -68,9 +100,9 @@ def _extract_usage(response: dict[str, Any]) -> tuple[int, int, float | None]: def complete_openrouter(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: - base_url = get_openrouter_base_url() + base_url = _get_openrouter_base_url() headers = { - **openrouter_auth_headers(), + **_openrouter_auth_headers(), "Content-Type": "application/json", } response = post_json( diff --git a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py index 5884201e..06d96e5a 100644 --- a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py +++ b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py @@ -1,5 +1,7 @@ """Vertex AI gatekeeper client with model-based API routing.""" +from typing import Literal + from linux_mcp_server.config import CONFIG from linux_mcp_server.gatekeeper.anthropic_client import build_messages_body from linux_mcp_server.gatekeeper.anthropic_client import extract_messages_text @@ -7,16 +9,42 @@ from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project from linux_mcp_server.gatekeeper.gemini_client import build_gemini_body from linux_mcp_server.gatekeeper.gemini_client import extract_gemini_text -from linux_mcp_server.gatekeeper.http_utils import ANTHROPIC_VERTEX_VERSION from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS -from linux_mcp_server.gatekeeper.http_utils import get_vertex_openapi_base_url from linux_mcp_server.gatekeeper.http_utils import normalize_model_id from linux_mcp_server.gatekeeper.http_utils import post_json -from linux_mcp_server.gatekeeper.http_utils import vertex_api_style -from linux_mcp_server.gatekeeper.http_utils import vertex_auth_headers -from linux_mcp_server.gatekeeper.llm import GatekeeperCompletion from linux_mcp_server.gatekeeper.openai_client import build_chat_completions_body from linux_mcp_server.gatekeeper.openai_client import extract_chat_completions_text +from linux_mcp_server.models import GatekeeperCompletion + + +ANTHROPIC_VERTEX_VERSION = "vertex-2023-10-16" + + +def _vertex_api_style(model: str) -> Literal["anthropic", "gemini", "openai_compatible"]: + normalized = normalize_model_id(model) + if normalized.startswith("claude"): + return "anthropic" + if normalized.startswith("gemini"): + return "gemini" + return "openai_compatible" + + +def _get_vertex_openapi_base_url() -> str: + cfg = CONFIG.gatekeeper.vertex_ai + if cfg and cfg.base_url: + return cfg.base_url.rstrip("/") + from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_location + from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_project + + project = get_gcp_project() + location = get_gcp_location() + return f"https://aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/endpoints/openapi" + + +def _vertex_auth_headers() -> dict[str, str]: + from linux_mcp_server.gatekeeper.gcp_auth import get_gcp_access_token + + return {"Authorization": f"Bearer {get_gcp_access_token()}"} def _anthropic_vertex_url(model: str) -> str: @@ -40,7 +68,7 @@ def _complete_anthropic_on_vertex(prompt: str, *, timeout: int) -> GatekeeperCom response = post_json( provider="anthropic", url=_anthropic_vertex_url(model), - headers={**vertex_auth_headers(), "Content-Type": "application/json"}, + headers={**_vertex_auth_headers(), "Content-Type": "application/json"}, body=body, timeout=timeout, ) @@ -52,7 +80,7 @@ def _complete_gemini_on_vertex(prompt: str, *, timeout: int) -> GatekeeperComple response = post_json( provider="gemini", url=_gemini_vertex_url(model), - headers={**vertex_auth_headers(), "Content-Type": "application/json"}, + headers={**_vertex_auth_headers(), "Content-Type": "application/json"}, body=build_gemini_body(prompt), timeout=timeout, ) @@ -60,11 +88,11 @@ def _complete_gemini_on_vertex(prompt: str, *, timeout: int) -> GatekeeperComple def _complete_openai_compatible_on_vertex(prompt: str, *, timeout: int) -> GatekeeperCompletion: - base_url = get_vertex_openapi_base_url() + base_url = _get_vertex_openapi_base_url() response = post_json( provider="openai", url=f"{base_url}/chat/completions", - headers={**vertex_auth_headers(), "Content-Type": "application/json"}, + headers={**_vertex_auth_headers(), "Content-Type": "application/json"}, body=build_chat_completions_body(prompt), timeout=timeout, ) @@ -73,12 +101,10 @@ def _complete_openai_compatible_on_vertex(prompt: str, *, timeout: int) -> Gatek def complete_vertex_ai(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: model = CONFIG.gatekeeper.model or "" - match vertex_api_style(model): + match _vertex_api_style(model): case "anthropic": return _complete_anthropic_on_vertex(prompt, timeout=timeout) case "gemini": return _complete_gemini_on_vertex(prompt, timeout=timeout) case "openai_compatible": return _complete_openai_compatible_on_vertex(prompt, timeout=timeout) - case _: # pragma: no cover - raise ValueError(f"Unsupported Vertex AI API style for model: {model}") diff --git a/src/linux_mcp_server/models.py b/src/linux_mcp_server/models.py index e734ed13..dbcc04b2 100644 --- a/src/linux_mcp_server/models.py +++ b/src/linux_mcp_server/models.py @@ -198,3 +198,10 @@ class LogEntries(BaseModel): @field_serializer("unit", "path") def serialize_empty_as_null(self, value: str | Path | None) -> str | None: return str(value) if value else None + + +class GatekeeperCompletion(BaseModel): + text: str + prompt_tokens: int = 0 + completion_tokens: int = 0 + usage_cost: float | None = None diff --git a/tests/gatekeeper/test_anthropic_client.py b/tests/gatekeeper/test_anthropic_client.py index 5b29a43b..4bc69cf6 100644 --- a/tests/gatekeeper/test_anthropic_client.py +++ b/tests/gatekeeper/test_anthropic_client.py @@ -3,7 +3,14 @@ from linux_mcp_server.config import CONFIG from linux_mcp_server.config import GatekeeperConfig from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper import anthropic_client +from linux_mcp_server.gatekeeper.anthropic_client import _anthropic_thinking_block + + +def test_anthropic_thinking_block_low(): + block = _anthropic_thinking_block(ReasoningEffort.LOW) + assert block == {"type": "enabled", "budget_tokens": 4096} class TestAnthropicClient: diff --git a/tests/gatekeeper/test_gemini_client.py b/tests/gatekeeper/test_gemini_client.py index 6151a9e6..afe0952a 100644 --- a/tests/gatekeeper/test_gemini_client.py +++ b/tests/gatekeeper/test_gemini_client.py @@ -5,6 +5,11 @@ from linux_mcp_server.config import GatekeeperProvider from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper import gemini_client +from linux_mcp_server.gatekeeper.gemini_client import _gemini_thinking_level + + +def test_gemini_thinking_level_medium(): + assert _gemini_thinking_level(ReasoningEffort.MEDIUM) == "MEDIUM" class TestGeminiClient: diff --git a/tests/gatekeeper/test_http_utils.py b/tests/gatekeeper/test_http_utils.py index b4c9bb37..8cdc0df3 100644 --- a/tests/gatekeeper/test_http_utils.py +++ b/tests/gatekeeper/test_http_utils.py @@ -1,17 +1,6 @@ import pytest -from linux_mcp_server.config import CONFIG -from linux_mcp_server.config import ReasoningEffort -from linux_mcp_server.config import VertexAIGatekeeperConfig -from linux_mcp_server.gatekeeper.http_utils import anthropic_thinking_block -from linux_mcp_server.gatekeeper.http_utils import gemini_thinking_level -from linux_mcp_server.gatekeeper.http_utils import get_vertex_openapi_base_url from linux_mcp_server.gatekeeper.http_utils import normalize_model_id -from linux_mcp_server.gatekeeper.http_utils import normalize_openrouter_model_id -from linux_mcp_server.gatekeeper.http_utils import openai_reasoning_block -from linux_mcp_server.gatekeeper.http_utils import openrouter_reasoning_block -from linux_mcp_server.gatekeeper.http_utils import prefers_openai_chat_completions -from linux_mcp_server.gatekeeper.http_utils import vertex_api_style @pytest.mark.parametrize( @@ -25,94 +14,3 @@ ) def test_normalize_model_id(model, expected): assert normalize_model_id(model) == expected - - -@pytest.mark.parametrize( - "base_url,expected", - [ - ("https://api.openai.com/v1", False), - ("http://localhost:11434/v1", False), - ("https://example.com/v1", False), - ( - "https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi", - True, - ), - ], -) -def test_prefers_openai_chat_completions(base_url, expected): - assert prefers_openai_chat_completions(base_url) is expected - - -def test_openai_reasoning_block_none(): - assert openai_reasoning_block(None) is None - assert openai_reasoning_block(ReasoningEffort.DEFAULT) is None - - -def test_openai_reasoning_block_low(): - assert openai_reasoning_block(ReasoningEffort.LOW) == {"effort": "low"} - - -@pytest.mark.parametrize( - "effort,expected", - [ - (ReasoningEffort.NONE, {"enabled": False}), - (ReasoningEffort.LOW, {"enabled": True, "effort": "low"}), - (ReasoningEffort.HIGH, {"enabled": True, "effort": "high"}), - ], -) -def test_openrouter_reasoning_block(effort, expected): - assert openrouter_reasoning_block(effort) == expected - - -def test_openrouter_reasoning_block_default(): - assert openrouter_reasoning_block(None) is None - assert openrouter_reasoning_block(ReasoningEffort.DEFAULT) is None - - -@pytest.mark.parametrize( - "model,expected", - [ - ("openrouter/google/gemma-4-26b-a4b-it", "google/gemma-4-26b-a4b-it"), - ("openai/gpt-oss-120b", "openai/gpt-oss-120b"), - ], -) -def test_normalize_openrouter_model_id(model, expected): - assert normalize_openrouter_model_id(model) == expected - - -def test_anthropic_thinking_block_low(): - block = anthropic_thinking_block(ReasoningEffort.LOW) - assert block == {"type": "enabled", "budget_tokens": 4096} - - -def test_gemini_thinking_level_medium(): - assert gemini_thinking_level(ReasoningEffort.MEDIUM) == "MEDIUM" - - -@pytest.mark.parametrize( - "model,expected", - [ - ("claude-sonnet-4-6", "anthropic"), - ("vertex_ai/gemini-3.1-pro-preview", "gemini"), - ("gpt-oss-120b-maas", "openai_compatible"), - ], -) -def test_vertex_api_style(model, expected): - assert vertex_api_style(model) == expected - - -def test_get_vertex_openapi_base_url_from_config(mocker): - mocker.patch.object( - CONFIG.gatekeeper, - "vertex_ai", - VertexAIGatekeeperConfig(base_url="https://custom.example.com/openapi"), - ) - assert get_vertex_openapi_base_url() == "https://custom.example.com/openapi" - - -def test_get_vertex_openapi_base_url_computed(mocker): - mocker.patch.object(CONFIG.gatekeeper, "vertex_ai", VertexAIGatekeeperConfig(project="my-project")) - mocker.patch("linux_mcp_server.gatekeeper.gcp_auth.get_gcp_project", return_value="my-project") - mocker.patch("linux_mcp_server.gatekeeper.gcp_auth.get_gcp_location", return_value="global") - url = get_vertex_openapi_base_url() - assert url == "https://aiplatform.googleapis.com/v1/projects/my-project/locations/global/endpoints/openapi" diff --git a/tests/gatekeeper/test_openai_client.py b/tests/gatekeeper/test_openai_client.py index 08a6dbd6..d1ab93de 100644 --- a/tests/gatekeeper/test_openai_client.py +++ b/tests/gatekeeper/test_openai_client.py @@ -7,6 +7,33 @@ from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper import openai_client from linux_mcp_server.gatekeeper.http_utils import GatekeeperHTTPError +from linux_mcp_server.gatekeeper.openai_client import _openai_reasoning_block +from linux_mcp_server.gatekeeper.openai_client import _prefers_openai_chat_completions + + +@pytest.mark.parametrize( + "base_url,expected", + [ + ("https://api.openai.com/v1", False), + ("http://localhost:11434/v1", False), + ("https://example.com/v1", False), + ( + "https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi", + True, + ), + ], +) +def test_prefers_openai_chat_completions(base_url, expected): + assert _prefers_openai_chat_completions(base_url) is expected + + +def test_openai_reasoning_block_none(): + assert _openai_reasoning_block(None) is None + assert _openai_reasoning_block(ReasoningEffort.DEFAULT) is None + + +def test_openai_reasoning_block_low(): + assert _openai_reasoning_block(ReasoningEffort.LOW) == {"effort": "low"} class TestOpenAIClient: diff --git a/tests/gatekeeper/test_openrouter_client.py b/tests/gatekeeper/test_openrouter_client.py index 6d499e1e..7d761086 100644 --- a/tests/gatekeeper/test_openrouter_client.py +++ b/tests/gatekeeper/test_openrouter_client.py @@ -6,6 +6,36 @@ from linux_mcp_server.config import OpenRouterGatekeeperConfig from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper import openrouter_client +from linux_mcp_server.gatekeeper.openrouter_client import _normalize_openrouter_model_id +from linux_mcp_server.gatekeeper.openrouter_client import _openrouter_reasoning_block + + +@pytest.mark.parametrize( + "effort,expected", + [ + (ReasoningEffort.NONE, {"enabled": False}), + (ReasoningEffort.LOW, {"enabled": True, "effort": "low"}), + (ReasoningEffort.HIGH, {"enabled": True, "effort": "high"}), + ], +) +def test_openrouter_reasoning_block(effort, expected): + assert _openrouter_reasoning_block(effort) == expected + + +def test_openrouter_reasoning_block_default(): + assert _openrouter_reasoning_block(None) is None + assert _openrouter_reasoning_block(ReasoningEffort.DEFAULT) is None + + +@pytest.mark.parametrize( + "model,expected", + [ + ("openrouter/google/gemma-4-26b-a4b-it", "google/gemma-4-26b-a4b-it"), + ("openai/gpt-oss-120b", "openai/gpt-oss-120b"), + ], +) +def test_normalize_openrouter_model_id(model, expected): + assert _normalize_openrouter_model_id(model) == expected class TestOpenRouterClient: diff --git a/tests/gatekeeper/test_vertex_ai_client.py b/tests/gatekeeper/test_vertex_ai_client.py index 865db31c..5269e491 100644 --- a/tests/gatekeeper/test_vertex_ai_client.py +++ b/tests/gatekeeper/test_vertex_ai_client.py @@ -5,6 +5,37 @@ from linux_mcp_server.config import GatekeeperProvider from linux_mcp_server.config import VertexAIGatekeeperConfig from linux_mcp_server.gatekeeper import vertex_ai_client +from linux_mcp_server.gatekeeper.vertex_ai_client import _get_vertex_openapi_base_url +from linux_mcp_server.gatekeeper.vertex_ai_client import _vertex_api_style + + +@pytest.mark.parametrize( + "model,expected", + [ + ("claude-sonnet-4-6", "anthropic"), + ("vertex_ai/gemini-3.1-pro-preview", "gemini"), + ("gpt-oss-120b-maas", "openai_compatible"), + ], +) +def test_vertex_api_style(model, expected): + assert _vertex_api_style(model) == expected + + +def test_get_vertex_openapi_base_url_from_config(mocker): + mocker.patch.object( + CONFIG.gatekeeper, + "vertex_ai", + VertexAIGatekeeperConfig(base_url="https://custom.example.com/openapi"), + ) + assert _get_vertex_openapi_base_url() == "https://custom.example.com/openapi" + + +def test_get_vertex_openapi_base_url_computed(mocker): + mocker.patch.object(CONFIG.gatekeeper, "vertex_ai", VertexAIGatekeeperConfig(project="my-project")) + mocker.patch("linux_mcp_server.gatekeeper.gcp_auth.get_gcp_project", return_value="my-project") + mocker.patch("linux_mcp_server.gatekeeper.gcp_auth.get_gcp_location", return_value="global") + url = _get_vertex_openapi_base_url() + assert url == "https://aiplatform.googleapis.com/v1/projects/my-project/locations/global/endpoints/openapi" class TestVertexAIClient: From 66d4c32015e58fafb077390620a0141982a5a73f Mon Sep 17 00:00:00 2001 From: Link Dupont Date: Mon, 15 Jun 2026 14:04:59 -0400 Subject: [PATCH 4/7] fix: Restore GATEKEEPER_MAX_TOKENS Passing it down into each provider's completion function. --- .../gatekeeper/anthropic_client.py | 9 +++---- .../gatekeeper/check_run_script.py | 4 ++- .../gatekeeper/gemini_client.py | 7 ++--- src/linux_mcp_server/gatekeeper/llm.py | 12 ++++----- .../gatekeeper/openai_client.py | 12 +++++---- .../gatekeeper/openrouter_client.py | 9 ++++--- src/linux_mcp_server/gatekeeper/schema.py | 4 +-- .../gatekeeper/vertex_ai_client.py | 20 +++++++------- tests/gatekeeper/test_anthropic_client.py | 2 +- tests/gatekeeper/test_check_run_script.py | 4 +-- tests/gatekeeper/test_gemini_client.py | 2 +- tests/gatekeeper/test_llm.py | 27 +++++++------------ tests/gatekeeper/test_openai_client.py | 10 +++---- tests/gatekeeper/test_openrouter_client.py | 14 +++++----- tests/gatekeeper/test_vertex_ai_client.py | 8 +++--- 15 files changed, 72 insertions(+), 72 deletions(-) diff --git a/src/linux_mcp_server/gatekeeper/anthropic_client.py b/src/linux_mcp_server/gatekeeper/anthropic_client.py index cca74c72..e9131109 100644 --- a/src/linux_mcp_server/gatekeeper/anthropic_client.py +++ b/src/linux_mcp_server/gatekeeper/anthropic_client.py @@ -15,7 +15,6 @@ ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages" ANTHROPIC_API_VERSION = "2023-06-01" -ANTHROPIC_DEFAULT_MAX_TOKENS = 4096 def _get_anthropic_api_key() -> str: @@ -41,9 +40,9 @@ def _anthropic_thinking_block(reasoning_effort: ReasoningEffort | None) -> dict[ return {"type": "enabled", "budget_tokens": budget} -def build_messages_body(prompt: str, *, include_model: bool) -> dict[str, Any]: +def build_messages_body(prompt: str, *, include_model: bool, max_tokens: int) -> dict[str, Any]: body: dict[str, Any] = { - "max_tokens": ANTHROPIC_DEFAULT_MAX_TOKENS, + "max_tokens": max_tokens, "messages": [{"role": "user", "content": prompt}], "temperature": CONFIG.gatekeeper.temperature, } @@ -66,7 +65,7 @@ def extract_messages_text(response: dict[str, Any]) -> str: return "" -def complete_anthropic(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: +def complete_anthropic(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: headers = { "x-api-key": _get_anthropic_api_key(), "anthropic-version": ANTHROPIC_API_VERSION, @@ -76,7 +75,7 @@ def complete_anthropic(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) - provider="anthropic", url=ANTHROPIC_API_URL, headers=headers, - body=build_messages_body(prompt, include_model=True), + body=build_messages_body(prompt, include_model=True, max_tokens=max_tokens), timeout=timeout, ) return GatekeeperCompletion(text=extract_messages_text(response)) diff --git a/src/linux_mcp_server/gatekeeper/check_run_script.py b/src/linux_mcp_server/gatekeeper/check_run_script.py index d0582d22..3546d23d 100644 --- a/src/linux_mcp_server/gatekeeper/check_run_script.py +++ b/src/linux_mcp_server/gatekeeper/check_run_script.py @@ -103,6 +103,8 @@ def get_model() -> str: """ +# Maximum number of completion tokens (including reasoning) +GATEKEEPER_MAX_TOKENS = 8000 # Timeout (s) GATEKEEPER_TIMEOUT = 120 @@ -226,7 +228,7 @@ async def check_run_script_with_stats( time_before = time.perf_counter() try: completion = await asyncio.wait_for( - asyncio.to_thread(complete_gatekeeper, prompt), + asyncio.to_thread(complete_gatekeeper, prompt, max_tokens=GATEKEEPER_MAX_TOKENS), timeout=GATEKEEPER_TIMEOUT, ) except asyncio.TimeoutError: diff --git a/src/linux_mcp_server/gatekeeper/gemini_client.py b/src/linux_mcp_server/gatekeeper/gemini_client.py index 34de487a..95dec35e 100644 --- a/src/linux_mcp_server/gatekeeper/gemini_client.py +++ b/src/linux_mcp_server/gatekeeper/gemini_client.py @@ -36,10 +36,11 @@ def _gemini_thinking_level(reasoning_effort: ReasoningEffort | None) -> str | No return mapping.get(reasoning_effort) -def build_gemini_body(prompt: str) -> dict[str, Any]: +def build_gemini_body(prompt: str, *, max_tokens: int) -> dict[str, Any]: generation_config = gemini_generation_config( temperature=CONFIG.gatekeeper.temperature, structured_output=CONFIG.gatekeeper.structured_output, + max_tokens=max_tokens, ) thinking_level = _gemini_thinking_level(CONFIG.gatekeeper.reasoning_effort) if thinking_level is not None: @@ -62,14 +63,14 @@ def extract_gemini_text(response: dict[str, Any]) -> str: return text.strip() if isinstance(text, str) else "" -def complete_gemini(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: +def complete_gemini(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: model = normalize_model_id(CONFIG.gatekeeper.model or "") api_key = _get_google_api_key() response = post_json( provider="gemini", url=f"{GOOGLE_AI_BASE_URL}/models/{model}:generateContent?key={api_key}", headers={"Content-Type": "application/json"}, - body=build_gemini_body(prompt), + body=build_gemini_body(prompt, max_tokens=max_tokens), timeout=timeout, ) return GatekeeperCompletion(text=extract_gemini_text(response)) diff --git a/src/linux_mcp_server/gatekeeper/llm.py b/src/linux_mcp_server/gatekeeper/llm.py index 56b8ca08..0378bd4c 100644 --- a/src/linux_mcp_server/gatekeeper/llm.py +++ b/src/linux_mcp_server/gatekeeper/llm.py @@ -33,19 +33,19 @@ def resolve_provider() -> GatekeeperProvider: return _infer_provider_from_model(CONFIG.gatekeeper.model) -def complete_gatekeeper(prompt: str) -> GatekeeperCompletion: +def complete_gatekeeper(prompt: str, *, max_tokens: int) -> GatekeeperCompletion: provider = resolve_provider() match provider: case GatekeeperProvider.OPENAI: - completion = complete_openai(prompt) + completion = complete_openai(prompt, max_tokens=max_tokens) case GatekeeperProvider.ANTHROPIC: - completion = complete_anthropic(prompt) + completion = complete_anthropic(prompt, max_tokens=max_tokens) case GatekeeperProvider.GEMINI: - completion = complete_gemini(prompt) + completion = complete_gemini(prompt, max_tokens=max_tokens) case GatekeeperProvider.OPENROUTER: - completion = complete_openrouter(prompt) + completion = complete_openrouter(prompt, max_tokens=max_tokens) case GatekeeperProvider.VERTEX_AI: - completion = complete_vertex_ai(prompt) + completion = complete_vertex_ai(prompt, max_tokens=max_tokens) case _: # pragma: no cover raise ValueError(f"Unsupported gatekeeper provider: {provider}") diff --git a/src/linux_mcp_server/gatekeeper/openai_client.py b/src/linux_mcp_server/gatekeeper/openai_client.py index 15f338b5..a11377c2 100644 --- a/src/linux_mcp_server/gatekeeper/openai_client.py +++ b/src/linux_mcp_server/gatekeeper/openai_client.py @@ -61,10 +61,11 @@ def _apply_chat_completions_extras(body: dict[str, Any]) -> dict[str, Any]: return body -def _build_responses_body(prompt: str) -> dict[str, Any]: +def _build_responses_body(prompt: str, *, max_tokens: int) -> dict[str, Any]: body: dict[str, Any] = { "model": normalize_model_id(CONFIG.gatekeeper.model or ""), "input": prompt, + "max_output_tokens": max_tokens, "temperature": CONFIG.gatekeeper.temperature, "store": False, } @@ -76,10 +77,11 @@ def _build_responses_body(prompt: str) -> dict[str, Any]: return body -def build_chat_completions_body(prompt: str) -> dict[str, Any]: +def build_chat_completions_body(prompt: str, *, max_tokens: int) -> dict[str, Any]: body: dict[str, Any] = { "model": normalize_model_id(CONFIG.gatekeeper.model or ""), "messages": [{"role": "user", "content": prompt}], + "max_completion_tokens": max_tokens, "temperature": CONFIG.gatekeeper.temperature, } if CONFIG.gatekeeper.structured_output: @@ -119,7 +121,7 @@ def extract_chat_completions_text(response: dict[str, Any]) -> str: return (content or "").strip() if isinstance(content, str) else "" -def complete_openai(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: +def complete_openai(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: base_url = _get_openai_base_url() headers = { **_openai_auth_headers(), @@ -133,7 +135,7 @@ def complete_openai(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> G provider="openai", url=f"{base_url}/responses", headers=headers, - body=_build_responses_body(prompt), + body=_build_responses_body(prompt, max_tokens=max_tokens), timeout=timeout, ) return GatekeeperCompletion(text=_extract_responses_text(response)) @@ -145,7 +147,7 @@ def complete_openai(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> G provider="openai", url=f"{base_url}/chat/completions", headers=headers, - body=build_chat_completions_body(prompt), + body=build_chat_completions_body(prompt, max_tokens=max_tokens), timeout=timeout, ) return GatekeeperCompletion(text=extract_chat_completions_text(response)) diff --git a/src/linux_mcp_server/gatekeeper/openrouter_client.py b/src/linux_mcp_server/gatekeeper/openrouter_client.py index 0e705a28..adf7c645 100644 --- a/src/linux_mcp_server/gatekeeper/openrouter_client.py +++ b/src/linux_mcp_server/gatekeeper/openrouter_client.py @@ -54,7 +54,7 @@ def _openrouter_config() -> dict[str, Any]: } -def _build_chat_completions_body(prompt: str) -> dict[str, Any]: +def _build_chat_completions_body(prompt: str, *, max_tokens: int) -> dict[str, Any]: openrouter = _openrouter_config() provider: dict[str, Any] = {"require_parameters": True} if openrouter["quantization"]: @@ -63,6 +63,7 @@ def _build_chat_completions_body(prompt: str) -> dict[str, Any]: body: dict[str, Any] = { "model": _normalize_openrouter_model_id(CONFIG.gatekeeper.model or ""), "messages": [{"role": "user", "content": prompt}], + "max_tokens": max_tokens, "temperature": CONFIG.gatekeeper.temperature, "provider": provider, } @@ -99,7 +100,9 @@ def _extract_usage(response: dict[str, Any]) -> tuple[int, int, float | None]: ) -def complete_openrouter(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: +def complete_openrouter( + prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS +) -> GatekeeperCompletion: base_url = _get_openrouter_base_url() headers = { **_openrouter_auth_headers(), @@ -109,7 +112,7 @@ def complete_openrouter(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) provider="openrouter", url=f"{base_url}/chat/completions", headers=headers, - body=_build_chat_completions_body(prompt), + body=_build_chat_completions_body(prompt, max_tokens=max_tokens), timeout=timeout, ) prompt_tokens, completion_tokens, usage_cost = _extract_usage(response) diff --git a/src/linux_mcp_server/gatekeeper/schema.py b/src/linux_mcp_server/gatekeeper/schema.py index c7a4ff4d..4a84a0d8 100644 --- a/src/linux_mcp_server/gatekeeper/schema.py +++ b/src/linux_mcp_server/gatekeeper/schema.py @@ -99,8 +99,8 @@ def anthropic_output_config() -> dict[str, Any]: } -def gemini_generation_config(*, temperature: float, structured_output: bool) -> dict[str, Any]: - config: dict[str, Any] = {"temperature": temperature} +def gemini_generation_config(*, temperature: float, structured_output: bool, max_tokens: int) -> dict[str, Any]: + config: dict[str, Any] = {"temperature": temperature, "maxOutputTokens": max_tokens} if structured_output: config["responseMimeType"] = "application/json" config["responseSchema"] = gemini_json_schema() diff --git a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py index 06d96e5a..0635688f 100644 --- a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py +++ b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py @@ -61,9 +61,9 @@ def _gemini_vertex_url(model: str) -> str: return f"https://{host}/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent" -def _complete_anthropic_on_vertex(prompt: str, *, timeout: int) -> GatekeeperCompletion: +def _complete_anthropic_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: model = normalize_model_id(CONFIG.gatekeeper.model or "") - body = build_messages_body(prompt, include_model=False) + body = build_messages_body(prompt, include_model=False, max_tokens=max_tokens) body["anthropic_version"] = ANTHROPIC_VERTEX_VERSION response = post_json( provider="anthropic", @@ -75,36 +75,36 @@ def _complete_anthropic_on_vertex(prompt: str, *, timeout: int) -> GatekeeperCom return GatekeeperCompletion(text=extract_messages_text(response)) -def _complete_gemini_on_vertex(prompt: str, *, timeout: int) -> GatekeeperCompletion: +def _complete_gemini_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: model = normalize_model_id(CONFIG.gatekeeper.model or "") response = post_json( provider="gemini", url=_gemini_vertex_url(model), headers={**_vertex_auth_headers(), "Content-Type": "application/json"}, - body=build_gemini_body(prompt), + body=build_gemini_body(prompt, max_tokens=max_tokens), timeout=timeout, ) return GatekeeperCompletion(text=extract_gemini_text(response)) -def _complete_openai_compatible_on_vertex(prompt: str, *, timeout: int) -> GatekeeperCompletion: +def _complete_openai_compatible_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: base_url = _get_vertex_openapi_base_url() response = post_json( provider="openai", url=f"{base_url}/chat/completions", headers={**_vertex_auth_headers(), "Content-Type": "application/json"}, - body=build_chat_completions_body(prompt), + body=build_chat_completions_body(prompt, max_tokens=max_tokens), timeout=timeout, ) return GatekeeperCompletion(text=extract_chat_completions_text(response)) -def complete_vertex_ai(prompt: str, *, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: +def complete_vertex_ai(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: model = CONFIG.gatekeeper.model or "" match _vertex_api_style(model): case "anthropic": - return _complete_anthropic_on_vertex(prompt, timeout=timeout) + return _complete_anthropic_on_vertex(prompt, max_tokens=max_tokens, timeout=timeout) case "gemini": - return _complete_gemini_on_vertex(prompt, timeout=timeout) + return _complete_gemini_on_vertex(prompt, max_tokens=max_tokens, timeout=timeout) case "openai_compatible": - return _complete_openai_compatible_on_vertex(prompt, timeout=timeout) + return _complete_openai_compatible_on_vertex(prompt, max_tokens=max_tokens, timeout=timeout) diff --git a/tests/gatekeeper/test_anthropic_client.py b/tests/gatekeeper/test_anthropic_client.py index 4bc69cf6..7e7365d2 100644 --- a/tests/gatekeeper/test_anthropic_client.py +++ b/tests/gatekeeper/test_anthropic_client.py @@ -32,7 +32,7 @@ def test_complete_anthropic_direct(self, gatekeeper_config, mocker): return_value={"content": [{"type": "text", "text": '{"status": "OK", "detail": ""}'}]}, ) - result = anthropic_client.complete_anthropic("prompt") + result = anthropic_client.complete_anthropic("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' assert mock_post.call_args.kwargs["url"] == "https://api.anthropic.com/v1/messages" diff --git a/tests/gatekeeper/test_check_run_script.py b/tests/gatekeeper/test_check_run_script.py index 8c0400c5..ab30b172 100644 --- a/tests/gatekeeper/test_check_run_script.py +++ b/tests/gatekeeper/test_check_run_script.py @@ -85,7 +85,7 @@ def _completion(text: str) -> GatekeeperCompletion: return mocker.patch.object( check_run_script_module, "complete_gatekeeper", - side_effect=lambda prompt: _completion('{"status": "OK", "detail": ""}'), + side_effect=lambda prompt, **kwargs: _completion('{"status": "OK", "detail": ""}'), ) async def test_rejects_script_with_prompt_injection_attempts(self): @@ -128,7 +128,7 @@ async def test_parse_errors(self, mocker, response_text): await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) async def test_timeout(self, mocker): - def slow_complete(_prompt: str) -> GatekeeperCompletion: + def slow_complete(_prompt: str, **kwargs: object) -> GatekeeperCompletion: time.sleep(10) return GatekeeperCompletion(text='{"status": "OK"}') diff --git a/tests/gatekeeper/test_gemini_client.py b/tests/gatekeeper/test_gemini_client.py index afe0952a..b62920a8 100644 --- a/tests/gatekeeper/test_gemini_client.py +++ b/tests/gatekeeper/test_gemini_client.py @@ -32,7 +32,7 @@ def test_complete_gemini_google_ai(self, gatekeeper_config, mocker): return_value={"candidates": [{"content": {"parts": [{"text": '{"status": "OK"}'}]}}]}, ) - result = gemini_client.complete_gemini("prompt") + result = gemini_client.complete_gemini("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' url = mock_post.call_args.kwargs["url"] diff --git a/tests/gatekeeper/test_llm.py b/tests/gatekeeper/test_llm.py index dcc3d6d6..d5b40ec4 100644 --- a/tests/gatekeeper/test_llm.py +++ b/tests/gatekeeper/test_llm.py @@ -36,31 +36,24 @@ class TestCompleteGatekeeper: def test_routes_to_openai(self, mocker): mocker.patch.object(llm_module, "resolve_provider", return_value=GatekeeperProvider.OPENAI) expected = GatekeeperCompletion(text='{"status": "OK"}') - mock_complete = mocker.patch( - "linux_mcp_server.gatekeeper.openai_client.complete_openai", - return_value=expected, - ) - result = complete_gatekeeper("prompt") + mock_complete = mocker.patch.object(llm_module, "complete_openai", return_value=expected) + result = complete_gatekeeper("prompt", max_tokens=8000) assert result == expected - mock_complete.assert_called_once_with("prompt") + mock_complete.assert_called_once_with("prompt", max_tokens=8000) def test_routes_to_openrouter(self, mocker): mocker.patch.object(llm_module, "resolve_provider", return_value=GatekeeperProvider.OPENROUTER) expected = GatekeeperCompletion(text='{"status": "OK"}', prompt_tokens=1, completion_tokens=2, usage_cost=0.5) - mock_complete = mocker.patch( - "linux_mcp_server.gatekeeper.openrouter_client.complete_openrouter", - return_value=expected, - ) - result = complete_gatekeeper("prompt") + mock_complete = mocker.patch.object(llm_module, "complete_openrouter", return_value=expected) + result = complete_gatekeeper("prompt", max_tokens=8000) assert result == expected - mock_complete.assert_called_once_with("prompt") + mock_complete.assert_called_once_with("prompt", max_tokens=8000) def test_routes_to_vertex_ai(self, mocker): mocker.patch.object(llm_module, "resolve_provider", return_value=GatekeeperProvider.VERTEX_AI) - mock_complete = mocker.patch( - "linux_mcp_server.gatekeeper.vertex_ai_client.complete_vertex_ai", - return_value=GatekeeperCompletion(text='{"status": "OK"}'), + mock_complete = mocker.patch.object( + llm_module, "complete_vertex_ai", return_value=GatekeeperCompletion(text='{"status": "OK"}') ) - result = complete_gatekeeper("prompt") + result = complete_gatekeeper("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' - mock_complete.assert_called_once_with("prompt") + mock_complete.assert_called_once_with("prompt", max_tokens=8000) diff --git a/tests/gatekeeper/test_openai_client.py b/tests/gatekeeper/test_openai_client.py index d1ab93de..d625eb5a 100644 --- a/tests/gatekeeper/test_openai_client.py +++ b/tests/gatekeeper/test_openai_client.py @@ -56,7 +56,7 @@ def test_complete_openai_uses_responses_api(self, gatekeeper_config, mocker): return_value={"output_text": '{"status": "OK", "detail": ""}'}, ) - result = openai_client.complete_openai("prompt") + result = openai_client.complete_openai("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' assert mock_post.call_args.kwargs["url"] == "https://api.openai.com/v1/responses" @@ -72,7 +72,7 @@ def test_complete_openai_uses_responses_api_for_ollama(self, gatekeeper_config, return_value={"output_text": '{"status": "OK", "detail": ""}'}, ) - result = openai_client.complete_openai("prompt") + result = openai_client.complete_openai("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' assert mock_post.call_args.kwargs["url"] == "http://localhost:11434/v1/responses" @@ -87,7 +87,7 @@ def test_complete_openai_falls_back_to_chat_completions_on_404(self, gatekeeper_ ], ) - result = openai_client.complete_openai("prompt") + result = openai_client.complete_openai("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' assert mock_post.call_args_list[0].kwargs["url"] == "https://models.example.com/v1/responses" @@ -103,7 +103,7 @@ def test_structured_output_disabled(self, gatekeeper_config, mocker): return_value={"output_text": '{"status": "OK"}'}, ) - openai_client.complete_openai("prompt") + openai_client.complete_openai("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert "text" not in body @@ -121,7 +121,7 @@ def test_template_kwargs_on_chat_completions(self, gatekeeper_config, mocker): ], ) - openai_client.complete_openai("prompt") + openai_client.complete_openai("prompt", max_tokens=8000) body = mock_post.call_args_list[1].kwargs["body"] assert body["chat_template_kwargs"] == {"enable_thinking": False} diff --git a/tests/gatekeeper/test_openrouter_client.py b/tests/gatekeeper/test_openrouter_client.py index 7d761086..5cee0ae4 100644 --- a/tests/gatekeeper/test_openrouter_client.py +++ b/tests/gatekeeper/test_openrouter_client.py @@ -61,7 +61,7 @@ def test_complete_openrouter_request_body(self, gatekeeper_config, mocker): }, ) - completion = openrouter_client.complete_openrouter("prompt") + completion = openrouter_client.complete_openrouter("prompt", max_tokens=8000) assert completion.text == '{"status": "OK", "detail": ""}' assert completion.prompt_tokens == 10 @@ -83,7 +83,7 @@ def test_complete_openrouter_quantization(self, gatekeeper_config, mocker): return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt") + openrouter_client.complete_openrouter("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert body["provider"] == {"require_parameters": True, "quantizations": ["fp4"]} @@ -95,7 +95,7 @@ def test_complete_openrouter_reasoning_none(self, gatekeeper_config, mocker): return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt") + openrouter_client.complete_openrouter("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert body["reasoning"] == {"enabled": False} @@ -107,7 +107,7 @@ def test_complete_openrouter_legacy_model_prefix(self, gatekeeper_config, mocker return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt") + openrouter_client.complete_openrouter("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert body["model"] == "google/gemma-4-26b-a4b-it" @@ -119,7 +119,7 @@ def test_complete_openrouter_custom_base_url(self, gatekeeper_config, mocker): return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt") + openrouter_client.complete_openrouter("prompt", max_tokens=8000) assert mock_post.call_args.kwargs["url"] == "https://openrouter.example.com/api/v1/chat/completions" @@ -130,7 +130,7 @@ def test_structured_output_disabled(self, gatekeeper_config, mocker): return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt") + openrouter_client.complete_openrouter("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert "response_format" not in body @@ -142,7 +142,7 @@ def test_template_kwargs(self, gatekeeper_config, mocker): return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt") + openrouter_client.complete_openrouter("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert body["chat_template_kwargs"] == {"enable_thinking": False} diff --git a/tests/gatekeeper/test_vertex_ai_client.py b/tests/gatekeeper/test_vertex_ai_client.py index 5269e491..d0144522 100644 --- a/tests/gatekeeper/test_vertex_ai_client.py +++ b/tests/gatekeeper/test_vertex_ai_client.py @@ -63,7 +63,7 @@ def test_complete_anthropic_on_vertex(self, gatekeeper_config, mocker): return_value={"content": [{"type": "text", "text": '{"status": "OK"}'}]}, ) - result = vertex_ai_client.complete_vertex_ai("prompt") + result = vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' body = mock_post.call_args.kwargs["body"] @@ -78,7 +78,7 @@ def test_complete_gemini_on_vertex(self, gatekeeper_config, mocker): return_value={"candidates": [{"content": {"parts": [{"text": '{"status": "OK"}'}]}}]}, ) - result = vertex_ai_client.complete_vertex_ai("prompt") + result = vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' assert ":generateContent" in mock_post.call_args.kwargs["url"] @@ -91,7 +91,7 @@ def test_complete_openai_compatible_on_vertex(self, gatekeeper_config, mocker): return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - result = vertex_ai_client.complete_vertex_ai("prompt") + result = vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' assert mock_post.call_args.kwargs["url"].endswith("/chat/completions") @@ -108,6 +108,6 @@ def test_custom_openapi_base_url(self, gatekeeper_config, mocker): return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - vertex_ai_client.complete_vertex_ai("prompt") + vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) assert mock_post.call_args.kwargs["url"].startswith("https://custom.example.com") From 5263bf72461d9c56b6626d6c93c939efedbc3e05 Mon Sep 17 00:00:00 2001 From: Link Dupont Date: Tue, 16 Jun 2026 14:14:14 -0400 Subject: [PATCH 5/7] feat(gatekeeper): enhance cost estimation and usage tracking - Introduced a new pricing module to compute costs based on token usage across different providers. - Added functionality to extract usage statistics from responses for OpenAI, Anthropic, Gemini, and OpenRouter. - Updated GatekeeperCompletion and GatekeeperStats models to include token counts and cost sources. - Updated documentation to clarify cost estimation logic and configuration options. - Added tests to ensure accurate cost calculations and usage extraction across various clients. --- docs/config-reference.md | 2 +- docs/guarded-command-execution.md | 11 + scripts/vendor_models_dev_pricing.py | 132 ++++++++++++ .../gatekeeper/anthropic_client.py | 8 +- .../gatekeeper/check_run_script.py | 25 +-- .../gatekeeper/data/models_dev_fallback.json | 96 +++++++++ .../gatekeeper/gemini_client.py | 8 +- .../gatekeeper/openai_client.py | 16 +- .../gatekeeper/openrouter_client.py | 23 +-- src/linux_mcp_server/gatekeeper/pricing.py | 189 ++++++++++++++++++ src/linux_mcp_server/gatekeeper/usage.py | 63 ++++++ .../gatekeeper/vertex_ai_client.py | 24 ++- src/linux_mcp_server/models.py | 20 ++ tests/gatekeeper/test_anthropic_client.py | 7 +- tests/gatekeeper/test_check_run_script.py | 2 + tests/gatekeeper/test_gemini_client.py | 7 +- tests/gatekeeper/test_openai_client.py | 14 +- tests/gatekeeper/test_pricing.py | 75 +++++++ tests/gatekeeper/test_usage.py | 48 +++++ 19 files changed, 726 insertions(+), 44 deletions(-) create mode 100644 scripts/vendor_models_dev_pricing.py create mode 100644 src/linux_mcp_server/gatekeeper/data/models_dev_fallback.json create mode 100644 src/linux_mcp_server/gatekeeper/pricing.py create mode 100644 src/linux_mcp_server/gatekeeper/usage.py create mode 100644 tests/gatekeeper/test_pricing.py create mode 100644 tests/gatekeeper/test_usage.py diff --git a/docs/config-reference.md b/docs/config-reference.md index 7f13474d..c4922686 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -63,7 +63,7 @@ These are used when `LINUX_MCP_TOOLSET` is set to `run_script` or `both`. | `--gatekeeper.reasoning_effort`
`LINUX_MCP_GATEKEEPER__REASONING_EFFORT` | _(model specific)_ | Reasoning effort (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`). Not all values are supported for all models. | | `--gatekeeper.structured_output`
`LINUX_MCP_GATEKEEPER__STRUCTURED_OUTPUT` | `True` | Whether to use structured JSON output from the model | | `--gatekeeper.temperature`
`LINUX_MCP_GATEKEEPER__TEMPERATURE` | 0.0 | Temperature to use for the model | -| `--gatekeeper.cost`
`LINUX_MCP_GATEKEEPER__COST` | _(none)_ | Gatekeeper cost for accounting (`input$/token:output$/token`) | +| `--gatekeeper.cost`
`LINUX_MCP_GATEKEEPER__COST` | _(none)_ | Optional per-token cost override for gatekeeper accounting (`input$/token:output$/token`). When unset, cost is estimated from [models.dev](https://models.dev) pricing (with a vendored fallback), except OpenRouter which uses API-reported `usage.cost` when available. Local OpenAI-compatible inference (e.g. llama.cpp on localhost) is reported as `$0`. | | `--gatekeeper.openai.base_url`
`LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL` / `OPENAI_API_BASE` | `https://api.openai.com/v1` | OpenAI provider: API base URL | | `--gatekeeper.openai.template_kwargs`
`LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS` | _(none)_ | OpenAI provider: extra chat-template arguments (e.g. llama.cpp `enable_thinking`), sent as `chat_template_kwargs` | | `--gatekeeper.openrouter.base_url`
`LINUX_MCP_GATEKEEPER__OPENROUTER__BASE_URL` | `https://openrouter.ai/api/v1` | OpenRouter provider: API base URL | diff --git a/docs/guarded-command-execution.md b/docs/guarded-command-execution.md index 5fb5c623..b0ca7e57 100644 --- a/docs/guarded-command-execution.md +++ b/docs/guarded-command-execution.md @@ -145,6 +145,17 @@ LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT=my-gcp-project GOOGLE_APPLICATION_CREDENTIALS= ``` +**Gatekeeper cost estimation** + +Eval runs and gatekeeper stats report an estimated dollar cost per check. Resolution order: + +1. **API-reported cost** — OpenRouter `usage.cost` when present. +2. **Config override** — `LINUX_MCP_GATEKEEPER__COST` as `input$/token:output$/token` (useful for Vertex MaaS eval models). +3. **models.dev** — runtime pricing lookup with a vendored snapshot fallback when the network is unavailable. +4. **Local inference** — OpenAI-compatible providers pointed at `localhost` / `127.0.0.1` (e.g. llama.cpp) report `$0`. +5. **Fallback** — hardcoded rates for known eval models, then a conservative cloud default. + +Totals are **estimates** except when OpenRouter returns API-reported cost. Token counts come from each provider's usage metadata in the completion response. **Configure your client's tool policy** diff --git a/scripts/vendor_models_dev_pricing.py b/scripts/vendor_models_dev_pricing.py new file mode 100644 index 00000000..543bc7c1 --- /dev/null +++ b/scripts/vendor_models_dev_pricing.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Refresh the vendored models.dev pricing snapshot used by the gatekeeper.""" + +import json +import sys +import urllib.request + +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUTPUT_PATH = REPO_ROOT / "src/linux_mcp_server/gatekeeper/data/models_dev_fallback.json" +MODELS_DEV_URL = "https://models.dev/api.json" + +# Provider -> model IDs used by eval/gatekeeper/standard-evals.sh +WANTED: dict[str, list[str]] = { + "anthropic": ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"], + "openai": ["gpt-5.4", "gpt-oss-20b", "gpt-oss-120b"], + "google": ["gemini-2.0-flash", "gemini-3.1-pro-preview"], + "openrouter": [ + "openai/gpt-oss-120b", + "anthropic/claude-sonnet-4-6", + "google/gemma-4-26b-a4b-it", + "ibm-granite/granite-4.0-h-small", + "qwen/qwen3.5-35b-a3b", + ], +} + + +@dataclass(frozen=True) +class TokenCostPerMillion: + """USD per million tokens, matching models.dev's cost.input / cost.output fields.""" + + input: float + output: float + + def to_dict(self) -> dict[str, float]: + return {"input": self.input, "output": self.output} + + +@dataclass(frozen=True) +class ModelPricing: + """Pricing structure for a single model. Contains the cost per million tokens.""" + + cost: TokenCostPerMillion + + def to_dict(self) -> dict[str, Any]: + return {"cost": self.cost.to_dict()} + + @classmethod + def from_models_dev_entry(cls, entry: object) -> "ModelPricing | None": + if not isinstance(entry, dict): + return None + cost = entry.get("cost") + if not isinstance(cost, dict): + return None + input_mtok = cost.get("input") + output_mtok = cost.get("output") + if not isinstance(input_mtok, (int, float)) or not isinstance(output_mtok, (int, float)): + return None + return cls(cost=TokenCostPerMillion(input=float(input_mtok), output=float(output_mtok))) + + +@dataclass +class ProviderPricing: + """Pricing structure for a single provider. Maps model ID to its pricing.""" + + models: dict[str, ModelPricing] + + def to_dict(self) -> dict[str, Any]: + return {"models": {model_id: pricing.to_dict() for model_id, pricing in self.models.items()}} + + +@dataclass +class PricingSnapshot: + """Snapshot of models.dev pricing for the gatekeeper. Maps provider name to a pricing structure.""" + + providers: dict[str, ProviderPricing] + + def to_dict(self) -> dict[str, Any]: + return {provider: pricing.to_dict() for provider, pricing in self.providers.items()} + + +def _models_dev_catalog(data: dict[str, Any], provider: str) -> dict[str, Any]: + """Extract the models catalog for a given provider from the models.dev data.""" + provider_data = data.get(provider, {}) + if not isinstance(provider_data, dict): + return {} + models = provider_data.get("models", {}) + return models if isinstance(models, dict) else {} + + +def build_snapshot(data: dict[str, Any]) -> tuple[PricingSnapshot, list[str]]: + """Build a pricing snapshot from the models.dev data.""" + providers: dict[str, ProviderPricing] = {} + missing: list[str] = [] + + for provider, model_ids in WANTED.items(): + src_models = _models_dev_catalog(data, provider) + picked: dict[str, ModelPricing] = {} + for model_id in model_ids: + pricing = ModelPricing.from_models_dev_entry(src_models.get(model_id)) + if pricing is None: + missing.append(f"{provider}/{model_id}") + else: + picked[model_id] = pricing + if picked: + providers[provider] = ProviderPricing(models=picked) + + return PricingSnapshot(providers=providers), missing + + +def main() -> int: + with urllib.request.urlopen(MODELS_DEV_URL, timeout=30) as response: + data = json.load(response) + + snapshot, missing = build_snapshot(data) + + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + OUTPUT_PATH.write_text(json.dumps(snapshot.to_dict(), indent=2) + "\n", encoding="utf-8") + print(f"Wrote {OUTPUT_PATH}") + + if missing: + print("Missing pricing for:", ", ".join(missing), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/linux_mcp_server/gatekeeper/anthropic_client.py b/src/linux_mcp_server/gatekeeper/anthropic_client.py index e9131109..bf169232 100644 --- a/src/linux_mcp_server/gatekeeper/anthropic_client.py +++ b/src/linux_mcp_server/gatekeeper/anthropic_client.py @@ -10,6 +10,7 @@ from linux_mcp_server.gatekeeper.http_utils import normalize_model_id from linux_mcp_server.gatekeeper.http_utils import post_json from linux_mcp_server.gatekeeper.schema import anthropic_output_config +from linux_mcp_server.gatekeeper.usage import extract_anthropic_usage from linux_mcp_server.models import GatekeeperCompletion @@ -78,4 +79,9 @@ def complete_anthropic(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_T body=build_messages_body(prompt, include_model=True, max_tokens=max_tokens), timeout=timeout, ) - return GatekeeperCompletion(text=extract_messages_text(response)) + usage = extract_anthropic_usage(response) + return GatekeeperCompletion( + text=extract_messages_text(response), + prompt_tokens=usage.input_tokens, + completion_tokens=usage.output_tokens, + ) diff --git a/src/linux_mcp_server/gatekeeper/check_run_script.py b/src/linux_mcp_server/gatekeeper/check_run_script.py index 3546d23d..a191a929 100644 --- a/src/linux_mcp_server/gatekeeper/check_run_script.py +++ b/src/linux_mcp_server/gatekeeper/check_run_script.py @@ -7,6 +7,8 @@ from linux_mcp_server.config import CONFIG from linux_mcp_server.gatekeeper.llm import complete_gatekeeper +from linux_mcp_server.gatekeeper.pricing import compute_cost +from linux_mcp_server.gatekeeper.pricing import CostSource from linux_mcp_server.utils import StrEnum @@ -184,6 +186,7 @@ class GatekeeperStats(BaseModel): prompt_tokens: int = 0 completion_tokens: int = 0 cost: float = 0 + cost_source: CostSource | None = None latency: float = 0 @@ -193,15 +196,6 @@ def __init__(self, message: str, *, stats: GatekeeperStats | None = None): self.stats = stats -def _compute_cost(prompt_tokens: int, completion_tokens: int, *, usage_cost: float | None = None) -> float: - if usage_cost is not None: - return usage_cost - if CONFIG.gatekeeper.cost is None: - return 0.0 - input_cost, output_cost = CONFIG.gatekeeper.cost - return prompt_tokens * input_cost + completion_tokens * output_cost - - async def check_run_script_with_stats( description: str, script_type: str, script: str, *, readonly: bool ) -> tuple[GatekeeperResult, GatekeeperStats]: @@ -234,14 +228,17 @@ async def check_run_script_with_stats( except asyncio.TimeoutError: raise GatekeeperException("Timeout calling gatekeeper model") from None + cost, cost_source = compute_cost( + completion.prompt_tokens, + completion.completion_tokens, + usage_cost=completion.usage_cost, + ) + stats = GatekeeperStats( prompt_tokens=completion.prompt_tokens, completion_tokens=completion.completion_tokens, - cost=_compute_cost( - completion.prompt_tokens, - completion.completion_tokens, - usage_cost=completion.usage_cost, - ), + cost=cost, + cost_source=cost_source, latency=time.perf_counter() - time_before, ) diff --git a/src/linux_mcp_server/gatekeeper/data/models_dev_fallback.json b/src/linux_mcp_server/gatekeeper/data/models_dev_fallback.json new file mode 100644 index 00000000..327a1204 --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/data/models_dev_fallback.json @@ -0,0 +1,96 @@ +{ + "anthropic": { + "models": { + "claude-sonnet-4-6": { + "cost": { + "input": 3.0, + "output": 15.0 + } + }, + "claude-opus-4-6": { + "cost": { + "input": 5.0, + "output": 25.0 + } + }, + "claude-haiku-4-5": { + "cost": { + "input": 1.0, + "output": 5.0 + } + } + } + }, + "openai": { + "models": { + "gpt-5.4": { + "cost": { + "input": 2.5, + "output": 15.0 + } + }, + "gpt-oss-20b": { + "cost": { + "input": 0.07, + "output": 0.3 + } + }, + "gpt-oss-120b": { + "cost": { + "input": 0.15, + "output": 0.6 + } + } + } + }, + "google": { + "models": { + "gemini-2.0-flash": { + "cost": { + "input": 0.1, + "output": 0.4 + } + }, + "gemini-3.1-pro-preview": { + "cost": { + "input": 2.0, + "output": 12.0 + } + } + } + }, + "openrouter": { + "models": { + "openai/gpt-oss-120b": { + "cost": { + "input": 0.09, + "output": 0.45 + } + }, + "anthropic/claude-sonnet-4-6": { + "cost": { + "input": 3.0, + "output": 15.0 + } + }, + "google/gemma-4-26b-a4b-it": { + "cost": { + "input": 0.15, + "output": 0.6 + } + }, + "ibm-granite/granite-4.0-h-small": { + "cost": { + "input": 0.1, + "output": 0.4 + } + }, + "qwen/qwen3.5-35b-a3b": { + "cost": { + "input": 0.2, + "output": 0.8 + } + } + } + } +} diff --git a/src/linux_mcp_server/gatekeeper/gemini_client.py b/src/linux_mcp_server/gatekeeper/gemini_client.py index 95dec35e..72f5d480 100644 --- a/src/linux_mcp_server/gatekeeper/gemini_client.py +++ b/src/linux_mcp_server/gatekeeper/gemini_client.py @@ -10,6 +10,7 @@ from linux_mcp_server.gatekeeper.http_utils import normalize_model_id from linux_mcp_server.gatekeeper.http_utils import post_json from linux_mcp_server.gatekeeper.schema import gemini_generation_config +from linux_mcp_server.gatekeeper.usage import extract_gemini_usage from linux_mcp_server.models import GatekeeperCompletion @@ -73,4 +74,9 @@ def complete_gemini(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIME body=build_gemini_body(prompt, max_tokens=max_tokens), timeout=timeout, ) - return GatekeeperCompletion(text=extract_gemini_text(response)) + usage = extract_gemini_usage(response) + return GatekeeperCompletion( + text=extract_gemini_text(response), + prompt_tokens=usage.input_tokens, + completion_tokens=usage.output_tokens, + ) diff --git a/src/linux_mcp_server/gatekeeper/openai_client.py b/src/linux_mcp_server/gatekeeper/openai_client.py index a11377c2..f6a1ac8b 100644 --- a/src/linux_mcp_server/gatekeeper/openai_client.py +++ b/src/linux_mcp_server/gatekeeper/openai_client.py @@ -13,6 +13,8 @@ from linux_mcp_server.gatekeeper.http_utils import post_json from linux_mcp_server.gatekeeper.schema import openai_response_format from linux_mcp_server.gatekeeper.schema import openai_text_format +from linux_mcp_server.gatekeeper.usage import extract_openai_chat_completions_usage +from linux_mcp_server.gatekeeper.usage import extract_openai_responses_usage from linux_mcp_server.models import GatekeeperCompletion @@ -138,7 +140,12 @@ def complete_openai(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIME body=_build_responses_body(prompt, max_tokens=max_tokens), timeout=timeout, ) - return GatekeeperCompletion(text=_extract_responses_text(response)) + usage = extract_openai_responses_usage(response) + return GatekeeperCompletion( + text=_extract_responses_text(response), + prompt_tokens=usage.input_tokens, + completion_tokens=usage.output_tokens, + ) except GatekeeperHTTPError as exc: if exc.status_code not in {404, 405}: raise @@ -150,4 +157,9 @@ def complete_openai(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIME body=build_chat_completions_body(prompt, max_tokens=max_tokens), timeout=timeout, ) - return GatekeeperCompletion(text=extract_chat_completions_text(response)) + usage = extract_openai_chat_completions_usage(response) + return GatekeeperCompletion( + text=extract_chat_completions_text(response), + prompt_tokens=usage.input_tokens, + completion_tokens=usage.output_tokens, + ) diff --git a/src/linux_mcp_server/gatekeeper/openrouter_client.py b/src/linux_mcp_server/gatekeeper/openrouter_client.py index adf7c645..636fd781 100644 --- a/src/linux_mcp_server/gatekeeper/openrouter_client.py +++ b/src/linux_mcp_server/gatekeeper/openrouter_client.py @@ -9,6 +9,7 @@ from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS from linux_mcp_server.gatekeeper.http_utils import post_json from linux_mcp_server.gatekeeper.schema import openai_response_format +from linux_mcp_server.gatekeeper.usage import extract_openrouter_usage from linux_mcp_server.models import GatekeeperCompletion @@ -86,20 +87,6 @@ def _extract_chat_completions_text(response: dict[str, Any]) -> str: return (content or "").strip() if isinstance(content, str) else "" -def _extract_usage(response: dict[str, Any]) -> tuple[int, int, float | None]: - usage = response.get("usage", {}) - if not isinstance(usage, dict): - return 0, 0, None - prompt_tokens = usage.get("prompt_tokens", 0) - completion_tokens = usage.get("completion_tokens", 0) - cost = usage.get("cost") - return ( - int(prompt_tokens) if isinstance(prompt_tokens, int) else 0, - int(completion_tokens) if isinstance(completion_tokens, int) else 0, - float(cost) if isinstance(cost, (int, float)) else None, - ) - - def complete_openrouter( prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS ) -> GatekeeperCompletion: @@ -115,10 +102,10 @@ def complete_openrouter( body=_build_chat_completions_body(prompt, max_tokens=max_tokens), timeout=timeout, ) - prompt_tokens, completion_tokens, usage_cost = _extract_usage(response) + usage = extract_openrouter_usage(response) return GatekeeperCompletion( text=_extract_chat_completions_text(response), - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, - usage_cost=usage_cost, + prompt_tokens=usage.input_tokens, + completion_tokens=usage.output_tokens, + usage_cost=usage.cost, ) diff --git a/src/linux_mcp_server/gatekeeper/pricing.py b/src/linux_mcp_server/gatekeeper/pricing.py new file mode 100644 index 00000000..a10ee2f2 --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/pricing.py @@ -0,0 +1,189 @@ +"""Gatekeeper cost estimation from usage tokens and pricing tables.""" + +import json +import logging +import os + +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + +import requests + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.gatekeeper.http_utils import normalize_model_id +from linux_mcp_server.gatekeeper.llm import resolve_provider +from linux_mcp_server.models import CostSource +from linux_mcp_server.models import TokenRates + + +logger = logging.getLogger("linux-mcp-server") + + +MODELS_DEV_API_URL = "https://models.dev/api.json" +FALLBACK_PRICING_PATH = Path(__file__).resolve().parent / "data" / "models_dev_fallback.json" + +FALLBACK_COST_PER_MTOK: dict[str, tuple[float, float]] = { + "gemma-4-26b-a4b-it-maas": (0.15, 0.60), + "gpt-oss-20b-maas": (0.07, 0.25), + "gpt-oss-120b-maas": (0.09, 0.36), +} + +DEFAULT_CLOUD_COST_PER_MTOK = (3.0, 15.0) + +_LOCAL_HOSTS = frozenset({"localhost", "127.0.0.1", "::1"}) + +_models_dev_cache: dict[str, Any] | None = None + + +def _rates_from_mtok(input_mtok: float, output_mtok: float, source: CostSource) -> TokenRates: + return TokenRates( + input_per_token=input_mtok / 1_000_000, + output_per_token=output_mtok / 1_000_000, + source=source, + ) + + +def _model_lookup_candidates(model: str) -> list[str]: + """Creates a list of variant model IDs based on the given model ID.""" + normalized = normalize_model_id(model) + candidates = [normalized, model] + if normalized.endswith("-maas"): + candidates.append(normalized[: -len("-maas")]) + if "/" in normalized: + candidates.append(normalized.split("/", 1)[1]) + seen: set[str] = set() + ordered: list[str] = [] + for candidate in candidates: + if candidate and candidate not in seen: + seen.add(candidate) + ordered.append(candidate) + return ordered + + +def _models_dev_provider_key(provider: GatekeeperProvider, model: str) -> str: + """Maps the provider to the corresponding models.dev provider name.""" + match provider: + case GatekeeperProvider.GEMINI: + return "google" + case GatekeeperProvider.VERTEX_AI: + normalized = normalize_model_id(model) + if normalized.startswith("claude"): + return "anthropic" + if normalized.startswith("gemini"): + return "google" + return "openai" + case _: + return provider.value + + +def _load_models_dev_fallback() -> dict[str, Any]: + """Loads the fallback pricing from the vendored models.dev pricing snapshot.""" + with FALLBACK_PRICING_PATH.open(encoding="utf-8") as handle: + return json.load(handle) + + +def _load_models_dev_pricing() -> dict[str, Any]: + """Loads the pricing from the models.dev API, falling back to the vendored fallback if the API is not available.""" + global _models_dev_cache + if _models_dev_cache is not None: + return _models_dev_cache + + try: + response = requests.get(MODELS_DEV_API_URL, timeout=10) + response.raise_for_status() + _models_dev_cache = response.json() + logger.debug("Loaded gatekeeper pricing from models.dev API") + except Exception as exc: + logger.debug("Failed to fetch models.dev pricing (%s); using vendored fallback", exc) + _models_dev_cache = _load_models_dev_fallback() + + assert _models_dev_cache is not None + return _models_dev_cache + + +def _lookup_models_dev_cost(provider_key: str, model: str) -> tuple[float, float] | None: + """Looks up the cost per million tokens for a given model and provider in the models.dev pricing.""" + pricing = _load_models_dev_pricing() + provider_data = pricing.get(provider_key, {}) + if not isinstance(provider_data, dict): + return None + models = provider_data.get("models", {}) + if not isinstance(models, dict): + return None + + for candidate in _model_lookup_candidates(model): + entry = models.get(candidate) + if not isinstance(entry, dict): + continue + cost = entry.get("cost") + if not isinstance(cost, dict): + continue + input_mtok = cost.get("input") + output_mtok = cost.get("output") + if isinstance(input_mtok, (int, float)) and isinstance(output_mtok, (int, float)): + return float(input_mtok), float(output_mtok) + return None + + +def is_local_inference() -> bool: + provider = resolve_provider() + if provider != GatekeeperProvider.OPENAI: + return False + base_url = None + if CONFIG.gatekeeper.openai and CONFIG.gatekeeper.openai.base_url: + base_url = CONFIG.gatekeeper.openai.base_url + else: + base_url = os.environ.get("OPENAI_API_BASE") + if not base_url: + return False + host = (urlparse(base_url).hostname or "").lower() + return host in _LOCAL_HOSTS + + +def resolve_token_rates() -> TokenRates: + if CONFIG.gatekeeper.cost is not None: + input_per_token, output_per_token = CONFIG.gatekeeper.cost + return TokenRates( + input_per_token=input_per_token, + output_per_token=output_per_token, + source="config", + ) + + provider = resolve_provider() + model = CONFIG.gatekeeper.model or "" + provider_key = _models_dev_provider_key(provider, model) + models_dev_cost = _lookup_models_dev_cost(provider_key, model) + if models_dev_cost is not None: + return _rates_from_mtok(models_dev_cost[0], models_dev_cost[1], "models_dev") + + for candidate in _model_lookup_candidates(model): + hardcoded = FALLBACK_COST_PER_MTOK.get(candidate) + if hardcoded is not None: + return _rates_from_mtok(hardcoded[0], hardcoded[1], "fallback") + + if is_local_inference(): + return _rates_from_mtok(0.0, 0.0, "local") + + return _rates_from_mtok(DEFAULT_CLOUD_COST_PER_MTOK[0], DEFAULT_CLOUD_COST_PER_MTOK[1], "fallback") + + +def compute_cost( + prompt_tokens: int, + completion_tokens: int, + *, + usage_cost: float | None, +) -> tuple[float, CostSource]: + if usage_cost is not None: + return usage_cost, "api" + + rates = resolve_token_rates() + cost = prompt_tokens * rates.input_per_token + completion_tokens * rates.output_per_token + return cost, rates.source + + +def reset_models_dev_cache() -> None: + """Clear the in-memory models.dev cache (for tests).""" + global _models_dev_cache + _models_dev_cache = None diff --git a/src/linux_mcp_server/gatekeeper/usage.py b/src/linux_mcp_server/gatekeeper/usage.py new file mode 100644 index 00000000..6c60d714 --- /dev/null +++ b/src/linux_mcp_server/gatekeeper/usage.py @@ -0,0 +1,63 @@ +"""Token and cost extraction from gatekeeper LLM API responses.""" + +from typing import Any + +from linux_mcp_server.models import Usage + + +def extract_openai_chat_completions_usage(response: dict[str, Any]) -> Usage: + """Extract prompt and completion tokens from OpenAI chat completions response.""" + usage = response.get("usage", {}) + if not isinstance(usage, dict): + return Usage() + return Usage( + input_tokens=int(usage.get("prompt_tokens") or 0), + output_tokens=int(usage.get("completion_tokens") or 0), + ) + + +def extract_openai_responses_usage(response: dict[str, Any]) -> Usage: + """Extract input and output tokens from OpenAI responses response.""" + usage = response.get("usage", {}) + if not isinstance(usage, dict): + return Usage() + return Usage( + input_tokens=int(usage.get("input_tokens") or 0), + output_tokens=int(usage.get("output_tokens") or 0), + ) + + +def extract_anthropic_usage(response: dict[str, Any]) -> Usage: + """Extract input and output tokens from Anthropic response.""" + usage = response.get("usage", {}) + if not isinstance(usage, dict): + return Usage() + return Usage( + input_tokens=int(usage.get("input_tokens") or 0), + output_tokens=int(usage.get("output_tokens") or 0), + ) + + +def extract_gemini_usage(response: dict[str, Any]) -> Usage: + """Extract prompt and candidate tokens from Gemini response.""" + usage = response.get("usageMetadata", {}) + if not isinstance(usage, dict): + return Usage() + return Usage( + input_tokens=int(usage.get("promptTokenCount") or 0), + output_tokens=int(usage.get("candidatesTokenCount") or 0), + ) + + +def extract_openrouter_usage(response: dict[str, Any]) -> Usage: + """Extract prompt and completion tokens from OpenRouter response, and cost if available.""" + usage = response.get("usage", {}) + if not isinstance(usage, dict): + return Usage() + cost_raw = usage.get("cost") + cost = float(cost_raw) if isinstance(cost_raw, (int, float)) else None + return Usage( + input_tokens=int(usage.get("prompt_tokens") or 0), + output_tokens=int(usage.get("completion_tokens") or 0), + cost=cost, + ) diff --git a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py index 0635688f..16987fa9 100644 --- a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py +++ b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py @@ -14,6 +14,9 @@ from linux_mcp_server.gatekeeper.http_utils import post_json from linux_mcp_server.gatekeeper.openai_client import build_chat_completions_body from linux_mcp_server.gatekeeper.openai_client import extract_chat_completions_text +from linux_mcp_server.gatekeeper.usage import extract_anthropic_usage +from linux_mcp_server.gatekeeper.usage import extract_gemini_usage +from linux_mcp_server.gatekeeper.usage import extract_openai_chat_completions_usage from linux_mcp_server.models import GatekeeperCompletion @@ -72,7 +75,12 @@ def _complete_anthropic_on_vertex(prompt: str, *, max_tokens: int, timeout: int) body=body, timeout=timeout, ) - return GatekeeperCompletion(text=extract_messages_text(response)) + usage = extract_anthropic_usage(response) + return GatekeeperCompletion( + text=extract_messages_text(response), + prompt_tokens=usage.input_tokens, + completion_tokens=usage.output_tokens, + ) def _complete_gemini_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: @@ -84,7 +92,12 @@ def _complete_gemini_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> body=build_gemini_body(prompt, max_tokens=max_tokens), timeout=timeout, ) - return GatekeeperCompletion(text=extract_gemini_text(response)) + usage = extract_gemini_usage(response) + return GatekeeperCompletion( + text=extract_gemini_text(response), + prompt_tokens=usage.input_tokens, + completion_tokens=usage.output_tokens, + ) def _complete_openai_compatible_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: @@ -96,7 +109,12 @@ def _complete_openai_compatible_on_vertex(prompt: str, *, max_tokens: int, timeo body=build_chat_completions_body(prompt, max_tokens=max_tokens), timeout=timeout, ) - return GatekeeperCompletion(text=extract_chat_completions_text(response)) + usage = extract_openai_chat_completions_usage(response) + return GatekeeperCompletion( + text=extract_chat_completions_text(response), + prompt_tokens=usage.input_tokens, + completion_tokens=usage.output_tokens, + ) def complete_vertex_ai(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: diff --git a/src/linux_mcp_server/models.py b/src/linux_mcp_server/models.py index dbcc04b2..8a019812 100644 --- a/src/linux_mcp_server/models.py +++ b/src/linux_mcp_server/models.py @@ -2,8 +2,10 @@ from datetime import datetime from pathlib import Path +from typing import Literal from pydantic import BaseModel +from pydantic import ConfigDict from pydantic import Field from pydantic import field_serializer from pydantic import model_validator @@ -205,3 +207,21 @@ class GatekeeperCompletion(BaseModel): prompt_tokens: int = 0 completion_tokens: int = 0 usage_cost: float | None = None + + +### Cost models ### +CostSource = Literal["api", "config", "models_dev", "fallback", "local"] + + +class Usage(BaseModel): + input_tokens: int = 0 + output_tokens: int = 0 + cost: float | None = None + + +class TokenRates(BaseModel): + model_config = ConfigDict(frozen=True) + + input_per_token: float + output_per_token: float + source: CostSource diff --git a/tests/gatekeeper/test_anthropic_client.py b/tests/gatekeeper/test_anthropic_client.py index 7e7365d2..d142b0bb 100644 --- a/tests/gatekeeper/test_anthropic_client.py +++ b/tests/gatekeeper/test_anthropic_client.py @@ -29,12 +29,17 @@ def gatekeeper_config(self, mocker): def test_complete_anthropic_direct(self, gatekeeper_config, mocker): mock_post = mocker.patch( "linux_mcp_server.gatekeeper.anthropic_client.post_json", - return_value={"content": [{"type": "text", "text": '{"status": "OK", "detail": ""}'}]}, + return_value={ + "content": [{"type": "text", "text": '{"status": "OK", "detail": ""}'}], + "usage": {"input_tokens": 30, "output_tokens": 10}, + }, ) result = anthropic_client.complete_anthropic("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' + assert result.prompt_tokens == 30 + assert result.completion_tokens == 10 assert mock_post.call_args.kwargs["url"] == "https://api.anthropic.com/v1/messages" body = mock_post.call_args.kwargs["body"] assert body["model"] == "claude-sonnet-4-6" diff --git a/tests/gatekeeper/test_check_run_script.py b/tests/gatekeeper/test_check_run_script.py index ab30b172..78be4462 100644 --- a/tests/gatekeeper/test_check_run_script.py +++ b/tests/gatekeeper/test_check_run_script.py @@ -166,6 +166,7 @@ async def test_custom_cost(self, mocker): ) assert stats.cost == pytest.approx(100 * 1e-6 + 50 * 4e-6) + assert stats.cost_source == "config" async def test_openrouter_usage_cost(self, mocker): mocker.patch.object( @@ -186,6 +187,7 @@ async def test_openrouter_usage_cost(self, mocker): assert stats.prompt_tokens == 10 assert stats.completion_tokens == 5 assert stats.cost == 0.42 + assert stats.cost_source == "api" class TestGatekeeperConfigIntegration: diff --git a/tests/gatekeeper/test_gemini_client.py b/tests/gatekeeper/test_gemini_client.py index b62920a8..494ca8cb 100644 --- a/tests/gatekeeper/test_gemini_client.py +++ b/tests/gatekeeper/test_gemini_client.py @@ -29,12 +29,17 @@ def gatekeeper_config(self, mocker): def test_complete_gemini_google_ai(self, gatekeeper_config, mocker): mock_post = mocker.patch( "linux_mcp_server.gatekeeper.gemini_client.post_json", - return_value={"candidates": [{"content": {"parts": [{"text": '{"status": "OK"}'}]}}]}, + return_value={ + "candidates": [{"content": {"parts": [{"text": '{"status": "OK"}'}]}}], + "usageMetadata": {"promptTokenCount": 15, "candidatesTokenCount": 6}, + }, ) result = gemini_client.complete_gemini("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' + assert result.prompt_tokens == 15 + assert result.completion_tokens == 6 url = mock_post.call_args.kwargs["url"] assert "generativelanguage.googleapis.com" in url assert "key=test-key" in url diff --git a/tests/gatekeeper/test_openai_client.py b/tests/gatekeeper/test_openai_client.py index d625eb5a..ee182eff 100644 --- a/tests/gatekeeper/test_openai_client.py +++ b/tests/gatekeeper/test_openai_client.py @@ -53,12 +53,17 @@ def gatekeeper_config(self, mocker): def test_complete_openai_uses_responses_api(self, gatekeeper_config, mocker): mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", - return_value={"output_text": '{"status": "OK", "detail": ""}'}, + return_value={ + "output_text": '{"status": "OK", "detail": ""}', + "usage": {"input_tokens": 11, "output_tokens": 4}, + }, ) result = openai_client.complete_openai("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' + assert result.prompt_tokens == 11 + assert result.completion_tokens == 4 assert mock_post.call_args.kwargs["url"] == "https://api.openai.com/v1/responses" body = mock_post.call_args.kwargs["body"] assert body["model"] == "gpt-5.4" @@ -83,13 +88,18 @@ def test_complete_openai_falls_back_to_chat_completions_on_404(self, gatekeeper_ "linux_mcp_server.gatekeeper.openai_client.post_json", side_effect=[ GatekeeperHTTPError("openai", 404, "not found"), - {"choices": [{"message": {"content": '{"status": "OK", "detail": ""}'}}]}, + { + "choices": [{"message": {"content": '{"status": "OK", "detail": ""}'}}], + "usage": {"prompt_tokens": 9, "completion_tokens": 2}, + }, ], ) result = openai_client.complete_openai("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' + assert result.prompt_tokens == 9 + assert result.completion_tokens == 2 assert mock_post.call_args_list[0].kwargs["url"] == "https://models.example.com/v1/responses" assert mock_post.call_args_list[1].kwargs["url"] == "https://models.example.com/v1/chat/completions" body = mock_post.call_args_list[1].kwargs["body"] diff --git a/tests/gatekeeper/test_pricing.py b/tests/gatekeeper/test_pricing.py new file mode 100644 index 00000000..2fa1882d --- /dev/null +++ b/tests/gatekeeper/test_pricing.py @@ -0,0 +1,75 @@ +import pytest + +from linux_mcp_server.config import CONFIG +from linux_mcp_server.config import GatekeeperConfig +from linux_mcp_server.config import GatekeeperProvider +from linux_mcp_server.config import OpenAIGatekeeperConfig +from linux_mcp_server.gatekeeper import pricing + + +@pytest.fixture(autouse=True) +def reset_pricing_cache(): + pricing.reset_models_dev_cache() + yield + pricing.reset_models_dev_cache() + + +@pytest.fixture +def gatekeeper_config(mocker): + config = GatekeeperConfig(provider=GatekeeperProvider.ANTHROPIC, model="claude-sonnet-4-6") + mocker.patch.object(CONFIG, "gatekeeper", config) + return config + + +class TestComputeCost: + def test_api_usage_cost(self): + cost, source = pricing.compute_cost(10, 5, usage_cost=0.42) + assert cost == 0.42 + assert source == "api" + + def test_config_override(self, gatekeeper_config, mocker): + gatekeeper_config.cost = (1e-6, 4e-6) + cost, source = pricing.compute_cost(100, 50, usage_cost=None) + assert cost == pytest.approx(100 * 1e-6 + 50 * 4e-6) + assert source == "config" + + def test_models_dev_lookup(self, gatekeeper_config, mocker): + gatekeeper_config.cost = None + mocker.patch.object(pricing, "_load_models_dev_pricing", return_value=pricing._load_models_dev_fallback()) + cost, source = pricing.compute_cost(1_000_000, 1_000_000, usage_cost=None) + assert cost == pytest.approx(3.0 + 15.0) + assert source == "models_dev" + + def test_hardcoded_maas_fallback(self, gatekeeper_config, mocker): + gatekeeper_config.provider = GatekeeperProvider.VERTEX_AI + gatekeeper_config.model = "gemma-4-26b-a4b-it-maas" + gatekeeper_config.cost = None + mocker.patch.object(pricing, "_load_models_dev_pricing", return_value={}) + cost, source = pricing.compute_cost(1_000_000, 1_000_000, usage_cost=None) + assert cost == pytest.approx(0.15 + 0.60) + assert source == "fallback" + + def test_local_inference_is_zero(self, gatekeeper_config, mocker): + gatekeeper_config.provider = GatekeeperProvider.OPENAI + gatekeeper_config.model = "google/gemma-4-26b-a4b" + gatekeeper_config.cost = None + gatekeeper_config.openai = OpenAIGatekeeperConfig(base_url="http://localhost:8080/v1") + mocker.patch.object(pricing, "_load_models_dev_pricing", return_value={}) + cost, source = pricing.compute_cost(1_000, 1_000, usage_cost=None) + assert cost == 0.0 + assert source == "local" + + def test_cloud_unknown_uses_conservative_default(self, gatekeeper_config, mocker): + gatekeeper_config.model = "unknown-model-xyz" + gatekeeper_config.cost = None + mocker.patch.object(pricing, "_load_models_dev_pricing", return_value={}) + cost, source = pricing.compute_cost(1_000_000, 1_000_000, usage_cost=None) + assert cost == pytest.approx(3.0 + 15.0) + assert source == "fallback" + + def test_fetch_failure_uses_vendored_fallback(self, gatekeeper_config, mocker): + gatekeeper_config.cost = None + mocker.patch("linux_mcp_server.gatekeeper.pricing.requests.get", side_effect=OSError("offline")) + cost, source = pricing.compute_cost(1_000_000, 0, usage_cost=None) + assert cost == pytest.approx(3.0) + assert source == "models_dev" diff --git a/tests/gatekeeper/test_usage.py b/tests/gatekeeper/test_usage.py new file mode 100644 index 00000000..48c24f54 --- /dev/null +++ b/tests/gatekeeper/test_usage.py @@ -0,0 +1,48 @@ +import pytest + +from linux_mcp_server.gatekeeper import usage +from linux_mcp_server.models import Usage + + +class TestUsageExtractors: + @pytest.mark.parametrize( + ("extractor", "response", "expected"), + [ + ( + usage.extract_openai_chat_completions_usage, + {"usage": {"prompt_tokens": 10, "completion_tokens": 5}}, + Usage(input_tokens=10, output_tokens=5), + ), + ( + usage.extract_openai_responses_usage, + {"usage": {"input_tokens": 12, "output_tokens": 3}}, + Usage(input_tokens=12, output_tokens=3), + ), + ( + usage.extract_anthropic_usage, + {"usage": {"input_tokens": 20, "output_tokens": 8}}, + Usage(input_tokens=20, output_tokens=8), + ), + ( + usage.extract_gemini_usage, + {"usageMetadata": {"promptTokenCount": 7, "candidatesTokenCount": 2}}, + Usage(input_tokens=7, output_tokens=2), + ), + ], + ) + def test_extractors(self, extractor, response, expected): + assert extractor(response) == expected + + def test_missing_usage_returns_zeros(self): + assert usage.extract_openai_chat_completions_usage({}) == Usage() + assert usage.extract_anthropic_usage({"usage": "bad"}) == Usage() + + def test_openrouter_usage_includes_cost(self): + assert usage.extract_openrouter_usage( + {"usage": {"prompt_tokens": 10, "completion_tokens": 5, "cost": 0.001}} + ) == Usage(input_tokens=10, output_tokens=5, cost=0.001) + + def test_openrouter_usage_missing_cost(self): + assert usage.extract_openrouter_usage({"usage": {"prompt_tokens": 1, "completion_tokens": 2}}) == Usage( + input_tokens=1, output_tokens=2, cost=None + ) From 041db481b95bbd83adb644818bc089954f921dca Mon Sep 17 00:00:00 2001 From: Link Dupont Date: Thu, 18 Jun 2026 11:39:16 -0400 Subject: [PATCH 6/7] feat(gatekeeper): refactor to async functions and update dependencies - Changed dependency from `requests` to `httpx`. - Updated HTTP client from `requests` to `httpx` for asynchronous capabilities. - Converted completion functions for OpenAI, Anthropic, Gemini, OpenRouter, and Vertex AI to async. - Added a new check in documentation to ensure async functions are only decorated when `asyncio_mode` is not set to auto. --- AGENTS.md | 1 + pyproject.toml | 2 +- .../gatekeeper/anthropic_client.py | 6 ++- .../gatekeeper/check_run_script.py | 2 +- .../gatekeeper/gemini_client.py | 6 ++- src/linux_mcp_server/gatekeeper/http_utils.py | 10 +++-- src/linux_mcp_server/gatekeeper/llm.py | 12 +++--- .../gatekeeper/openai_client.py | 8 ++-- .../gatekeeper/openrouter_client.py | 4 +- src/linux_mcp_server/gatekeeper/pricing.py | 23 +++++------ .../gatekeeper/vertex_ai_client.py | 22 ++++++----- tests/gatekeeper/test_anthropic_client.py | 5 ++- tests/gatekeeper/test_check_run_script.py | 18 +++++---- tests/gatekeeper/test_gemini_client.py | 5 ++- tests/gatekeeper/test_http_utils.py | 39 +++++++++++++++++++ tests/gatekeeper/test_llm.py | 25 +++++++----- tests/gatekeeper/test_openai_client.py | 25 +++++++----- tests/gatekeeper/test_openrouter_client.py | 35 ++++++++++------- tests/gatekeeper/test_pricing.py | 2 +- tests/gatekeeper/test_vertex_ai_client.py | 20 ++++++---- uv.lock | 4 +- 21 files changed, 175 insertions(+), 99 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a100430a..4c4ae87b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,7 @@ make verify # All checks (required before commit) - Use pytest-mock (`mocker` fixture) for mocking instead of `unittest.mock` imports - Use `autospec=True` when patching; `spec=` with Mock - 100% patch coverage for new code +- Check the project config and test runners and only decorate async functions if the ascyio_mode is not set to auto. **Security (Critical):** - All tools must be read-only with `readOnlyHint=True` diff --git a/pyproject.toml b/pyproject.toml index 4b6876dd..bb255a63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ requires-python = ">=3.10" dependencies = [ "asyncssh[bcrypt] >= 2.22.0", "fastmcp >= 3.2.4", - "requests>=2.32.0", + "httpx>=0.28.1", "pydantic-settings >= 2.12.0", "pydantic >= 2.12.5", # pydocket is incompatible with newer versions of fakeredis. diff --git a/src/linux_mcp_server/gatekeeper/anthropic_client.py b/src/linux_mcp_server/gatekeeper/anthropic_client.py index bf169232..e2c4ee97 100644 --- a/src/linux_mcp_server/gatekeeper/anthropic_client.py +++ b/src/linux_mcp_server/gatekeeper/anthropic_client.py @@ -66,13 +66,15 @@ def extract_messages_text(response: dict[str, Any]) -> str: return "" -def complete_anthropic(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: +async def complete_anthropic( + prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS +) -> GatekeeperCompletion: headers = { "x-api-key": _get_anthropic_api_key(), "anthropic-version": ANTHROPIC_API_VERSION, "Content-Type": "application/json", } - response = post_json( + response = await post_json( provider="anthropic", url=ANTHROPIC_API_URL, headers=headers, diff --git a/src/linux_mcp_server/gatekeeper/check_run_script.py b/src/linux_mcp_server/gatekeeper/check_run_script.py index a191a929..1200ab14 100644 --- a/src/linux_mcp_server/gatekeeper/check_run_script.py +++ b/src/linux_mcp_server/gatekeeper/check_run_script.py @@ -222,7 +222,7 @@ async def check_run_script_with_stats( time_before = time.perf_counter() try: completion = await asyncio.wait_for( - asyncio.to_thread(complete_gatekeeper, prompt, max_tokens=GATEKEEPER_MAX_TOKENS), + complete_gatekeeper(prompt, max_tokens=GATEKEEPER_MAX_TOKENS), timeout=GATEKEEPER_TIMEOUT, ) except asyncio.TimeoutError: diff --git a/src/linux_mcp_server/gatekeeper/gemini_client.py b/src/linux_mcp_server/gatekeeper/gemini_client.py index 72f5d480..ee49e4b3 100644 --- a/src/linux_mcp_server/gatekeeper/gemini_client.py +++ b/src/linux_mcp_server/gatekeeper/gemini_client.py @@ -64,10 +64,12 @@ def extract_gemini_text(response: dict[str, Any]) -> str: return text.strip() if isinstance(text, str) else "" -def complete_gemini(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: +async def complete_gemini( + prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS +) -> GatekeeperCompletion: model = normalize_model_id(CONFIG.gatekeeper.model or "") api_key = _get_google_api_key() - response = post_json( + response = await post_json( provider="gemini", url=f"{GOOGLE_AI_BASE_URL}/models/{model}:generateContent?key={api_key}", headers={"Content-Type": "application/json"}, diff --git a/src/linux_mcp_server/gatekeeper/http_utils.py b/src/linux_mcp_server/gatekeeper/http_utils.py index 9fefd75a..67c0caf7 100644 --- a/src/linux_mcp_server/gatekeeper/http_utils.py +++ b/src/linux_mcp_server/gatekeeper/http_utils.py @@ -2,11 +2,13 @@ from typing import Any -import requests +import httpx DEFAULT_TIMEOUT_SECONDS = 120 +HTTP_CLIENT = httpx.AsyncClient() + class GatekeeperHTTPError(RuntimeError): """Raised when an LLM provider returns an error response.""" @@ -19,7 +21,7 @@ def __init__(self, provider: str, status_code: int, body: str): self.body = body -def post_json( +async def post_json( *, provider: str, url: str, @@ -27,8 +29,8 @@ def post_json( body: dict[str, Any], timeout: int = DEFAULT_TIMEOUT_SECONDS, ) -> dict[str, Any]: - response = requests.post(url, headers=headers, json=body, timeout=timeout) - if not response.ok: + response = await HTTP_CLIENT.post(url, headers=headers, json=body, timeout=timeout) + if not response.is_success: raise GatekeeperHTTPError(provider, response.status_code, response.text) return response.json() diff --git a/src/linux_mcp_server/gatekeeper/llm.py b/src/linux_mcp_server/gatekeeper/llm.py index 0378bd4c..161c656e 100644 --- a/src/linux_mcp_server/gatekeeper/llm.py +++ b/src/linux_mcp_server/gatekeeper/llm.py @@ -33,19 +33,19 @@ def resolve_provider() -> GatekeeperProvider: return _infer_provider_from_model(CONFIG.gatekeeper.model) -def complete_gatekeeper(prompt: str, *, max_tokens: int) -> GatekeeperCompletion: +async def complete_gatekeeper(prompt: str, *, max_tokens: int) -> GatekeeperCompletion: provider = resolve_provider() match provider: case GatekeeperProvider.OPENAI: - completion = complete_openai(prompt, max_tokens=max_tokens) + completion = await complete_openai(prompt, max_tokens=max_tokens) case GatekeeperProvider.ANTHROPIC: - completion = complete_anthropic(prompt, max_tokens=max_tokens) + completion = await complete_anthropic(prompt, max_tokens=max_tokens) case GatekeeperProvider.GEMINI: - completion = complete_gemini(prompt, max_tokens=max_tokens) + completion = await complete_gemini(prompt, max_tokens=max_tokens) case GatekeeperProvider.OPENROUTER: - completion = complete_openrouter(prompt, max_tokens=max_tokens) + completion = await complete_openrouter(prompt, max_tokens=max_tokens) case GatekeeperProvider.VERTEX_AI: - completion = complete_vertex_ai(prompt, max_tokens=max_tokens) + completion = await complete_vertex_ai(prompt, max_tokens=max_tokens) case _: # pragma: no cover raise ValueError(f"Unsupported gatekeeper provider: {provider}") diff --git a/src/linux_mcp_server/gatekeeper/openai_client.py b/src/linux_mcp_server/gatekeeper/openai_client.py index f6a1ac8b..6a803e5c 100644 --- a/src/linux_mcp_server/gatekeeper/openai_client.py +++ b/src/linux_mcp_server/gatekeeper/openai_client.py @@ -123,7 +123,9 @@ def extract_chat_completions_text(response: dict[str, Any]) -> str: return (content or "").strip() if isinstance(content, str) else "" -def complete_openai(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: +async def complete_openai( + prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS +) -> GatekeeperCompletion: base_url = _get_openai_base_url() headers = { **_openai_auth_headers(), @@ -133,7 +135,7 @@ def complete_openai(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIME # Try the Responses API first, falling back to Chat Completions if it's not available. if not _prefers_openai_chat_completions(base_url): try: - response = post_json( + response = await post_json( provider="openai", url=f"{base_url}/responses", headers=headers, @@ -150,7 +152,7 @@ def complete_openai(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIME if exc.status_code not in {404, 405}: raise - response = post_json( + response = await post_json( provider="openai", url=f"{base_url}/chat/completions", headers=headers, diff --git a/src/linux_mcp_server/gatekeeper/openrouter_client.py b/src/linux_mcp_server/gatekeeper/openrouter_client.py index 636fd781..993f396b 100644 --- a/src/linux_mcp_server/gatekeeper/openrouter_client.py +++ b/src/linux_mcp_server/gatekeeper/openrouter_client.py @@ -87,7 +87,7 @@ def _extract_chat_completions_text(response: dict[str, Any]) -> str: return (content or "").strip() if isinstance(content, str) else "" -def complete_openrouter( +async def complete_openrouter( prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS ) -> GatekeeperCompletion: base_url = _get_openrouter_base_url() @@ -95,7 +95,7 @@ def complete_openrouter( **_openrouter_auth_headers(), "Content-Type": "application/json", } - response = post_json( + response = await post_json( provider="openrouter", url=f"{base_url}/chat/completions", headers=headers, diff --git a/src/linux_mcp_server/gatekeeper/pricing.py b/src/linux_mcp_server/gatekeeper/pricing.py index a10ee2f2..e94a398c 100644 --- a/src/linux_mcp_server/gatekeeper/pricing.py +++ b/src/linux_mcp_server/gatekeeper/pricing.py @@ -4,11 +4,12 @@ import logging import os +from functools import cache from pathlib import Path from typing import Any from urllib.parse import urlparse -import requests +import httpx from linux_mcp_server.config import CONFIG from linux_mcp_server.config import GatekeeperProvider @@ -84,23 +85,19 @@ def _load_models_dev_fallback() -> dict[str, Any]: return json.load(handle) +@cache def _load_models_dev_pricing() -> dict[str, Any]: """Loads the pricing from the models.dev API, falling back to the vendored fallback if the API is not available.""" - global _models_dev_cache - if _models_dev_cache is not None: - return _models_dev_cache - try: - response = requests.get(MODELS_DEV_API_URL, timeout=10) - response.raise_for_status() - _models_dev_cache = response.json() - logger.debug("Loaded gatekeeper pricing from models.dev API") + with httpx.Client(timeout=10) as client: + response = client.get(MODELS_DEV_API_URL) + response.raise_for_status() + pricing = response.json() + logger.debug("Loaded gatekeeper pricing from models.dev API", extra={"pricing": pricing}) + return pricing except Exception as exc: logger.debug("Failed to fetch models.dev pricing (%s); using vendored fallback", exc) - _models_dev_cache = _load_models_dev_fallback() - - assert _models_dev_cache is not None - return _models_dev_cache + return _load_models_dev_fallback() def _lookup_models_dev_cost(provider_key: str, model: str) -> tuple[float, float] | None: diff --git a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py index 16987fa9..32bdb73c 100644 --- a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py +++ b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py @@ -64,11 +64,11 @@ def _gemini_vertex_url(model: str) -> str: return f"https://{host}/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent" -def _complete_anthropic_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: +async def _complete_anthropic_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: model = normalize_model_id(CONFIG.gatekeeper.model or "") body = build_messages_body(prompt, include_model=False, max_tokens=max_tokens) body["anthropic_version"] = ANTHROPIC_VERTEX_VERSION - response = post_json( + response = await post_json( provider="anthropic", url=_anthropic_vertex_url(model), headers={**_vertex_auth_headers(), "Content-Type": "application/json"}, @@ -83,9 +83,9 @@ def _complete_anthropic_on_vertex(prompt: str, *, max_tokens: int, timeout: int) ) -def _complete_gemini_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: +async def _complete_gemini_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: model = normalize_model_id(CONFIG.gatekeeper.model or "") - response = post_json( + response = await post_json( provider="gemini", url=_gemini_vertex_url(model), headers={**_vertex_auth_headers(), "Content-Type": "application/json"}, @@ -100,9 +100,9 @@ def _complete_gemini_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> ) -def _complete_openai_compatible_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: +async def _complete_openai_compatible_on_vertex(prompt: str, *, max_tokens: int, timeout: int) -> GatekeeperCompletion: base_url = _get_vertex_openapi_base_url() - response = post_json( + response = await post_json( provider="openai", url=f"{base_url}/chat/completions", headers={**_vertex_auth_headers(), "Content-Type": "application/json"}, @@ -117,12 +117,14 @@ def _complete_openai_compatible_on_vertex(prompt: str, *, max_tokens: int, timeo ) -def complete_vertex_ai(prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS) -> GatekeeperCompletion: +async def complete_vertex_ai( + prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS +) -> GatekeeperCompletion: model = CONFIG.gatekeeper.model or "" match _vertex_api_style(model): case "anthropic": - return _complete_anthropic_on_vertex(prompt, max_tokens=max_tokens, timeout=timeout) + return await _complete_anthropic_on_vertex(prompt, max_tokens=max_tokens, timeout=timeout) case "gemini": - return _complete_gemini_on_vertex(prompt, max_tokens=max_tokens, timeout=timeout) + return await _complete_gemini_on_vertex(prompt, max_tokens=max_tokens, timeout=timeout) case "openai_compatible": - return _complete_openai_compatible_on_vertex(prompt, max_tokens=max_tokens, timeout=timeout) + return await _complete_openai_compatible_on_vertex(prompt, max_tokens=max_tokens, timeout=timeout) diff --git a/tests/gatekeeper/test_anthropic_client.py b/tests/gatekeeper/test_anthropic_client.py index d142b0bb..082bb9b9 100644 --- a/tests/gatekeeper/test_anthropic_client.py +++ b/tests/gatekeeper/test_anthropic_client.py @@ -26,16 +26,17 @@ def gatekeeper_config(self, mocker): mocker.patch.object(CONFIG, "gatekeeper", config) return config - def test_complete_anthropic_direct(self, gatekeeper_config, mocker): + async def test_complete_anthropic_direct(self, gatekeeper_config, mocker): mock_post = mocker.patch( "linux_mcp_server.gatekeeper.anthropic_client.post_json", + new_callable=mocker.AsyncMock, return_value={ "content": [{"type": "text", "text": '{"status": "OK", "detail": ""}'}], "usage": {"input_tokens": 30, "output_tokens": 10}, }, ) - result = anthropic_client.complete_anthropic("prompt", max_tokens=8000) + result = await anthropic_client.complete_anthropic("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' assert result.prompt_tokens == 30 diff --git a/tests/gatekeeper/test_check_run_script.py b/tests/gatekeeper/test_check_run_script.py index 78be4462..4d5b148c 100644 --- a/tests/gatekeeper/test_check_run_script.py +++ b/tests/gatekeeper/test_check_run_script.py @@ -1,5 +1,5 @@ +import asyncio import importlib -import time import pytest @@ -79,13 +79,11 @@ def mock_llm(self, mocker): mocker.patch.object(CONFIG.gatekeeper, "model", "gpt-5.4") mocker.patch.object(CONFIG.gatekeeper, "provider", GatekeeperProvider.OPENAI) - def _completion(text: str) -> GatekeeperCompletion: - return GatekeeperCompletion(text=text) - return mocker.patch.object( check_run_script_module, "complete_gatekeeper", - side_effect=lambda prompt, **kwargs: _completion('{"status": "OK", "detail": ""}'), + new_callable=mocker.AsyncMock, + return_value=GatekeeperCompletion(text='{"status": "OK", "detail": ""}'), ) async def test_rejects_script_with_prompt_injection_attempts(self): @@ -109,6 +107,7 @@ async def test_missing_detail_defaults_to_empty(self, mocker): mocker.patch.object( check_run_script_module, "complete_gatekeeper", + new_callable=mocker.AsyncMock, return_value=GatekeeperCompletion(text='{"status": "OK"}'), ) @@ -121,6 +120,7 @@ async def test_parse_errors(self, mocker, response_text): mocker.patch.object( check_run_script_module, "complete_gatekeeper", + new_callable=mocker.AsyncMock, return_value=GatekeeperCompletion(text=response_text), ) @@ -128,8 +128,8 @@ async def test_parse_errors(self, mocker, response_text): await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) async def test_timeout(self, mocker): - def slow_complete(_prompt: str, **kwargs: object) -> GatekeeperCompletion: - time.sleep(10) + async def slow_complete(_prompt: str, **kwargs: object) -> GatekeeperCompletion: + await asyncio.sleep(10) return GatekeeperCompletion(text='{"status": "OK"}') mocker.patch.object( @@ -156,6 +156,7 @@ async def test_custom_cost(self, mocker): mocker.patch.object( check_run_script_module, "complete_gatekeeper", + new_callable=mocker.AsyncMock, return_value=GatekeeperCompletion( text='{"status": "OK", "detail": ""}', prompt_tokens=100, completion_tokens=50 ), @@ -172,6 +173,7 @@ async def test_openrouter_usage_cost(self, mocker): mocker.patch.object( check_run_script_module, "complete_gatekeeper", + new_callable=mocker.AsyncMock, return_value=GatekeeperCompletion( text='{"status": "OK", "detail": ""}', prompt_tokens=10, @@ -196,6 +198,7 @@ def mock_openai_post(self, mocker): mocker.patch.dict("os.environ", {"OPENAI_API_KEY": "test-key"}, clear=False) return mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", + new_callable=mocker.AsyncMock, return_value={"output_text": '{"status": "OK", "detail": ""}'}, ) @@ -228,6 +231,7 @@ async def test_vertex_ai_provider_uses_openapi_base_url(self, mocker): ) mock_post = mocker.patch( "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", + new_callable=mocker.AsyncMock, return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) mocker.patch.object( diff --git a/tests/gatekeeper/test_gemini_client.py b/tests/gatekeeper/test_gemini_client.py index 494ca8cb..19f7c63c 100644 --- a/tests/gatekeeper/test_gemini_client.py +++ b/tests/gatekeeper/test_gemini_client.py @@ -26,16 +26,17 @@ def gatekeeper_config(self, mocker): mocker.patch.object(CONFIG, "gatekeeper", config) return config - def test_complete_gemini_google_ai(self, gatekeeper_config, mocker): + async def test_complete_gemini_google_ai(self, gatekeeper_config, mocker): mock_post = mocker.patch( "linux_mcp_server.gatekeeper.gemini_client.post_json", + new_callable=mocker.AsyncMock, return_value={ "candidates": [{"content": {"parts": [{"text": '{"status": "OK"}'}]}}], "usageMetadata": {"promptTokenCount": 15, "candidatesTokenCount": 6}, }, ) - result = gemini_client.complete_gemini("prompt", max_tokens=8000) + result = await gemini_client.complete_gemini("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' assert result.prompt_tokens == 15 diff --git a/tests/gatekeeper/test_http_utils.py b/tests/gatekeeper/test_http_utils.py index 8cdc0df3..3cb9ab62 100644 --- a/tests/gatekeeper/test_http_utils.py +++ b/tests/gatekeeper/test_http_utils.py @@ -1,6 +1,8 @@ import pytest +from linux_mcp_server.gatekeeper.http_utils import GatekeeperHTTPError from linux_mcp_server.gatekeeper.http_utils import normalize_model_id +from linux_mcp_server.gatekeeper.http_utils import post_json @pytest.mark.parametrize( @@ -14,3 +16,40 @@ ) def test_normalize_model_id(model, expected): assert normalize_model_id(model) == expected + + +async def test_post_json_success(mocker): + response = mocker.MagicMock() + response.is_success = True + response.json.return_value = {"ok": True} + mock_client = mocker.AsyncMock() + mock_client.post.return_value = response + mocker.patch("linux_mcp_server.gatekeeper.http_utils.HTTP_CLIENT", mock_client) + + result = await post_json( + provider="openai", + url="https://example.com/v1/responses", + headers={"Authorization": "Bearer test"}, + body={"model": "gpt-5.4"}, + ) + + assert result == {"ok": True} + mock_client.post.assert_awaited_once() + + +async def test_post_json_error(mocker): + response = mocker.MagicMock() + response.is_success = False + response.status_code = 503 + response.text = "service unavailable" + mock_client = mocker.AsyncMock() + mock_client.post.return_value = response + mocker.patch("linux_mcp_server.gatekeeper.http_utils.HTTP_CLIENT", mock_client) + + with pytest.raises(GatekeeperHTTPError, match="openai API error \\(503\\)"): + await post_json( + provider="openai", + url="https://example.com/v1/responses", + headers={}, + body={}, + ) diff --git a/tests/gatekeeper/test_llm.py b/tests/gatekeeper/test_llm.py index d5b40ec4..154794e8 100644 --- a/tests/gatekeeper/test_llm.py +++ b/tests/gatekeeper/test_llm.py @@ -33,27 +33,34 @@ def test_infer_openrouter_from_model_prefix(self, mocker): class TestCompleteGatekeeper: - def test_routes_to_openai(self, mocker): + async def test_routes_to_openai(self, mocker): mocker.patch.object(llm_module, "resolve_provider", return_value=GatekeeperProvider.OPENAI) expected = GatekeeperCompletion(text='{"status": "OK"}') - mock_complete = mocker.patch.object(llm_module, "complete_openai", return_value=expected) - result = complete_gatekeeper("prompt", max_tokens=8000) + mock_complete = mocker.patch.object( + llm_module, "complete_openai", new_callable=mocker.AsyncMock, return_value=expected + ) + result = await complete_gatekeeper("prompt", max_tokens=8000) assert result == expected mock_complete.assert_called_once_with("prompt", max_tokens=8000) - def test_routes_to_openrouter(self, mocker): + async def test_routes_to_openrouter(self, mocker): mocker.patch.object(llm_module, "resolve_provider", return_value=GatekeeperProvider.OPENROUTER) expected = GatekeeperCompletion(text='{"status": "OK"}', prompt_tokens=1, completion_tokens=2, usage_cost=0.5) - mock_complete = mocker.patch.object(llm_module, "complete_openrouter", return_value=expected) - result = complete_gatekeeper("prompt", max_tokens=8000) + mock_complete = mocker.patch.object( + llm_module, "complete_openrouter", new_callable=mocker.AsyncMock, return_value=expected + ) + result = await complete_gatekeeper("prompt", max_tokens=8000) assert result == expected mock_complete.assert_called_once_with("prompt", max_tokens=8000) - def test_routes_to_vertex_ai(self, mocker): + async def test_routes_to_vertex_ai(self, mocker): mocker.patch.object(llm_module, "resolve_provider", return_value=GatekeeperProvider.VERTEX_AI) mock_complete = mocker.patch.object( - llm_module, "complete_vertex_ai", return_value=GatekeeperCompletion(text='{"status": "OK"}') + llm_module, + "complete_vertex_ai", + new_callable=mocker.AsyncMock, + return_value=GatekeeperCompletion(text='{"status": "OK"}'), ) - result = complete_gatekeeper("prompt", max_tokens=8000) + result = await complete_gatekeeper("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' mock_complete.assert_called_once_with("prompt", max_tokens=8000) diff --git a/tests/gatekeeper/test_openai_client.py b/tests/gatekeeper/test_openai_client.py index ee182eff..f4e9442f 100644 --- a/tests/gatekeeper/test_openai_client.py +++ b/tests/gatekeeper/test_openai_client.py @@ -50,16 +50,17 @@ def gatekeeper_config(self, mocker): mocker.patch.object(CONFIG, "gatekeeper", config) return config - def test_complete_openai_uses_responses_api(self, gatekeeper_config, mocker): + async def test_complete_openai_uses_responses_api(self, gatekeeper_config, mocker): mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", + new_callable=mocker.AsyncMock, return_value={ "output_text": '{"status": "OK", "detail": ""}', "usage": {"input_tokens": 11, "output_tokens": 4}, }, ) - result = openai_client.complete_openai("prompt", max_tokens=8000) + result = await openai_client.complete_openai("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' assert result.prompt_tokens == 11 @@ -70,22 +71,24 @@ def test_complete_openai_uses_responses_api(self, gatekeeper_config, mocker): assert body["reasoning"] == {"effort": "low"} assert body["text"]["format"]["type"] == "json_schema" - def test_complete_openai_uses_responses_api_for_ollama(self, gatekeeper_config, mocker): + async def test_complete_openai_uses_responses_api_for_ollama(self, gatekeeper_config, mocker): gatekeeper_config.openai = OpenAIGatekeeperConfig(base_url="http://localhost:11434/v1") mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", + new_callable=mocker.AsyncMock, return_value={"output_text": '{"status": "OK", "detail": ""}'}, ) - result = openai_client.complete_openai("prompt", max_tokens=8000) + result = await openai_client.complete_openai("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' assert mock_post.call_args.kwargs["url"] == "http://localhost:11434/v1/responses" - def test_complete_openai_falls_back_to_chat_completions_on_404(self, gatekeeper_config, mocker): + async def test_complete_openai_falls_back_to_chat_completions_on_404(self, gatekeeper_config, mocker): gatekeeper_config.openai = OpenAIGatekeeperConfig(base_url="https://models.example.com/v1") mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", + new_callable=mocker.AsyncMock, side_effect=[ GatekeeperHTTPError("openai", 404, "not found"), { @@ -95,7 +98,7 @@ def test_complete_openai_falls_back_to_chat_completions_on_404(self, gatekeeper_ ], ) - result = openai_client.complete_openai("prompt", max_tokens=8000) + result = await openai_client.complete_openai("prompt", max_tokens=8000) assert result.text == '{"status": "OK", "detail": ""}' assert result.prompt_tokens == 9 @@ -106,32 +109,34 @@ def test_complete_openai_falls_back_to_chat_completions_on_404(self, gatekeeper_ assert body["response_format"]["type"] == "json_schema" assert body["reasoning_effort"] == "low" - def test_structured_output_disabled(self, gatekeeper_config, mocker): + async def test_structured_output_disabled(self, gatekeeper_config, mocker): gatekeeper_config.structured_output = False mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", + new_callable=mocker.AsyncMock, return_value={"output_text": '{"status": "OK"}'}, ) - openai_client.complete_openai("prompt", max_tokens=8000) + await openai_client.complete_openai("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert "text" not in body - def test_template_kwargs_on_chat_completions(self, gatekeeper_config, mocker): + async def test_template_kwargs_on_chat_completions(self, gatekeeper_config, mocker): gatekeeper_config.openai = OpenAIGatekeeperConfig( base_url="https://models.example.com/v1", template_kwargs={"enable_thinking": False}, ) mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", + new_callable=mocker.AsyncMock, side_effect=[ GatekeeperHTTPError("openai", 404, "not found"), {"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ], ) - openai_client.complete_openai("prompt", max_tokens=8000) + await openai_client.complete_openai("prompt", max_tokens=8000) body = mock_post.call_args_list[1].kwargs["body"] assert body["chat_template_kwargs"] == {"enable_thinking": False} diff --git a/tests/gatekeeper/test_openrouter_client.py b/tests/gatekeeper/test_openrouter_client.py index 5cee0ae4..640ba502 100644 --- a/tests/gatekeeper/test_openrouter_client.py +++ b/tests/gatekeeper/test_openrouter_client.py @@ -52,16 +52,17 @@ def gatekeeper_config(self, mocker): mocker.patch.object(CONFIG, "gatekeeper", config) return config - def test_complete_openrouter_request_body(self, gatekeeper_config, mocker): + async def test_complete_openrouter_request_body(self, gatekeeper_config, mocker): mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openrouter_client.post_json", + new_callable=mocker.AsyncMock, return_value={ "choices": [{"message": {"content": '{"status": "OK", "detail": ""}'}}], "usage": {"prompt_tokens": 10, "completion_tokens": 5, "cost": 0.001}, }, ) - completion = openrouter_client.complete_openrouter("prompt", max_tokens=8000) + completion = await openrouter_client.complete_openrouter("prompt", max_tokens=8000) assert completion.text == '{"status": "OK", "detail": ""}' assert completion.prompt_tokens == 10 @@ -76,73 +77,79 @@ def test_complete_openrouter_request_body(self, gatekeeper_config, mocker): assert body["provider"] == {"require_parameters": True} assert body["response_format"]["type"] == "json_schema" - def test_complete_openrouter_quantization(self, gatekeeper_config, mocker): + async def test_complete_openrouter_quantization(self, gatekeeper_config, mocker): gatekeeper_config.openrouter = OpenRouterGatekeeperConfig(quantization="fp4") mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openrouter_client.post_json", + new_callable=mocker.AsyncMock, return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt", max_tokens=8000) + await openrouter_client.complete_openrouter("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert body["provider"] == {"require_parameters": True, "quantizations": ["fp4"]} - def test_complete_openrouter_reasoning_none(self, gatekeeper_config, mocker): + async def test_complete_openrouter_reasoning_none(self, gatekeeper_config, mocker): gatekeeper_config.reasoning_effort = ReasoningEffort.NONE mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openrouter_client.post_json", + new_callable=mocker.AsyncMock, return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt", max_tokens=8000) + await openrouter_client.complete_openrouter("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert body["reasoning"] == {"enabled": False} - def test_complete_openrouter_legacy_model_prefix(self, gatekeeper_config, mocker): + async def test_complete_openrouter_legacy_model_prefix(self, gatekeeper_config, mocker): gatekeeper_config.model = "openrouter/google/gemma-4-26b-a4b-it" mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openrouter_client.post_json", + new_callable=mocker.AsyncMock, return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt", max_tokens=8000) + await openrouter_client.complete_openrouter("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert body["model"] == "google/gemma-4-26b-a4b-it" - def test_complete_openrouter_custom_base_url(self, gatekeeper_config, mocker): + async def test_complete_openrouter_custom_base_url(self, gatekeeper_config, mocker): gatekeeper_config.openrouter = OpenRouterGatekeeperConfig(base_url="https://openrouter.example.com/api/v1") mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openrouter_client.post_json", + new_callable=mocker.AsyncMock, return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt", max_tokens=8000) + await openrouter_client.complete_openrouter("prompt", max_tokens=8000) assert mock_post.call_args.kwargs["url"] == "https://openrouter.example.com/api/v1/chat/completions" - def test_structured_output_disabled(self, gatekeeper_config, mocker): + async def test_structured_output_disabled(self, gatekeeper_config, mocker): gatekeeper_config.structured_output = False mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openrouter_client.post_json", + new_callable=mocker.AsyncMock, return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt", max_tokens=8000) + await openrouter_client.complete_openrouter("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert "response_format" not in body - def test_template_kwargs(self, gatekeeper_config, mocker): + async def test_template_kwargs(self, gatekeeper_config, mocker): gatekeeper_config.openrouter = OpenRouterGatekeeperConfig(template_kwargs={"enable_thinking": False}) mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openrouter_client.post_json", + new_callable=mocker.AsyncMock, return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - openrouter_client.complete_openrouter("prompt", max_tokens=8000) + await openrouter_client.complete_openrouter("prompt", max_tokens=8000) body = mock_post.call_args.kwargs["body"] assert body["chat_template_kwargs"] == {"enable_thinking": False} diff --git a/tests/gatekeeper/test_pricing.py b/tests/gatekeeper/test_pricing.py index 2fa1882d..30cab536 100644 --- a/tests/gatekeeper/test_pricing.py +++ b/tests/gatekeeper/test_pricing.py @@ -69,7 +69,7 @@ def test_cloud_unknown_uses_conservative_default(self, gatekeeper_config, mocker def test_fetch_failure_uses_vendored_fallback(self, gatekeeper_config, mocker): gatekeeper_config.cost = None - mocker.patch("linux_mcp_server.gatekeeper.pricing.requests.get", side_effect=OSError("offline")) + mocker.patch("linux_mcp_server.gatekeeper.pricing.httpx.Client", side_effect=OSError("offline")) cost, source = pricing.compute_cost(1_000_000, 0, usage_cost=None) assert cost == pytest.approx(3.0) assert source == "models_dev" diff --git a/tests/gatekeeper/test_vertex_ai_client.py b/tests/gatekeeper/test_vertex_ai_client.py index d0144522..ebc58baa 100644 --- a/tests/gatekeeper/test_vertex_ai_client.py +++ b/tests/gatekeeper/test_vertex_ai_client.py @@ -57,13 +57,14 @@ def gatekeeper_config(self, mocker): ) return config - def test_complete_anthropic_on_vertex(self, gatekeeper_config, mocker): + async def test_complete_anthropic_on_vertex(self, gatekeeper_config, mocker): mock_post = mocker.patch( "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", + new_callable=mocker.AsyncMock, return_value={"content": [{"type": "text", "text": '{"status": "OK"}'}]}, ) - result = vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) + result = await vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' body = mock_post.call_args.kwargs["body"] @@ -71,33 +72,35 @@ def test_complete_anthropic_on_vertex(self, gatekeeper_config, mocker): assert body["anthropic_version"] == "vertex-2023-10-16" assert ":rawPredict" in mock_post.call_args.kwargs["url"] - def test_complete_gemini_on_vertex(self, gatekeeper_config, mocker): + async def test_complete_gemini_on_vertex(self, gatekeeper_config, mocker): gatekeeper_config.model = "gemini-3.1-pro-preview" mock_post = mocker.patch( "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", + new_callable=mocker.AsyncMock, return_value={"candidates": [{"content": {"parts": [{"text": '{"status": "OK"}'}]}}]}, ) - result = vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) + result = await vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' assert ":generateContent" in mock_post.call_args.kwargs["url"] assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer gcp-token" - def test_complete_openai_compatible_on_vertex(self, gatekeeper_config, mocker): + async def test_complete_openai_compatible_on_vertex(self, gatekeeper_config, mocker): gatekeeper_config.model = "gpt-oss-120b-maas" mock_post = mocker.patch( "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", + new_callable=mocker.AsyncMock, return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - result = vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) + result = await vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' assert mock_post.call_args.kwargs["url"].endswith("/chat/completions") assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer gcp-token" - def test_custom_openapi_base_url(self, gatekeeper_config, mocker): + async def test_custom_openapi_base_url(self, gatekeeper_config, mocker): gatekeeper_config.model = "gpt-oss-120b-maas" gatekeeper_config.vertex_ai = VertexAIGatekeeperConfig( project="test-project", @@ -105,9 +108,10 @@ def test_custom_openapi_base_url(self, gatekeeper_config, mocker): ) mock_post = mocker.patch( "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", + new_callable=mocker.AsyncMock, return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, ) - vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) + await vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) assert mock_post.call_args.kwargs["url"].startswith("https://custom.example.com") diff --git a/uv.lock b/uv.lock index aa5f3d7a..5f025b2c 100644 --- a/uv.lock +++ b/uv.lock @@ -1157,9 +1157,9 @@ dependencies = [ { name = "asyncssh", extra = ["bcrypt"] }, { name = "fakeredis" }, { name = "fastmcp" }, + { name = "httpx" }, { name = "pydantic" }, { name = "pydantic-settings" }, - { name = "requests" }, ] [package.optional-dependencies] @@ -1209,9 +1209,9 @@ requires-dist = [ { name = "fastmcp", specifier = ">=3.2.4" }, { name = "google-auth", marker = "extra == 'gcp'", specifier = ">=2.40.0" }, { name = "gssapi", marker = "extra == 'gssapi'", specifier = ">=1.11.1" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic-settings", specifier = ">=2.12.0" }, - { name = "requests", specifier = ">=2.32.0" }, ] provides-extras = ["gcp", "gssapi"] From e85f661e5fa0f3f106bf13ac76023d52f175d061 Mon Sep 17 00:00:00 2001 From: Link Dupont Date: Thu, 18 Jun 2026 11:49:08 -0400 Subject: [PATCH 7/7] refactor(gatekeeper): Drop Chat Completions API from OpenAI client --- .../gatekeeper/openai_client.py | 79 ++----------------- .../gatekeeper/vertex_ai_client.py | 14 ++-- tests/gatekeeper/test_check_run_script.py | 4 +- tests/gatekeeper/test_openai_client.py | 63 ++------------- tests/gatekeeper/test_vertex_ai_client.py | 6 +- 5 files changed, 23 insertions(+), 143 deletions(-) diff --git a/src/linux_mcp_server/gatekeeper/openai_client.py b/src/linux_mcp_server/gatekeeper/openai_client.py index 6a803e5c..37248808 100644 --- a/src/linux_mcp_server/gatekeeper/openai_client.py +++ b/src/linux_mcp_server/gatekeeper/openai_client.py @@ -1,19 +1,15 @@ -"""OpenAI Responses and Chat Completions clients for the gatekeeper.""" +"""OpenAI Responses API client for the gatekeeper.""" import os from typing import Any -from urllib.parse import urlparse from linux_mcp_server.config import CONFIG from linux_mcp_server.config import ReasoningEffort from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS -from linux_mcp_server.gatekeeper.http_utils import GatekeeperHTTPError from linux_mcp_server.gatekeeper.http_utils import normalize_model_id from linux_mcp_server.gatekeeper.http_utils import post_json -from linux_mcp_server.gatekeeper.schema import openai_response_format from linux_mcp_server.gatekeeper.schema import openai_text_format -from linux_mcp_server.gatekeeper.usage import extract_openai_chat_completions_usage from linux_mcp_server.gatekeeper.usage import extract_openai_responses_usage from linux_mcp_server.models import GatekeeperCompletion @@ -43,26 +39,6 @@ def _get_openai_base_url() -> str: return (configured or os.environ.get("OPENAI_API_BASE") or OPENAI_DEFAULT_BASE_URL).rstrip("/") -def _prefers_openai_chat_completions(base_url: str) -> bool: - """Hosts known to expose only Chat Completions, not the Responses API.""" - path = urlparse(base_url).path or "" - return "/endpoints/openapi" in path - - -def _openai_template_kwargs() -> dict[str, Any]: - if CONFIG.gatekeeper.openai is None: - return {} - return CONFIG.gatekeeper.openai.template_kwargs - - -def _apply_chat_completions_extras(body: dict[str, Any]) -> dict[str, Any]: - """Merge template_kwargs into Chat Completions bodies (llama.cpp, etc.).""" - template_kwargs = _openai_template_kwargs() - if template_kwargs: - body["chat_template_kwargs"] = template_kwargs - return body - - def _build_responses_body(prompt: str, *, max_tokens: int) -> dict[str, Any]: body: dict[str, Any] = { "model": normalize_model_id(CONFIG.gatekeeper.model or ""), @@ -79,21 +55,6 @@ def _build_responses_body(prompt: str, *, max_tokens: int) -> dict[str, Any]: return body -def build_chat_completions_body(prompt: str, *, max_tokens: int) -> dict[str, Any]: - body: dict[str, Any] = { - "model": normalize_model_id(CONFIG.gatekeeper.model or ""), - "messages": [{"role": "user", "content": prompt}], - "max_completion_tokens": max_tokens, - "temperature": CONFIG.gatekeeper.temperature, - } - if CONFIG.gatekeeper.structured_output: - body["response_format"] = openai_response_format() - reasoning_effort = CONFIG.gatekeeper.reasoning_effort - if reasoning_effort is not None: - body["reasoning_effort"] = reasoning_effort.value - return _apply_chat_completions_extras(body) - - def _extract_responses_text(response: dict[str, Any]) -> str: output_text = response.get("output_text") if isinstance(output_text, str) and output_text.strip(): @@ -114,15 +75,6 @@ def _extract_responses_text(response: dict[str, Any]) -> str: return "".join(chunks).strip() -def extract_chat_completions_text(response: dict[str, Any]) -> str: - choices = response.get("choices", []) - if not choices: - return "" - message = choices[0].get("message", {}) - content = message.get("content") - return (content or "").strip() if isinstance(content, str) else "" - - async def complete_openai( prompt: str, *, max_tokens: int, timeout: int = DEFAULT_TIMEOUT_SECONDS ) -> GatekeeperCompletion: @@ -131,37 +83,16 @@ async def complete_openai( **_openai_auth_headers(), "Content-Type": "application/json", } - - # Try the Responses API first, falling back to Chat Completions if it's not available. - if not _prefers_openai_chat_completions(base_url): - try: - response = await post_json( - provider="openai", - url=f"{base_url}/responses", - headers=headers, - body=_build_responses_body(prompt, max_tokens=max_tokens), - timeout=timeout, - ) - usage = extract_openai_responses_usage(response) - return GatekeeperCompletion( - text=_extract_responses_text(response), - prompt_tokens=usage.input_tokens, - completion_tokens=usage.output_tokens, - ) - except GatekeeperHTTPError as exc: - if exc.status_code not in {404, 405}: - raise - response = await post_json( provider="openai", - url=f"{base_url}/chat/completions", + url=f"{base_url}/responses", headers=headers, - body=build_chat_completions_body(prompt, max_tokens=max_tokens), + body=_build_responses_body(prompt, max_tokens=max_tokens), timeout=timeout, ) - usage = extract_openai_chat_completions_usage(response) + usage = extract_openai_responses_usage(response) return GatekeeperCompletion( - text=extract_chat_completions_text(response), + text=_extract_responses_text(response), prompt_tokens=usage.input_tokens, completion_tokens=usage.output_tokens, ) diff --git a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py index 32bdb73c..29810885 100644 --- a/src/linux_mcp_server/gatekeeper/vertex_ai_client.py +++ b/src/linux_mcp_server/gatekeeper/vertex_ai_client.py @@ -12,11 +12,11 @@ from linux_mcp_server.gatekeeper.http_utils import DEFAULT_TIMEOUT_SECONDS from linux_mcp_server.gatekeeper.http_utils import normalize_model_id from linux_mcp_server.gatekeeper.http_utils import post_json -from linux_mcp_server.gatekeeper.openai_client import build_chat_completions_body -from linux_mcp_server.gatekeeper.openai_client import extract_chat_completions_text +from linux_mcp_server.gatekeeper.openai_client import _build_responses_body +from linux_mcp_server.gatekeeper.openai_client import _extract_responses_text from linux_mcp_server.gatekeeper.usage import extract_anthropic_usage from linux_mcp_server.gatekeeper.usage import extract_gemini_usage -from linux_mcp_server.gatekeeper.usage import extract_openai_chat_completions_usage +from linux_mcp_server.gatekeeper.usage import extract_openai_responses_usage from linux_mcp_server.models import GatekeeperCompletion @@ -104,14 +104,14 @@ async def _complete_openai_compatible_on_vertex(prompt: str, *, max_tokens: int, base_url = _get_vertex_openapi_base_url() response = await post_json( provider="openai", - url=f"{base_url}/chat/completions", + url=f"{base_url}/responses", headers={**_vertex_auth_headers(), "Content-Type": "application/json"}, - body=build_chat_completions_body(prompt, max_tokens=max_tokens), + body=_build_responses_body(prompt, max_tokens=max_tokens), timeout=timeout, ) - usage = extract_openai_chat_completions_usage(response) + usage = extract_openai_responses_usage(response) return GatekeeperCompletion( - text=extract_chat_completions_text(response), + text=_extract_responses_text(response), prompt_tokens=usage.input_tokens, completion_tokens=usage.output_tokens, ) diff --git a/tests/gatekeeper/test_check_run_script.py b/tests/gatekeeper/test_check_run_script.py index 4d5b148c..8da8a62a 100644 --- a/tests/gatekeeper/test_check_run_script.py +++ b/tests/gatekeeper/test_check_run_script.py @@ -232,7 +232,7 @@ async def test_vertex_ai_provider_uses_openapi_base_url(self, mocker): mock_post = mocker.patch( "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", new_callable=mocker.AsyncMock, - return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + return_value={"output_text": '{"status": "OK", "detail": ""}'}, ) mocker.patch.object( CONFIG, @@ -251,5 +251,5 @@ async def test_vertex_ai_provider_uses_openapi_base_url(self, mocker): await check_run_script(description="test", script_type="bash", script="echo hi", readonly=True) - assert mock_post.call_args.kwargs["url"].endswith("/chat/completions") + assert mock_post.call_args.kwargs["url"].endswith("/responses") assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer gcp-token" diff --git a/tests/gatekeeper/test_openai_client.py b/tests/gatekeeper/test_openai_client.py index f4e9442f..4d5b048d 100644 --- a/tests/gatekeeper/test_openai_client.py +++ b/tests/gatekeeper/test_openai_client.py @@ -8,23 +8,6 @@ from linux_mcp_server.gatekeeper import openai_client from linux_mcp_server.gatekeeper.http_utils import GatekeeperHTTPError from linux_mcp_server.gatekeeper.openai_client import _openai_reasoning_block -from linux_mcp_server.gatekeeper.openai_client import _prefers_openai_chat_completions - - -@pytest.mark.parametrize( - "base_url,expected", - [ - ("https://api.openai.com/v1", False), - ("http://localhost:11434/v1", False), - ("https://example.com/v1", False), - ( - "https://aiplatform.googleapis.com/v1/projects/p/locations/global/endpoints/openapi", - True, - ), - ], -) -def test_prefers_openai_chat_completions(base_url, expected): - assert _prefers_openai_chat_completions(base_url) is expected def test_openai_reasoning_block_none(): @@ -71,7 +54,7 @@ async def test_complete_openai_uses_responses_api(self, gatekeeper_config, mocke assert body["reasoning"] == {"effort": "low"} assert body["text"]["format"]["type"] == "json_schema" - async def test_complete_openai_uses_responses_api_for_ollama(self, gatekeeper_config, mocker): + async def test_complete_openai_uses_responses_api_for_custom_base_url(self, gatekeeper_config, mocker): gatekeeper_config.openai = OpenAIGatekeeperConfig(base_url="http://localhost:11434/v1") mock_post = mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", @@ -84,30 +67,15 @@ async def test_complete_openai_uses_responses_api_for_ollama(self, gatekeeper_co assert result.text == '{"status": "OK", "detail": ""}' assert mock_post.call_args.kwargs["url"] == "http://localhost:11434/v1/responses" - async def test_complete_openai_falls_back_to_chat_completions_on_404(self, gatekeeper_config, mocker): - gatekeeper_config.openai = OpenAIGatekeeperConfig(base_url="https://models.example.com/v1") - mock_post = mocker.patch( + async def test_complete_openai_propagates_responses_api_errors(self, gatekeeper_config, mocker): + mocker.patch( "linux_mcp_server.gatekeeper.openai_client.post_json", new_callable=mocker.AsyncMock, - side_effect=[ - GatekeeperHTTPError("openai", 404, "not found"), - { - "choices": [{"message": {"content": '{"status": "OK", "detail": ""}'}}], - "usage": {"prompt_tokens": 9, "completion_tokens": 2}, - }, - ], + side_effect=GatekeeperHTTPError("openai", 404, "not found"), ) - result = await openai_client.complete_openai("prompt", max_tokens=8000) - - assert result.text == '{"status": "OK", "detail": ""}' - assert result.prompt_tokens == 9 - assert result.completion_tokens == 2 - assert mock_post.call_args_list[0].kwargs["url"] == "https://models.example.com/v1/responses" - assert mock_post.call_args_list[1].kwargs["url"] == "https://models.example.com/v1/chat/completions" - body = mock_post.call_args_list[1].kwargs["body"] - assert body["response_format"]["type"] == "json_schema" - assert body["reasoning_effort"] == "low" + with pytest.raises(GatekeeperHTTPError, match="not found"): + await openai_client.complete_openai("prompt", max_tokens=8000) async def test_structured_output_disabled(self, gatekeeper_config, mocker): gatekeeper_config.structured_output = False @@ -121,22 +89,3 @@ async def test_structured_output_disabled(self, gatekeeper_config, mocker): body = mock_post.call_args.kwargs["body"] assert "text" not in body - - async def test_template_kwargs_on_chat_completions(self, gatekeeper_config, mocker): - gatekeeper_config.openai = OpenAIGatekeeperConfig( - base_url="https://models.example.com/v1", - template_kwargs={"enable_thinking": False}, - ) - mock_post = mocker.patch( - "linux_mcp_server.gatekeeper.openai_client.post_json", - new_callable=mocker.AsyncMock, - side_effect=[ - GatekeeperHTTPError("openai", 404, "not found"), - {"choices": [{"message": {"content": '{"status": "OK"}'}}]}, - ], - ) - - await openai_client.complete_openai("prompt", max_tokens=8000) - - body = mock_post.call_args_list[1].kwargs["body"] - assert body["chat_template_kwargs"] == {"enable_thinking": False} diff --git a/tests/gatekeeper/test_vertex_ai_client.py b/tests/gatekeeper/test_vertex_ai_client.py index ebc58baa..09d9acba 100644 --- a/tests/gatekeeper/test_vertex_ai_client.py +++ b/tests/gatekeeper/test_vertex_ai_client.py @@ -91,13 +91,13 @@ async def test_complete_openai_compatible_on_vertex(self, gatekeeper_config, moc mock_post = mocker.patch( "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", new_callable=mocker.AsyncMock, - return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + return_value={"output_text": '{"status": "OK"}'}, ) result = await vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000) assert result.text == '{"status": "OK"}' - assert mock_post.call_args.kwargs["url"].endswith("/chat/completions") + assert mock_post.call_args.kwargs["url"].endswith("/responses") assert mock_post.call_args.kwargs["headers"]["Authorization"] == "Bearer gcp-token" async def test_custom_openapi_base_url(self, gatekeeper_config, mocker): @@ -109,7 +109,7 @@ async def test_custom_openapi_base_url(self, gatekeeper_config, mocker): mock_post = mocker.patch( "linux_mcp_server.gatekeeper.vertex_ai_client.post_json", new_callable=mocker.AsyncMock, - return_value={"choices": [{"message": {"content": '{"status": "OK"}'}}]}, + return_value={"output_text": '{"status": "OK"}'}, ) await vertex_ai_client.complete_vertex_ai("prompt", max_tokens=8000)