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=