Spaces:
Running
Running
Upload 5 files
Browse files- README.md +44 -10
- _shared_logic.py +472 -15
- app.py +65 -6
README.md
CHANGED
|
@@ -123,19 +123,23 @@ All variables are set in **Space β Settings β Repository secrets**.
|
|
| 123 |
| Variable | Required? | Scope | Description |
|
| 124 |
|---|---|---|---|
|
| 125 |
| `HF_TOKEN` | Yes (for Path 3) | Read / inference-api | Injected as `Bearer` into Path 1 and Path 3 upstream requests. Use a **read-only** fine-grained token β it never needs write permission. |
|
|
|
|
| 126 |
| `HF_WRITE_TOKEN` | No | Write (dataset repo only) | Dedicated write token used **only** by `POST /v1/contribute` to push JSONL files to `TRAINING_DATASET_REPO`. When set, `HF_TOKEN` remains strictly read-only. When absent, the proxy falls back to `HF_TOKEN` for writes (single-token mode, backward compatible but less secure). |
|
|
|
|
| 127 |
|
| 128 |
> **Why two tokens?** Principle of least privilege. `HF_TOKEN` is forwarded
|
| 129 |
> to external model backends on every inference request β if it carried write
|
| 130 |
> permission a compromised upstream could modify your repos. `HF_WRITE_TOKEN`
|
| 131 |
> never leaves the proxy and is scoped to one dataset repo.
|
| 132 |
|
| 133 |
-
The `/` status page shows
|
| 134 |
|
| 135 |
```json
|
| 136 |
"tokens": {
|
| 137 |
-
"hf_token_set":
|
| 138 |
-
"
|
|
|
|
|
|
|
| 139 |
"least_privilege_mode": true
|
| 140 |
},
|
| 141 |
"training": {
|
|
@@ -144,6 +148,10 @@ The `/` status page shows both token states without exposing values:
|
|
| 144 |
}
|
| 145 |
```
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
`least_privilege_mode: false` means `HF_WRITE_TOKEN` is absent and writes fall
|
| 148 |
back to `HF_TOKEN` β functional but not recommended for production.
|
| 149 |
|
|
@@ -155,7 +163,7 @@ back to `HF_TOKEN` β functional but not recommended for production.
|
|
| 155 |
| `HF_SPACES_MODEL_URL` | No | β | **Path 2** β Custom ZeroGPU Space URL (e.g. `https://scikit-plots-ai-model.hf.space/v1/chat/completions`). Receives requests whose `model` matches `HF_SPACES_MODEL_NAMESPACES`. |
|
| 156 |
| `HF_SPACES_MODEL_NAMESPACES` | No | `scikit-plots` | Comma-separated model owner prefixes routed to Path 2 (e.g. `scikit-plots,my-org`). |
|
| 157 |
| `HF_BASE` | No | `https://router.huggingface.co` | **Path 3** β HF Serverless API base URL. Only used when Path 1 and Path 2 do not match. |
|
| 158 |
-
| `DEFAULT_MODEL` | No | `scikit-plots/Qwen2.5-Coder-
|
| 159 |
|
| 160 |
### Training data collection
|
| 161 |
|
|
@@ -224,6 +232,26 @@ read role β HF_TOKEN (inference only; set in Space secrets)
|
|
| 224 |
write role β HF_WRITE_TOKEN (dataset push; set in Space secrets)
|
| 225 |
```
|
| 226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
---
|
| 228 |
|
| 229 |
## Training data setup
|
|
@@ -314,7 +342,7 @@ below.
|
|
| 314 |
HF_SPACES_MODEL_URL = https://scikit-plots-ai-model.hf.space/v1/chat/completions
|
| 315 |
HF_SPACES_MODEL_NAMESPACES = scikit-plots
|
| 316 |
PATH2_TIMEOUT = 600 β CPU cold start takes 4β5 minutes
|
| 317 |
-
DEFAULT_MODEL = scikit-plots/Qwen2.5-Coder-
|
| 318 |
```
|
| 319 |
|
| 320 |
### Path 1 β Local / custom backend (Docker Model Runner, Ollama)
|
|
@@ -332,14 +360,16 @@ export HF_TOKEN=hf_<optional-for-path-1>
|
|
| 332 |
```
|
| 333 |
# Inference
|
| 334 |
HF_TOKEN = hf_<read-token>
|
|
|
|
| 335 |
HF_SPACES_MODEL_URL = https://scikit-plots-ai-model.hf.space/v1/chat/completions
|
| 336 |
HF_SPACES_MODEL_NAMESPACES = scikit-plots
|
| 337 |
-
DEFAULT_MODEL = scikit-plots/Qwen2.5-Coder-
|
| 338 |
PATH2_TIMEOUT = 600
|
| 339 |
PATH3_TIMEOUT = 120
|
| 340 |
|
| 341 |
# Training data collection
|
| 342 |
HF_WRITE_TOKEN = hf_<write-token>
|
|
|
|
| 343 |
TRAINING_DATASET_REPO = scikit-plots/ai-assistant-contributions
|
| 344 |
|
| 345 |
# Security
|
|
@@ -355,7 +385,7 @@ BASE=https://scikit-plots-ai.hf.space
|
|
| 355 |
|
| 356 |
# 1. Liveness probe
|
| 357 |
curl $BASE/health
|
| 358 |
-
# {"status":"ok","version":"6.
|
| 359 |
|
| 360 |
# 2. Full status β check routing, token slots, and training readiness
|
| 361 |
curl $BASE/ | python3 -m json.tool
|
|
@@ -371,7 +401,7 @@ curl $BASE/v1/chat/completions \
|
|
| 371 |
# 4. Test a chat completion (Path 2 β custom Space)
|
| 372 |
curl $BASE/v1/chat/completions \
|
| 373 |
-H "Content-Type: application/json" \
|
| 374 |
-
-d '{"model":"scikit-plots/Qwen2.5-Coder-
|
| 375 |
|
| 376 |
# 5. Test training contribution (requires TRAINING_DATASET_REPO + write token)
|
| 377 |
curl $BASE/v1/contribute \
|
|
@@ -400,7 +430,7 @@ curl $BASE/v1/contribute \
|
|
| 400 |
|
| 401 |
## Why mirror repos fail β and how Path 2 solves it
|
| 402 |
|
| 403 |
-
`scikit-plots/gpt-oss-20b` and `scikit-plots/Qwen2.5-Coder-
|
| 404 |
**mirror repositories** β weights copied from the originals but **not
|
| 405 |
registered with any HF Inference Provider**.
|
| 406 |
|
|
@@ -413,7 +443,7 @@ registered with any HF Inference Provider**.
|
|
| 413 |
Path 2 intercepts requests whose `model` field starts with a configured
|
| 414 |
namespace **before** they reach the HF Serverless router, and forwards them to
|
| 415 |
the custom ZeroGPU Space that actually has the weights loaded. This is why
|
| 416 |
-
`DEFAULT_MODEL = scikit-plots/Qwen2.5-Coder-
|
| 417 |
`HF_SPACES_MODEL_NAMESPACES = scikit-plots` is set.
|
| 418 |
|
| 419 |
---
|
|
@@ -433,6 +463,10 @@ the custom ZeroGPU Space that actually has the weights loaded. This is why
|
|
| 433 |
| `least_privilege_mode: false` on status page | `HF_WRITE_TOKEN` not set | Set `HF_WRITE_TOKEN`; proxy works but uses `HF_TOKEN` for writes (less secure) |
|
| 434 |
| Container crashes: `ModuleNotFoundError: No module named '_shared_logic'` | `_shared_logic.py` missing from the Space repo | Commit `_shared_logic.py` alongside `app.py` |
|
| 435 |
| 413 on large conversation export | Body exceeds `MAX_BODY_BYTES` (default 10 MB) | Increase `MAX_BODY_BYTES` in Space secrets |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
---
|
| 438 |
|
|
|
|
| 123 |
| Variable | Required? | Scope | Description |
|
| 124 |
|---|---|---|---|
|
| 125 |
| `HF_TOKEN` | Yes (for Path 3) | Read / inference-api | Injected as `Bearer` into Path 1 and Path 3 upstream requests. Use a **read-only** fine-grained token β it never needs write permission. |
|
| 126 |
+
| `HF_TOKEN_TYPE` | No | β | Declares the type of `HF_TOKEN`. Accepted: `fine-grained` \| `read` \| `write`. When set, startup validation enforces least-privilege without network calls. When absent, a length-based heuristic is used and type shows as `"unknown"` on the status page. Setting `"write"` triggers a startup `WARNING`. |
|
| 127 |
| `HF_WRITE_TOKEN` | No | Write (dataset repo only) | Dedicated write token used **only** by `POST /v1/contribute` to push JSONL files to `TRAINING_DATASET_REPO`. When set, `HF_TOKEN` remains strictly read-only. When absent, the proxy falls back to `HF_TOKEN` for writes (single-token mode, backward compatible but less secure). |
|
| 128 |
+
| `HF_WRITE_TOKEN_TYPE` | No | β | Declares the type of `HF_WRITE_TOKEN`. Accepted: `fine-grained` \| `write`. Enables startup validation to detect a read token used for writes (which would cause every `/v1/contribute` call to fail with 403). |
|
| 129 |
|
| 130 |
> **Why two tokens?** Principle of least privilege. `HF_TOKEN` is forwarded
|
| 131 |
> to external model backends on every inference request β if it carried write
|
| 132 |
> permission a compromised upstream could modify your repos. `HF_WRITE_TOKEN`
|
| 133 |
> never leaves the proxy and is scoped to one dataset repo.
|
| 134 |
|
| 135 |
+
The `/` status page shows all token states without exposing values:
|
| 136 |
|
| 137 |
```json
|
| 138 |
"tokens": {
|
| 139 |
+
"hf_token_set": true,
|
| 140 |
+
"hf_token_type": "fine-grained",
|
| 141 |
+
"hf_write_token_set": true,
|
| 142 |
+
"hf_write_token_type": "fine-grained",
|
| 143 |
"least_privilege_mode": true
|
| 144 |
},
|
| 145 |
"training": {
|
|
|
|
| 148 |
}
|
| 149 |
```
|
| 150 |
|
| 151 |
+
`hf_token_type: "unknown"` means `HF_TOKEN_TYPE` is not set and the length
|
| 152 |
+
heuristic could not determine the type β set `HF_TOKEN_TYPE=fine-grained` or
|
| 153 |
+
`HF_TOKEN_TYPE=read` in Space secrets to resolve.
|
| 154 |
+
|
| 155 |
`least_privilege_mode: false` means `HF_WRITE_TOKEN` is absent and writes fall
|
| 156 |
back to `HF_TOKEN` β functional but not recommended for production.
|
| 157 |
|
|
|
|
| 163 |
| `HF_SPACES_MODEL_URL` | No | β | **Path 2** β Custom ZeroGPU Space URL (e.g. `https://scikit-plots-ai-model.hf.space/v1/chat/completions`). Receives requests whose `model` matches `HF_SPACES_MODEL_NAMESPACES`. |
|
| 164 |
| `HF_SPACES_MODEL_NAMESPACES` | No | `scikit-plots` | Comma-separated model owner prefixes routed to Path 2 (e.g. `scikit-plots,my-org`). |
|
| 165 |
| `HF_BASE` | No | `https://router.huggingface.co` | **Path 3** β HF Serverless API base URL. Only used when Path 1 and Path 2 do not match. |
|
| 166 |
+
| `DEFAULT_MODEL` | No | `scikit-plots/Qwen2.5-Coder-7B-Instruct` | Fallback model when the request body omits `"model"`. |
|
| 167 |
|
| 168 |
### Training data collection
|
| 169 |
|
|
|
|
| 232 |
write role β HF_WRITE_TOKEN (dataset push; set in Space secrets)
|
| 233 |
```
|
| 234 |
|
| 235 |
+
### Declare token types in Space secrets
|
| 236 |
+
|
| 237 |
+
After creating the tokens, set the corresponding type variables so the proxy
|
| 238 |
+
can validate least-privilege at startup without network calls:
|
| 239 |
+
|
| 240 |
+
```
|
| 241 |
+
# Fine-grained tokens (recommended):
|
| 242 |
+
HF_TOKEN_TYPE = fine-grained
|
| 243 |
+
HF_WRITE_TOKEN_TYPE = fine-grained
|
| 244 |
+
|
| 245 |
+
# Classic read/write tokens (legacy):
|
| 246 |
+
HF_TOKEN_TYPE = read
|
| 247 |
+
HF_WRITE_TOKEN_TYPE = write
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
If you omit these variables the proxy applies a length-based heuristic
|
| 251 |
+
(tokens β₯ 52 chars β `fine-grained`; shorter β `unknown`). The startup log
|
| 252 |
+
and status page will show `"unknown"` for the type, which suppresses
|
| 253 |
+
least-privilege warnings β always set explicit types in production.
|
| 254 |
+
|
| 255 |
---
|
| 256 |
|
| 257 |
## Training data setup
|
|
|
|
| 342 |
HF_SPACES_MODEL_URL = https://scikit-plots-ai-model.hf.space/v1/chat/completions
|
| 343 |
HF_SPACES_MODEL_NAMESPACES = scikit-plots
|
| 344 |
PATH2_TIMEOUT = 600 β CPU cold start takes 4β5 minutes
|
| 345 |
+
DEFAULT_MODEL = scikit-plots/Qwen2.5-Coder-7B-Instruct
|
| 346 |
```
|
| 347 |
|
| 348 |
### Path 1 β Local / custom backend (Docker Model Runner, Ollama)
|
|
|
|
| 360 |
```
|
| 361 |
# Inference
|
| 362 |
HF_TOKEN = hf_<read-token>
|
| 363 |
+
HF_TOKEN_TYPE = fine-grained
|
| 364 |
HF_SPACES_MODEL_URL = https://scikit-plots-ai-model.hf.space/v1/chat/completions
|
| 365 |
HF_SPACES_MODEL_NAMESPACES = scikit-plots
|
| 366 |
+
DEFAULT_MODEL = scikit-plots/Qwen2.5-Coder-7B-Instruct
|
| 367 |
PATH2_TIMEOUT = 600
|
| 368 |
PATH3_TIMEOUT = 120
|
| 369 |
|
| 370 |
# Training data collection
|
| 371 |
HF_WRITE_TOKEN = hf_<write-token>
|
| 372 |
+
HF_WRITE_TOKEN_TYPE = fine-grained
|
| 373 |
TRAINING_DATASET_REPO = scikit-plots/ai-assistant-contributions
|
| 374 |
|
| 375 |
# Security
|
|
|
|
| 385 |
|
| 386 |
# 1. Liveness probe
|
| 387 |
curl $BASE/health
|
| 388 |
+
# {"status":"ok","version":"6.1.0"}
|
| 389 |
|
| 390 |
# 2. Full status β check routing, token slots, and training readiness
|
| 391 |
curl $BASE/ | python3 -m json.tool
|
|
|
|
| 401 |
# 4. Test a chat completion (Path 2 β custom Space)
|
| 402 |
curl $BASE/v1/chat/completions \
|
| 403 |
-H "Content-Type: application/json" \
|
| 404 |
+
-d '{"model":"scikit-plots/Qwen2.5-Coder-7B-Instruct","messages":[{"role":"user","content":"hi"}]}'
|
| 405 |
|
| 406 |
# 5. Test training contribution (requires TRAINING_DATASET_REPO + write token)
|
| 407 |
curl $BASE/v1/contribute \
|
|
|
|
| 430 |
|
| 431 |
## Why mirror repos fail β and how Path 2 solves it
|
| 432 |
|
| 433 |
+
`scikit-plots/gpt-oss-20b` and `scikit-plots/Qwen2.5-Coder-7B-Instruct` are
|
| 434 |
**mirror repositories** β weights copied from the originals but **not
|
| 435 |
registered with any HF Inference Provider**.
|
| 436 |
|
|
|
|
| 443 |
Path 2 intercepts requests whose `model` field starts with a configured
|
| 444 |
namespace **before** they reach the HF Serverless router, and forwards them to
|
| 445 |
the custom ZeroGPU Space that actually has the weights loaded. This is why
|
| 446 |
+
`DEFAULT_MODEL = scikit-plots/Qwen2.5-Coder-7B-Instruct` works when
|
| 447 |
`HF_SPACES_MODEL_NAMESPACES = scikit-plots` is set.
|
| 448 |
|
| 449 |
---
|
|
|
|
| 463 |
| `least_privilege_mode: false` on status page | `HF_WRITE_TOKEN` not set | Set `HF_WRITE_TOKEN`; proxy works but uses `HF_TOKEN` for writes (less secure) |
|
| 464 |
| Container crashes: `ModuleNotFoundError: No module named '_shared_logic'` | `_shared_logic.py` missing from the Space repo | Commit `_shared_logic.py` alongside `app.py` |
|
| 465 |
| 413 on large conversation export | Body exceeds `MAX_BODY_BYTES` (default 10 MB) | Increase `MAX_BODY_BYTES` in Space secrets |
|
| 466 |
+
| Startup log: `WARNING: Startup token-config check: HF_TOKEN has type 'write' β¦` | A write token is being used for inference β violates least-privilege | Replace `HF_TOKEN` with a read or fine-grained token scoped to Inference only; set `HF_TOKEN_TYPE=read` or `HF_TOKEN_TYPE=fine-grained` in Space secrets |
|
| 467 |
+
| Startup log: `WARNING: Startup token-config check: HF_WRITE_TOKEN has type 'read' β¦` | A read-only token is configured as the write token | Replace `HF_WRITE_TOKEN` with a write or fine-grained token that has dataset write scope; set `HF_WRITE_TOKEN_TYPE=write` or `HF_WRITE_TOKEN_TYPE=fine-grained` |
|
| 468 |
+
| Status page shows `hf_token_type: "unknown"` | `HF_TOKEN_TYPE` not set; length-based heuristic could not determine token type | Set `HF_TOKEN_TYPE=fine-grained` or `HF_TOKEN_TYPE=read` in Space secrets to enable least-privilege startup checks |
|
| 469 |
+
| Status page shows `hf_token_type: "write"` with a startup WARNING | `HF_TOKEN` is a write token β too many permissions for inference | Create a separate read or fine-grained token for `HF_TOKEN` and set `HF_TOKEN_TYPE=read`; keep the write token only in `HF_WRITE_TOKEN` |
|
| 470 |
|
| 471 |
---
|
| 472 |
|
_shared_logic.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# _shared_logic.py v6.
|
| 2 |
#
|
| 3 |
# Single source of truth for shared constants, pure helper functions, and
|
| 4 |
# type aliases used by the deployed proxy (_hf_spaces_proxy/app.py) and the
|
|
@@ -56,6 +56,26 @@
|
|
| 56 |
# Callers who hard-code ``HF_BASE`` to the old hostname must migrate to
|
| 57 |
# the new router URL.
|
| 58 |
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
# SPDX-License-Identifier: BSD-3-Clause
|
| 60 |
# Authors: The scikit-plots developers
|
| 61 |
|
|
@@ -134,10 +154,12 @@ from __future__ import annotations
|
|
| 134 |
|
| 135 |
import json
|
| 136 |
import os
|
| 137 |
-
from typing import Any
|
| 138 |
|
| 139 |
__all__ = [
|
| 140 |
-
#
|
|
|
|
|
|
|
| 141 |
"DEFAULT_HF_BASE",
|
| 142 |
"DEFAULT_HF_SPACES_MODEL_NAMESPACES",
|
| 143 |
"DEFAULT_HF_SPACES_MODEL_URL",
|
|
@@ -146,14 +168,28 @@ __all__ = [
|
|
| 146 |
"DEFAULT_PATH2_READ_TIMEOUT",
|
| 147 |
"DEFAULT_PATH3_READ_TIMEOUT",
|
| 148 |
"DEFAULT_PROXY_TIMEOUT",
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
"_build_cors_headers",
|
| 152 |
"_is_custom_model_namespace",
|
| 153 |
"_parse_model",
|
| 154 |
-
"
|
| 155 |
"_safe_int",
|
| 156 |
"_token_log_fragment",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
"_validate_env",
|
| 158 |
"load_proxy_env",
|
| 159 |
]
|
|
@@ -164,7 +200,7 @@ __all__ = [
|
|
| 164 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 165 |
|
| 166 |
#: Proxy release version β bump on every breaking change.
|
| 167 |
-
PROXY_VERSION: str = "6.
|
| 168 |
|
| 169 |
#: HuggingFace Inference Providers router base URL (no trailing slash).
|
| 170 |
#: Only used for Path 3 (standard provider models) when ``BACKEND_URL`` is
|
|
@@ -179,7 +215,7 @@ DEFAULT_HF_BASE: str = "https://router.huggingface.co"
|
|
| 179 |
|
| 180 |
#: Fallback model ID when the request body omits the ``model`` field.
|
| 181 |
#: Must have a registered HF Inference Provider for Path 3.
|
| 182 |
-
DEFAULT_MODEL: str = "scikit-plots/Qwen2.5-Coder-
|
| 183 |
|
| 184 |
#: Global upstream read timeout in seconds (used for Path 1 / backward compat).
|
| 185 |
#:
|
|
@@ -224,6 +260,90 @@ DEFAULT_HF_SPACES_MODEL_URL: str = (
|
|
| 224 |
DEFAULT_HF_SPACES_MODEL_NAMESPACES: tuple[str, ...] = ("scikit-plots",)
|
| 225 |
|
| 226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 228 |
# Pure helper functions
|
| 229 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -340,11 +460,11 @@ def _parse_model(body: bytes, default: str = DEFAULT_MODEL) -> str:
|
|
| 340 |
>>> _parse_model(b'{"model": "Qwen/Qwen2.5-Coder-7B-Instruct"}')
|
| 341 |
'Qwen/Qwen2.5-Coder-7B-Instruct'
|
| 342 |
>>> _parse_model(b"{}")
|
| 343 |
-
'scikit-plots/Qwen2.5-Coder-
|
| 344 |
>>> _parse_model(b"not-json")
|
| 345 |
-
'scikit-plots/Qwen2.5-Coder-
|
| 346 |
>>> _parse_model(b'{"model": " "}')
|
| 347 |
-
'scikit-plots/Qwen2.5-Coder-
|
| 348 |
"""
|
| 349 |
try:
|
| 350 |
data: Any = json.loads(body)
|
|
@@ -441,7 +561,7 @@ def _build_cors_headers(allowed_origin: str = "*") -> dict[str, str]:
|
|
| 441 |
}
|
| 442 |
|
| 443 |
|
| 444 |
-
def _token_log_fragment(token: str) -> str:
|
| 445 |
"""
|
| 446 |
Produce a safely-truncated token string for log output.
|
| 447 |
|
|
@@ -449,6 +569,11 @@ def _token_log_fragment(token: str) -> str:
|
|
| 449 |
----------
|
| 450 |
token : str
|
| 451 |
A HuggingFace API token (typically ``hf_xxx...``).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
Returns
|
| 454 |
-------
|
|
@@ -466,13 +591,322 @@ def _token_log_fragment(token: str) -> str:
|
|
| 466 |
--------
|
| 467 |
>>> _token_log_fragment("hf_abcdefghij1234")
|
| 468 |
'hf_abcde...1234'
|
|
|
|
|
|
|
| 469 |
>>> _token_log_fragment("")
|
| 470 |
'<not-set>'
|
| 471 |
"""
|
| 472 |
if not token or len(token) < 12: # noqa: PLR2004
|
| 473 |
return "<not-set>"
|
| 474 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
|
| 477 |
def _resolve_upstream_url(
|
| 478 |
body: bytes,
|
|
@@ -711,6 +1145,12 @@ def load_proxy_env() -> dict[str, Any]:
|
|
| 711 |
Path 3 read timeout (env ``PATH3_TIMEOUT``).
|
| 712 |
``max_body_bytes`` : int
|
| 713 |
``allowed_origins`` : str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
|
| 715 |
Examples
|
| 716 |
--------
|
|
@@ -736,6 +1176,18 @@ def load_proxy_env() -> dict[str, Any]:
|
|
| 736 |
_hf_token: str = os.environ.get("HF_TOKEN", "").strip()
|
| 737 |
_hf_write_token: str = os.environ.get("HF_WRITE_TOKEN", "").strip()
|
| 738 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
return {
|
| 740 |
"backend_url": os.environ.get("BACKEND_URL", "").strip(),
|
| 741 |
"hf_token": _hf_token,
|
|
@@ -743,8 +1195,13 @@ def load_proxy_env() -> dict[str, Any]:
|
|
| 743 |
# Principle of least privilege: set HF_WRITE_TOKEN to a write-scoped
|
| 744 |
# fine-grained token so HF_TOKEN can remain read-only / inference-only.
|
| 745 |
# Falls back to hf_token when absent for backward compatibility.
|
| 746 |
-
"hf_write_token":
|
| 747 |
-
"hf_dataset_token":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
"hf_base": os.environ.get("HF_BASE", DEFAULT_HF_BASE).rstrip("/"),
|
| 749 |
"default_model": (
|
| 750 |
os.environ.get("DEFAULT_MODEL", DEFAULT_MODEL).strip() or DEFAULT_MODEL
|
|
|
|
| 1 |
+
# _shared_logic.py v6.1.0
|
| 2 |
#
|
| 3 |
# Single source of truth for shared constants, pure helper functions, and
|
| 4 |
# type aliases used by the deployed proxy (_hf_spaces_proxy/app.py) and the
|
|
|
|
| 56 |
# Callers who hard-code ``HF_BASE`` to the old hostname must migrate to
|
| 57 |
# the new router URL.
|
| 58 |
#
|
| 59 |
+
# New in v6.1.0 β Three-type HF token system
|
| 60 |
+
# -------------------------------------------
|
| 61 |
+
# + ``HFTokenType`` literal type alias added: ``"fine-grained" | "read" |
|
| 62 |
+
# ``"write" | "unknown"``. Maps directly to the three token types exposed
|
| 63 |
+
# in HF Settings β Tokens.
|
| 64 |
+
# + ``HF_TOKEN_TYPE_*`` string constants and ``HF_INFERENCE_TOKEN_TYPES`` /
|
| 65 |
+
# ``HF_WRITE_TOKEN_TYPES`` frozensets added for type-safe comparisons.
|
| 66 |
+
# + ``_classify_token_type()`` β classify a token by explicit env-var
|
| 67 |
+
# declaration (``HF_TOKEN_TYPE``, ``HF_WRITE_TOKEN_TYPE``) with a length-
|
| 68 |
+
# based heuristic fallback.
|
| 69 |
+
# + ``_token_suitable_for_inference()`` / ``_token_suitable_for_writes()``
|
| 70 |
+
# predicates for principle-of-least-privilege validation.
|
| 71 |
+
# + ``_validate_token_config()`` β returns actionable WARNING / ERROR strings
|
| 72 |
+
# for token-type mismatches detected at startup.
|
| 73 |
+
# + ``_token_log_fragment()`` gains an optional ``token_type`` parameter so
|
| 74 |
+
# log lines include the token type (e.g. ``hf_abcde...1234 (read)``).
|
| 75 |
+
# + ``load_proxy_env()`` extended with ``hf_token_type`` and
|
| 76 |
+
# ``hf_write_token_type`` keys read from the matching env vars.
|
| 77 |
+
# + ``_safe_float`` added to ``__all__`` (was importable but unadvertised).
|
| 78 |
+
#
|
| 79 |
# SPDX-License-Identifier: BSD-3-Clause
|
| 80 |
# Authors: The scikit-plots developers
|
| 81 |
|
|
|
|
| 154 |
|
| 155 |
import json
|
| 156 |
import os
|
| 157 |
+
from typing import Any, Literal
|
| 158 |
|
| 159 |
__all__ = [
|
| 160 |
+
# Version
|
| 161 |
+
"PROXY_VERSION",
|
| 162 |
+
# Constants β routing / timeout
|
| 163 |
"DEFAULT_HF_BASE",
|
| 164 |
"DEFAULT_HF_SPACES_MODEL_NAMESPACES",
|
| 165 |
"DEFAULT_HF_SPACES_MODEL_URL",
|
|
|
|
| 168 |
"DEFAULT_PATH2_READ_TIMEOUT",
|
| 169 |
"DEFAULT_PATH3_READ_TIMEOUT",
|
| 170 |
"DEFAULT_PROXY_TIMEOUT",
|
| 171 |
+
# Constants β token type system (v6.1.0)
|
| 172 |
+
"HFTokenType",
|
| 173 |
+
"HF_TOKEN_TYPE_FINE_GRAINED",
|
| 174 |
+
"HF_TOKEN_TYPE_READ",
|
| 175 |
+
"HF_TOKEN_TYPE_WRITE",
|
| 176 |
+
"HF_TOKEN_TYPE_UNKNOWN",
|
| 177 |
+
"HF_INFERENCE_TOKEN_TYPES",
|
| 178 |
+
"HF_WRITE_TOKEN_TYPES",
|
| 179 |
+
# Helpers β general
|
| 180 |
"_build_cors_headers",
|
| 181 |
"_is_custom_model_namespace",
|
| 182 |
"_parse_model",
|
| 183 |
+
"_safe_float",
|
| 184 |
"_safe_int",
|
| 185 |
"_token_log_fragment",
|
| 186 |
+
# Helpers β token type system (v6.1.0)
|
| 187 |
+
"_classify_token_type",
|
| 188 |
+
"_token_suitable_for_inference",
|
| 189 |
+
"_token_suitable_for_writes",
|
| 190 |
+
"_validate_token_config",
|
| 191 |
+
# Helpers β routing / env
|
| 192 |
+
"_resolve_upstream_url",
|
| 193 |
"_validate_env",
|
| 194 |
"load_proxy_env",
|
| 195 |
]
|
|
|
|
| 200 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 201 |
|
| 202 |
#: Proxy release version β bump on every breaking change.
|
| 203 |
+
PROXY_VERSION: str = "6.1.0"
|
| 204 |
|
| 205 |
#: HuggingFace Inference Providers router base URL (no trailing slash).
|
| 206 |
#: Only used for Path 3 (standard provider models) when ``BACKEND_URL`` is
|
|
|
|
| 215 |
|
| 216 |
#: Fallback model ID when the request body omits the ``model`` field.
|
| 217 |
#: Must have a registered HF Inference Provider for Path 3.
|
| 218 |
+
DEFAULT_MODEL: str = "scikit-plots/Qwen2.5-Coder-7B-Instruct"
|
| 219 |
|
| 220 |
#: Global upstream read timeout in seconds (used for Path 1 / backward compat).
|
| 221 |
#:
|
|
|
|
| 260 |
DEFAULT_HF_SPACES_MODEL_NAMESPACES: tuple[str, ...] = ("scikit-plots",)
|
| 261 |
|
| 262 |
|
| 263 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 264 |
+
# HuggingFace token type system (v6.1.0)
|
| 265 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 266 |
+
#
|
| 267 |
+
# HuggingFace exposes exactly three token types in
|
| 268 |
+
# https://huggingface.co/settings/tokens:
|
| 269 |
+
#
|
| 270 |
+
# β Fine-grained β New-style token. Permissions set at creation time:
|
| 271 |
+
# choose any combination of per-repo access levels and
|
| 272 |
+
# API capabilities. Recommended for production because
|
| 273 |
+
# each token carries only the minimum required scope.
|
| 274 |
+
#
|
| 275 |
+
# β‘ Read (classic) β Legacy read-only token. Grants read access to all
|
| 276 |
+
# public repos and any private repos you can access.
|
| 277 |
+
# Always includes the Serverless Inference API capability.
|
| 278 |
+
# Cannot push commits or create repos.
|
| 279 |
+
#
|
| 280 |
+
# β’ Write (classic)β Legacy read+write token. All read permissions plus
|
| 281 |
+
# the ability to push commits, create repos, manage
|
| 282 |
+
# members, etc. Over-privileged for inference-only use.
|
| 283 |
+
#
|
| 284 |
+
# Mapping to proxy env vars
|
| 285 |
+
# βββββββββββββββββββββββββ
|
| 286 |
+
# HF_TOKEN β inference token (Path 2 private Space + Path 3 HF API).
|
| 287 |
+
# Best practice: fine-grained with inference-api scope only,
|
| 288 |
+
# OR classic read. Never use a write token here.
|
| 289 |
+
#
|
| 290 |
+
# HF_WRITE_TOKEN β dataset write token (/v1/contribute β HfApi.create_commit).
|
| 291 |
+
# Best practice: fine-grained scoped to ONE dataset repo,
|
| 292 |
+
# OR classic write. Never use a read token here.
|
| 293 |
+
#
|
| 294 |
+
# Optional type-declaration env vars (Space β Settings β Repository secrets):
|
| 295 |
+
# HF_TOKEN_TYPE = fine-grained | read | write (default: auto-detect)
|
| 296 |
+
# HF_WRITE_TOKEN_TYPE = fine-grained | write (default: auto-detect)
|
| 297 |
+
#
|
| 298 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 299 |
+
|
| 300 |
+
#: Literal type for HuggingFace token type labels.
|
| 301 |
+
#: Use as type annotation and for exhaustive ``isinstance``-free comparisons.
|
| 302 |
+
HFTokenType = Literal["fine-grained", "read", "write", "unknown"]
|
| 303 |
+
|
| 304 |
+
#: New-style fine-grained HF token. Permissions defined at creation time.
|
| 305 |
+
#: Declare via env var: ``HF_TOKEN_TYPE=fine-grained``.
|
| 306 |
+
HF_TOKEN_TYPE_FINE_GRAINED: str = "fine-grained"
|
| 307 |
+
|
| 308 |
+
#: Classic HF read token. Read + Inference API; no write capability.
|
| 309 |
+
#: Declare via env var: ``HF_TOKEN_TYPE=read``.
|
| 310 |
+
HF_TOKEN_TYPE_READ: str = "read"
|
| 311 |
+
|
| 312 |
+
#: Classic HF write token. All read permissions + repo push capability.
|
| 313 |
+
#: Declare via env var: ``HF_TOKEN_TYPE=write`` or ``HF_WRITE_TOKEN_TYPE=write``.
|
| 314 |
+
HF_TOKEN_TYPE_WRITE: str = "write"
|
| 315 |
+
|
| 316 |
+
#: Sentinel: token type not declared and could not be inferred.
|
| 317 |
+
#: Runtime operations are not blocked, but :func:`_validate_token_config` omits
|
| 318 |
+
#: least-privilege warnings because the type is unknown.
|
| 319 |
+
HF_TOKEN_TYPE_UNKNOWN: str = "unknown"
|
| 320 |
+
|
| 321 |
+
#: Token types that are appropriate for HF Serverless Inference API calls
|
| 322 |
+
#: (Path 3) and private HF Space access (Path 2).
|
| 323 |
+
#:
|
| 324 |
+
#: Classic write tokens ARE technically capable of inference (write β read),
|
| 325 |
+
#: but are excluded from this set so :func:`_validate_token_config` can emit
|
| 326 |
+
#: a startup warning when a write token is used where a read / fine-grained
|
| 327 |
+
#: token is the correct choice. The ``"unknown"`` sentinel is included so
|
| 328 |
+
#: that un-declared tokens do not trigger false-positive warnings.
|
| 329 |
+
HF_INFERENCE_TOKEN_TYPES: frozenset[str] = frozenset({
|
| 330 |
+
HF_TOKEN_TYPE_FINE_GRAINED,
|
| 331 |
+
HF_TOKEN_TYPE_READ,
|
| 332 |
+
HF_TOKEN_TYPE_UNKNOWN,
|
| 333 |
+
})
|
| 334 |
+
|
| 335 |
+
#: Token types that can push commits to HuggingFace repos and datasets.
|
| 336 |
+
#:
|
| 337 |
+
#: Classic read tokens **cannot** write β any ``HfApi.create_commit`` call
|
| 338 |
+
#: returns HTTP 403 / 401. ``"unknown"`` is excluded so that
|
| 339 |
+
#: :func:`_validate_token_config` can flag a read token configured as the write
|
| 340 |
+
#: token as a hard error rather than silently failing at request time.
|
| 341 |
+
HF_WRITE_TOKEN_TYPES: frozenset[str] = frozenset({
|
| 342 |
+
HF_TOKEN_TYPE_FINE_GRAINED,
|
| 343 |
+
HF_TOKEN_TYPE_WRITE,
|
| 344 |
+
})
|
| 345 |
+
|
| 346 |
+
|
| 347 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 348 |
# Pure helper functions
|
| 349 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 460 |
>>> _parse_model(b'{"model": "Qwen/Qwen2.5-Coder-7B-Instruct"}')
|
| 461 |
'Qwen/Qwen2.5-Coder-7B-Instruct'
|
| 462 |
>>> _parse_model(b"{}")
|
| 463 |
+
'scikit-plots/Qwen2.5-Coder-7B-Instruct'
|
| 464 |
>>> _parse_model(b"not-json")
|
| 465 |
+
'scikit-plots/Qwen2.5-Coder-7B-Instruct'
|
| 466 |
>>> _parse_model(b'{"model": " "}')
|
| 467 |
+
'scikit-plots/Qwen2.5-Coder-7B-Instruct'
|
| 468 |
"""
|
| 469 |
try:
|
| 470 |
data: Any = json.loads(body)
|
|
|
|
| 561 |
}
|
| 562 |
|
| 563 |
|
| 564 |
+
def _token_log_fragment(token: str, token_type: str = "") -> str:
|
| 565 |
"""
|
| 566 |
Produce a safely-truncated token string for log output.
|
| 567 |
|
|
|
|
| 569 |
----------
|
| 570 |
token : str
|
| 571 |
A HuggingFace API token (typically ``hf_xxx...``).
|
| 572 |
+
token_type : str, optional
|
| 573 |
+
Token type string (one of the ``HF_TOKEN_TYPE_*`` constants).
|
| 574 |
+
When non-empty and not ``"unknown"``, appended as a parenthetical
|
| 575 |
+
suffix so log lines identify the token class without exposing the
|
| 576 |
+
value. Example: ``hf_abcde...1234 (read)``.
|
| 577 |
|
| 578 |
Returns
|
| 579 |
-------
|
|
|
|
| 591 |
--------
|
| 592 |
>>> _token_log_fragment("hf_abcdefghij1234")
|
| 593 |
'hf_abcde...1234'
|
| 594 |
+
>>> _token_log_fragment("hf_abcdefghij1234", token_type="read")
|
| 595 |
+
'hf_abcde...1234 (read)'
|
| 596 |
>>> _token_log_fragment("")
|
| 597 |
'<not-set>'
|
| 598 |
"""
|
| 599 |
if not token or len(token) < 12: # noqa: PLR2004
|
| 600 |
return "<not-set>"
|
| 601 |
+
fragment = f"{token[:8]}...{token[-4:]}"
|
| 602 |
+
if token_type and token_type != HF_TOKEN_TYPE_UNKNOWN:
|
| 603 |
+
return f"{fragment} ({token_type})"
|
| 604 |
+
return fragment
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
def _classify_token_type(
|
| 608 |
+
token: str,
|
| 609 |
+
declared_type: str | None = None,
|
| 610 |
+
) -> HFTokenType:
|
| 611 |
+
"""
|
| 612 |
+
Classify a HuggingFace token by its declared type or format heuristics.
|
| 613 |
+
|
| 614 |
+
Token type classification is used at startup by :func:`_validate_token_config`
|
| 615 |
+
to enforce the principle of least privilege before any requests arrive.
|
| 616 |
+
|
| 617 |
+
Parameters
|
| 618 |
+
----------
|
| 619 |
+
token : str
|
| 620 |
+
The HuggingFace API token string.
|
| 621 |
+
declared_type : str or None, optional
|
| 622 |
+
Explicitly declared type from an environment variable
|
| 623 |
+
(``HF_TOKEN_TYPE`` or ``HF_WRITE_TOKEN_TYPE``).
|
| 624 |
+
Accepted values: ``"fine-grained"``, ``"read"``, ``"write"``
|
| 625 |
+
(and minor formatting variants: ``"finegrained"``,
|
| 626 |
+
``"fine_grained"``). When provided and recognized, it takes
|
| 627 |
+
precedence over all heuristics.
|
| 628 |
|
| 629 |
+
Returns
|
| 630 |
+
-------
|
| 631 |
+
HFTokenType
|
| 632 |
+
One of ``"fine-grained"``, ``"read"``, ``"write"``, or ``"unknown"``.
|
| 633 |
+
|
| 634 |
+
Notes
|
| 635 |
+
-----
|
| 636 |
+
**Security note** β Token type cannot be verified without an authenticated
|
| 637 |
+
call to the HF API (``GET https://huggingface.co/api/whoami-v2``). This
|
| 638 |
+
function applies lightweight format heuristics only. For production
|
| 639 |
+
deployments, always declare the type explicitly via ``HF_TOKEN_TYPE`` /
|
| 640 |
+
``HF_WRITE_TOKEN_TYPE`` so :func:`_validate_token_config` can enforce
|
| 641 |
+
least-privilege at startup without any network calls.
|
| 642 |
+
|
| 643 |
+
**Developer note** β As of 2025, classic HF tokens are approximately 34
|
| 644 |
+
characters total (``hf_`` prefix + 30 alphanumeric chars). Fine-grained
|
| 645 |
+
tokens are substantially longer (β₯ 52 characters total as of the HF 2025
|
| 646 |
+
token format). This length heuristic is imprecise and subject to silent
|
| 647 |
+
change by HF; explicit declaration via env vars is always preferred.
|
| 648 |
+
|
| 649 |
+
Examples
|
| 650 |
+
--------
|
| 651 |
+
Explicit declaration takes precedence over heuristics:
|
| 652 |
+
|
| 653 |
+
>>> _classify_token_type("hf_" + "a" * 30, declared_type="read")
|
| 654 |
+
'read'
|
| 655 |
+
>>> _classify_token_type("hf_" + "a" * 30, declared_type="write")
|
| 656 |
+
'write'
|
| 657 |
+
|
| 658 |
+
Heuristic: token β₯ 52 chars β fine-grained:
|
| 659 |
+
|
| 660 |
+
>>> _classify_token_type("hf_" + "a" * 50)
|
| 661 |
+
'fine-grained'
|
| 662 |
+
|
| 663 |
+
Short classic token without declaration β unknown:
|
| 664 |
+
|
| 665 |
+
>>> _classify_token_type("hf_" + "a" * 28)
|
| 666 |
+
'unknown'
|
| 667 |
+
|
| 668 |
+
Empty or malformed token β unknown:
|
| 669 |
+
|
| 670 |
+
>>> _classify_token_type("")
|
| 671 |
+
'unknown'
|
| 672 |
+
"""
|
| 673 |
+
# Normalise accepted declared-type values (tolerate minor formatting variants).
|
| 674 |
+
_declared_map: dict[str, HFTokenType] = {
|
| 675 |
+
"fine-grained": "fine-grained",
|
| 676 |
+
"finegrained": "fine-grained",
|
| 677 |
+
"fine_grained": "fine-grained",
|
| 678 |
+
"read": "read",
|
| 679 |
+
"write": "write",
|
| 680 |
+
}
|
| 681 |
+
if declared_type:
|
| 682 |
+
normalised = _declared_map.get(declared_type.lower().strip())
|
| 683 |
+
if normalised is not None:
|
| 684 |
+
return normalised
|
| 685 |
+
|
| 686 |
+
# Validate basic token format β all HF tokens start with "hf_".
|
| 687 |
+
if not token or not token.startswith("hf_") or len(token) < 10: # noqa: PLR2004
|
| 688 |
+
return "unknown"
|
| 689 |
+
|
| 690 |
+
# Heuristic: fine-grained tokens are substantially longer than classic tokens.
|
| 691 |
+
# Classic tokens: ~34 chars total. Fine-grained tokens: β₯ 52 chars (HF 2025).
|
| 692 |
+
# Best-effort only; explicit declaration via env vars is always preferred.
|
| 693 |
+
if len(token) >= 52: # noqa: PLR2004
|
| 694 |
+
return "fine-grained"
|
| 695 |
+
|
| 696 |
+
# Cannot distinguish classic read vs write by token string alone.
|
| 697 |
+
return "unknown"
|
| 698 |
+
|
| 699 |
+
|
| 700 |
+
def _token_suitable_for_inference(token_type: str) -> bool:
|
| 701 |
+
"""
|
| 702 |
+
Return ``True`` when *token_type* is appropriate for HF Inference API calls.
|
| 703 |
+
|
| 704 |
+
This predicate guards inference paths (Path 2 private Space access and
|
| 705 |
+
Path 3 HF Serverless API). Returning ``False`` for a classic write token
|
| 706 |
+
does not block the token at runtime β it causes :func:`_validate_token_config`
|
| 707 |
+
to emit a startup ``WARNING`` so the operator knows they are running with
|
| 708 |
+
more permission than necessary.
|
| 709 |
+
|
| 710 |
+
Parameters
|
| 711 |
+
----------
|
| 712 |
+
token_type : str
|
| 713 |
+
One of the ``HF_TOKEN_TYPE_*`` constants or a free-form string parsed
|
| 714 |
+
from an environment variable.
|
| 715 |
+
|
| 716 |
+
Returns
|
| 717 |
+
-------
|
| 718 |
+
bool
|
| 719 |
+
``True`` for ``"fine-grained"``, ``"read"``, and ``"unknown"``.
|
| 720 |
+
``False`` for ``"write"`` (classic write token β over-privileged).
|
| 721 |
+
|
| 722 |
+
Notes
|
| 723 |
+
-----
|
| 724 |
+
The recommended configuration is a fine-grained token scoped exclusively
|
| 725 |
+
to ``Make calls to the serverless Inference API``, or a classic read
|
| 726 |
+
token. Classic write tokens carry unnecessary repo-write permission
|
| 727 |
+
and violate the principle of least privilege.
|
| 728 |
+
|
| 729 |
+
Examples
|
| 730 |
+
--------
|
| 731 |
+
>>> _token_suitable_for_inference("read")
|
| 732 |
+
True
|
| 733 |
+
>>> _token_suitable_for_inference("fine-grained")
|
| 734 |
+
True
|
| 735 |
+
>>> _token_suitable_for_inference("write")
|
| 736 |
+
False
|
| 737 |
+
>>> _token_suitable_for_inference("unknown")
|
| 738 |
+
True
|
| 739 |
+
"""
|
| 740 |
+
return token_type in HF_INFERENCE_TOKEN_TYPES
|
| 741 |
+
|
| 742 |
+
|
| 743 |
+
def _token_suitable_for_writes(token_type: str) -> bool:
|
| 744 |
+
"""
|
| 745 |
+
Return ``True`` when *token_type* can authorize HuggingFace write operations.
|
| 746 |
+
|
| 747 |
+
This predicate guards the ``/v1/contribute`` endpoint. Returning ``False``
|
| 748 |
+
for a classic read or unknown token causes :func:`_validate_token_config`
|
| 749 |
+
to emit a startup ``ERROR`` string because the token WILL fail at
|
| 750 |
+
``HfApi.create_commit`` time (HTTP 403 / 401 from HF).
|
| 751 |
+
|
| 752 |
+
Parameters
|
| 753 |
+
----------
|
| 754 |
+
token_type : str
|
| 755 |
+
One of the ``HF_TOKEN_TYPE_*`` constants or a free-form string parsed
|
| 756 |
+
from an environment variable.
|
| 757 |
+
|
| 758 |
+
Returns
|
| 759 |
+
-------
|
| 760 |
+
bool
|
| 761 |
+
``True`` for ``"fine-grained"`` and ``"write"``.
|
| 762 |
+
``False`` for ``"read"`` and ``"unknown"``.
|
| 763 |
+
|
| 764 |
+
Notes
|
| 765 |
+
-----
|
| 766 |
+
Fine-grained tokens can write **only if** write permission was granted to
|
| 767 |
+
the target repo at token-creation time. A fine-grained token created
|
| 768 |
+
with only inference-API scope will also fail on write operations, but the
|
| 769 |
+
proxy cannot verify fine-grained permissions without an authenticated API
|
| 770 |
+
call. Fine-grained tokens are therefore accepted here and any permission
|
| 771 |
+
failures surface at operation time with a clear HTTP 503 error.
|
| 772 |
+
|
| 773 |
+
Examples
|
| 774 |
+
--------
|
| 775 |
+
>>> _token_suitable_for_writes("write")
|
| 776 |
+
True
|
| 777 |
+
>>> _token_suitable_for_writes("fine-grained")
|
| 778 |
+
True
|
| 779 |
+
>>> _token_suitable_for_writes("read")
|
| 780 |
+
False
|
| 781 |
+
>>> _token_suitable_for_writes("unknown")
|
| 782 |
+
False
|
| 783 |
+
"""
|
| 784 |
+
return token_type in HF_WRITE_TOKEN_TYPES
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
def _validate_token_config(
|
| 788 |
+
hf_token: str,
|
| 789 |
+
hf_write_token: str,
|
| 790 |
+
training_dataset_repo: str = "",
|
| 791 |
+
*,
|
| 792 |
+
hf_token_type: str = HF_TOKEN_TYPE_UNKNOWN,
|
| 793 |
+
hf_write_token_type: str = HF_TOKEN_TYPE_UNKNOWN,
|
| 794 |
+
) -> list[str]:
|
| 795 |
+
"""
|
| 796 |
+
Validate token types and return actionable warning / error strings.
|
| 797 |
+
|
| 798 |
+
Enforces the principle of least privilege and detects token-type
|
| 799 |
+
misconfigurations that would cause silent failures at request time.
|
| 800 |
+
Returns a list of strings rather than raising exceptions so the proxy
|
| 801 |
+
can start in degraded mode and surface issues through structured logs.
|
| 802 |
+
|
| 803 |
+
Call this at startup **after** :func:`_validate_env` so routing is
|
| 804 |
+
confirmed viable before type checks are run.
|
| 805 |
+
|
| 806 |
+
Parameters
|
| 807 |
+
----------
|
| 808 |
+
hf_token : str
|
| 809 |
+
HuggingFace token used for inference (``HF_TOKEN`` env var).
|
| 810 |
+
hf_write_token : str
|
| 811 |
+
HuggingFace token used for dataset writes (``HF_WRITE_TOKEN`` env
|
| 812 |
+
var). Pass empty string when not configured.
|
| 813 |
+
training_dataset_repo : str, optional
|
| 814 |
+
HuggingFace Dataset repo ID (``TRAINING_DATASET_REPO`` env var).
|
| 815 |
+
Pass empty string when ``/v1/contribute`` is not enabled.
|
| 816 |
+
hf_token_type : str, optional
|
| 817 |
+
Classified type for *hf_token* (from :func:`_classify_token_type`).
|
| 818 |
+
Defaults to ``"unknown"``.
|
| 819 |
+
hf_write_token_type : str, optional
|
| 820 |
+
Classified type for *hf_write_token*. Defaults to ``"unknown"``.
|
| 821 |
+
|
| 822 |
+
Returns
|
| 823 |
+
-------
|
| 824 |
+
list[str]
|
| 825 |
+
Zero or more diagnostic strings. Each message is prefixed with
|
| 826 |
+
``"WARNING:"`` or ``"ERROR:"`` so callers can log at the correct
|
| 827 |
+
level. An empty list means the configuration passes all checks.
|
| 828 |
+
|
| 829 |
+
Notes
|
| 830 |
+
-----
|
| 831 |
+
**Security note** β ``"write"`` token used for inference is a WARNING
|
| 832 |
+
(not an error) because it functions correctly at runtime. The warning
|
| 833 |
+
exists to prompt the operator to apply least-privilege.
|
| 834 |
+
|
| 835 |
+
**Security note** β ``"read"`` token used for writes is a hard ERROR:
|
| 836 |
+
the token WILL fail on every ``HfApi.create_commit`` call. The proxy
|
| 837 |
+
can still start (useful for operators who only need inference), but
|
| 838 |
+
``/v1/contribute`` will be permanently non-functional until the token is
|
| 839 |
+
replaced.
|
| 840 |
+
|
| 841 |
+
Examples
|
| 842 |
+
--------
|
| 843 |
+
Clean configuration β no messages:
|
| 844 |
+
|
| 845 |
+
>>> _validate_token_config("hf_readtok", "", hf_token_type="read")
|
| 846 |
+
[]
|
| 847 |
+
|
| 848 |
+
Write token for inference (overprivileged) β WARNING:
|
| 849 |
+
|
| 850 |
+
>>> msgs = _validate_token_config("hf_writetok", "", hf_token_type="write")
|
| 851 |
+
>>> any("WARNING" in m for m in msgs)
|
| 852 |
+
True
|
| 853 |
+
|
| 854 |
+
Read token for writes β ERROR:
|
| 855 |
+
|
| 856 |
+
>>> msgs = _validate_token_config(
|
| 857 |
+
... "hf_tok", "hf_readtok",
|
| 858 |
+
... training_dataset_repo="org/dataset",
|
| 859 |
+
... hf_write_token_type="read",
|
| 860 |
+
... )
|
| 861 |
+
>>> any("ERROR" in m for m in msgs)
|
| 862 |
+
True
|
| 863 |
+
"""
|
| 864 |
+
messages: list[str] = []
|
| 865 |
+
|
| 866 |
+
# ββ Inference token (HF_TOKEN) type check ββββββββββββββββββββββββββββββββ
|
| 867 |
+
if hf_token and not _token_suitable_for_inference(hf_token_type):
|
| 868 |
+
messages.append(
|
| 869 |
+
f"WARNING: HF_TOKEN type is {hf_token_type!r} (classic write token). "
|
| 870 |
+
"Write tokens carry unnecessary repo-push permission and violate the "
|
| 871 |
+
"principle of least privilege for inference. "
|
| 872 |
+
"Replace HF_TOKEN with: (a) a fine-grained token scoped to "
|
| 873 |
+
"'Make calls to the serverless Inference API' only, or "
|
| 874 |
+
"(b) a classic read token. "
|
| 875 |
+
"See HF Settings β Tokens β New token β Fine-grained. "
|
| 876 |
+
"Set HF_TOKEN_TYPE=read or HF_TOKEN_TYPE=fine-grained after replacing."
|
| 877 |
+
)
|
| 878 |
+
|
| 879 |
+
# ββ Write token (HF_WRITE_TOKEN) type check ββββββββββββββββββββββββββββββ
|
| 880 |
+
if hf_write_token and not _token_suitable_for_writes(hf_write_token_type):
|
| 881 |
+
messages.append(
|
| 882 |
+
f"ERROR: HF_WRITE_TOKEN type is {hf_write_token_type!r}. "
|
| 883 |
+
"Classic read tokens cannot push commits to HuggingFace repositories "
|
| 884 |
+
"(HF API returns HTTP 403 / 401). "
|
| 885 |
+
"Every POST /v1/contribute will fail with HTTP 503. "
|
| 886 |
+
"Replace HF_WRITE_TOKEN with: (a) a fine-grained token with write "
|
| 887 |
+
"access scoped to the training dataset repo, or (b) a classic write "
|
| 888 |
+
"token. "
|
| 889 |
+
"See HF Settings β Tokens β New token β Fine-grained β Write access."
|
| 890 |
+
)
|
| 891 |
+
|
| 892 |
+
# ββ Training repo + effective write token consistency ββββββββββββββββββββ
|
| 893 |
+
if training_dataset_repo:
|
| 894 |
+
# Effective write token is HF_WRITE_TOKEN when set; else falls back to
|
| 895 |
+
# HF_TOKEN. Check that the effective token type can authorize writes.
|
| 896 |
+
effective_token = hf_write_token or hf_token
|
| 897 |
+
effective_type = hf_write_token_type if hf_write_token else hf_token_type
|
| 898 |
+
if effective_token and not _token_suitable_for_writes(effective_type):
|
| 899 |
+
messages.append(
|
| 900 |
+
f"ERROR: TRAINING_DATASET_REPO={training_dataset_repo!r} is "
|
| 901 |
+
"configured but the effective write token type "
|
| 902 |
+
f"({effective_type!r}) cannot push to HuggingFace repositories. "
|
| 903 |
+
"POST /v1/contribute will always fail with HTTP 503. "
|
| 904 |
+
"Set HF_WRITE_TOKEN to a write-capable token (fine-grained with "
|
| 905 |
+
"write access to the dataset repo, or a classic write token). "
|
| 906 |
+
f"Set HF_WRITE_TOKEN_TYPE accordingly."
|
| 907 |
+
)
|
| 908 |
+
|
| 909 |
+
return messages
|
| 910 |
|
| 911 |
def _resolve_upstream_url(
|
| 912 |
body: bytes,
|
|
|
|
| 1145 |
Path 3 read timeout (env ``PATH3_TIMEOUT``).
|
| 1146 |
``max_body_bytes`` : int
|
| 1147 |
``allowed_origins`` : str
|
| 1148 |
+
``hf_token_type`` : str
|
| 1149 |
+
Classified token type for *hf_token* (env ``HF_TOKEN_TYPE``).
|
| 1150 |
+
One of ``"fine-grained"``, ``"read"``, ``"write"``, ``"unknown"``.
|
| 1151 |
+
``hf_write_token_type`` : str
|
| 1152 |
+
Classified token type for *hf_write_token* (env ``HF_WRITE_TOKEN_TYPE``).
|
| 1153 |
+
One of ``"fine-grained"``, ``"write"``, ``"unknown"``.
|
| 1154 |
|
| 1155 |
Examples
|
| 1156 |
--------
|
|
|
|
| 1176 |
_hf_token: str = os.environ.get("HF_TOKEN", "").strip()
|
| 1177 |
_hf_write_token: str = os.environ.get("HF_WRITE_TOKEN", "").strip()
|
| 1178 |
|
| 1179 |
+
# Classify token types from explicit declarations (preferred) or heuristics.
|
| 1180 |
+
# Explicit: set HF_TOKEN_TYPE=read|write|fine-grained in Space secrets.
|
| 1181 |
+
# Heuristic: length-based guess (fine-grained tokens are β₯ 52 chars).
|
| 1182 |
+
_hf_token_type: str = _classify_token_type(
|
| 1183 |
+
_hf_token,
|
| 1184 |
+
declared_type=os.environ.get("HF_TOKEN_TYPE"),
|
| 1185 |
+
)
|
| 1186 |
+
_hf_write_token_type: str = _classify_token_type(
|
| 1187 |
+
_hf_write_token,
|
| 1188 |
+
declared_type=os.environ.get("HF_WRITE_TOKEN_TYPE"),
|
| 1189 |
+
)
|
| 1190 |
+
|
| 1191 |
return {
|
| 1192 |
"backend_url": os.environ.get("BACKEND_URL", "").strip(),
|
| 1193 |
"hf_token": _hf_token,
|
|
|
|
| 1195 |
# Principle of least privilege: set HF_WRITE_TOKEN to a write-scoped
|
| 1196 |
# fine-grained token so HF_TOKEN can remain read-only / inference-only.
|
| 1197 |
# Falls back to hf_token when absent for backward compatibility.
|
| 1198 |
+
"hf_write_token": _hf_write_token,
|
| 1199 |
+
"hf_dataset_token": _hf_write_token or _hf_token,
|
| 1200 |
+
# Token type metadata β used by _validate_token_config at startup.
|
| 1201 |
+
# Operators can override auto-detection by setting HF_TOKEN_TYPE /
|
| 1202 |
+
# HF_WRITE_TOKEN_TYPE to "fine-grained", "read", or "write".
|
| 1203 |
+
"hf_token_type": _hf_token_type,
|
| 1204 |
+
"hf_write_token_type": _hf_write_token_type,
|
| 1205 |
"hf_base": os.environ.get("HF_BASE", DEFAULT_HF_BASE).rstrip("/"),
|
| 1206 |
"default_model": (
|
| 1207 |
os.environ.get("DEFAULT_MODEL", DEFAULT_MODEL).strip() or DEFAULT_MODEL
|
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# scikit-plots/ai Β· _hf_spaces_proxy/app.py v6.
|
| 2 |
#
|
| 3 |
# Thin OpenAI-compatible reverse proxy for sphinx-ai-assistant.
|
| 4 |
#
|
|
@@ -51,13 +51,23 @@
|
|
| 51 |
# HF_BASE HF Serverless API base URL.
|
| 52 |
# Default: https://router.huggingface.co
|
| 53 |
# DEFAULT_MODEL Fallback model when request body omits "model".
|
| 54 |
-
# Default: scikit-plots/Qwen2.5-Coder-
|
| 55 |
# PROXY_TIMEOUT Path 1 read timeout in seconds. Default: 600.
|
| 56 |
# PATH2_TIMEOUT Path 2 read timeout in seconds. Default: 600.
|
| 57 |
# PATH3_TIMEOUT Path 3 read timeout in seconds. Default: 120.
|
| 58 |
# PROXY_CONNECT_TIMEOUT TCP handshake timeout. Default: 10.
|
| 59 |
# PROXY_WRITE_TIMEOUT Request body upload timeout. Default: 30.
|
| 60 |
# PROXY_POOL_TIMEOUT Connection pool acquire timeout. Default: 10.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
# ALLOWED_ORIGINS Comma-separated CORS origins. Default: *.
|
| 62 |
# MAX_BODY_BYTES Maximum accepted body size. Default: 10485760.
|
| 63 |
#
|
|
@@ -129,11 +139,13 @@ try:
|
|
| 129 |
DEFAULT_PATH3_READ_TIMEOUT,
|
| 130 |
DEFAULT_PROXY_TIMEOUT,
|
| 131 |
PROXY_VERSION,
|
|
|
|
| 132 |
_resolve_upstream_url,
|
| 133 |
_safe_float,
|
| 134 |
_safe_int,
|
| 135 |
_token_log_fragment,
|
| 136 |
_validate_env,
|
|
|
|
| 137 |
)
|
| 138 |
except ImportError:
|
| 139 |
from .._shared_logic import ( # type: ignore[import]
|
|
@@ -146,11 +158,13 @@ except ImportError:
|
|
| 146 |
DEFAULT_PATH3_READ_TIMEOUT,
|
| 147 |
DEFAULT_PROXY_TIMEOUT,
|
| 148 |
PROXY_VERSION,
|
|
|
|
| 149 |
_resolve_upstream_url,
|
| 150 |
_safe_float,
|
| 151 |
_safe_int,
|
| 152 |
_token_log_fragment,
|
| 153 |
_validate_env,
|
|
|
|
| 154 |
)
|
| 155 |
|
| 156 |
|
|
@@ -281,6 +295,29 @@ HF_WRITE_TOKEN: str = os.environ.get("HF_WRITE_TOKEN", "").strip()
|
|
| 281 |
#: Never forward this token to model inference backends.
|
| 282 |
HF_DATASET_TOKEN: str = HF_WRITE_TOKEN or HF_TOKEN
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
#: HF Serverless Inference API base URL (no trailing slash).
|
| 285 |
HF_BASE: str = os.environ.get("HF_BASE", DEFAULT_HF_BASE).rstrip("/")
|
| 286 |
|
|
@@ -388,6 +425,21 @@ _feedback_rl_lock = asyncio.Lock()
|
|
| 388 |
|
| 389 |
_validate_env(BACKEND_URL, HF_TOKEN, HF_SPACES_MODEL_URL)
|
| 390 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
if not BACKEND_URL and not HF_TOKEN:
|
| 392 |
logger.warning(
|
| 393 |
"HF_TOKEN is not set. Requests to standard HF Inference API models "
|
|
@@ -459,8 +511,8 @@ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
| 459 |
BACKEND_URL or None,
|
| 460 |
HF_SPACES_MODEL_URL or None,
|
| 461 |
list(HF_SPACES_MODEL_NAMESPACES),
|
| 462 |
-
_token_log_fragment(HF_TOKEN),
|
| 463 |
-
_token_log_fragment(HF_WRITE_TOKEN) if HF_WRITE_TOKEN else "<using-hf-token-fallback>",
|
| 464 |
DEFAULT_MODEL,
|
| 465 |
)
|
| 466 |
logger.info(
|
|
@@ -878,8 +930,15 @@ async def root() -> JSONResponse:
|
|
| 878 |
},
|
| 879 |
"tokens": {
|
| 880 |
# Never expose token values β only whether each slot is filled.
|
| 881 |
-
|
| 882 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
# True when HF_WRITE_TOKEN is set; False means writes fall
|
| 884 |
# back to HF_TOKEN (single-token mode, less secure).
|
| 885 |
"least_privilege_mode": bool(HF_WRITE_TOKEN),
|
|
|
|
| 1 |
+
# scikit-plots/ai Β· _hf_spaces_proxy/app.py v6.1.0
|
| 2 |
#
|
| 3 |
# Thin OpenAI-compatible reverse proxy for sphinx-ai-assistant.
|
| 4 |
#
|
|
|
|
| 51 |
# HF_BASE HF Serverless API base URL.
|
| 52 |
# Default: https://router.huggingface.co
|
| 53 |
# DEFAULT_MODEL Fallback model when request body omits "model".
|
| 54 |
+
# Default: scikit-plots/Qwen2.5-Coder-7B-Instruct
|
| 55 |
# PROXY_TIMEOUT Path 1 read timeout in seconds. Default: 600.
|
| 56 |
# PATH2_TIMEOUT Path 2 read timeout in seconds. Default: 600.
|
| 57 |
# PATH3_TIMEOUT Path 3 read timeout in seconds. Default: 120.
|
| 58 |
# PROXY_CONNECT_TIMEOUT TCP handshake timeout. Default: 10.
|
| 59 |
# PROXY_WRITE_TIMEOUT Request body upload timeout. Default: 30.
|
| 60 |
# PROXY_POOL_TIMEOUT Connection pool acquire timeout. Default: 10.
|
| 61 |
+
# HF_TOKEN_TYPE Declare the type of HF_TOKEN so startup validation can
|
| 62 |
+
# enforce principle-of-least-privilege without network calls.
|
| 63 |
+
# Accepted values: fine-grained | read | write
|
| 64 |
+
# When absent the proxy applies a length-based heuristic.
|
| 65 |
+
# Set to "read" when using a classic read token; set to
|
| 66 |
+
# "fine-grained" for new-style scoped tokens. Never set
|
| 67 |
+
# "write" β that triggers a startup WARNING.
|
| 68 |
+
# HF_WRITE_TOKEN_TYPE Declare the type of HF_WRITE_TOKEN.
|
| 69 |
+
# Accepted values: fine-grained | write
|
| 70 |
+
# Set to match the token created in HF Settings β Tokens.
|
| 71 |
# ALLOWED_ORIGINS Comma-separated CORS origins. Default: *.
|
| 72 |
# MAX_BODY_BYTES Maximum accepted body size. Default: 10485760.
|
| 73 |
#
|
|
|
|
| 139 |
DEFAULT_PATH3_READ_TIMEOUT,
|
| 140 |
DEFAULT_PROXY_TIMEOUT,
|
| 141 |
PROXY_VERSION,
|
| 142 |
+
_classify_token_type,
|
| 143 |
_resolve_upstream_url,
|
| 144 |
_safe_float,
|
| 145 |
_safe_int,
|
| 146 |
_token_log_fragment,
|
| 147 |
_validate_env,
|
| 148 |
+
_validate_token_config,
|
| 149 |
)
|
| 150 |
except ImportError:
|
| 151 |
from .._shared_logic import ( # type: ignore[import]
|
|
|
|
| 158 |
DEFAULT_PATH3_READ_TIMEOUT,
|
| 159 |
DEFAULT_PROXY_TIMEOUT,
|
| 160 |
PROXY_VERSION,
|
| 161 |
+
_classify_token_type,
|
| 162 |
_resolve_upstream_url,
|
| 163 |
_safe_float,
|
| 164 |
_safe_int,
|
| 165 |
_token_log_fragment,
|
| 166 |
_validate_env,
|
| 167 |
+
_validate_token_config,
|
| 168 |
)
|
| 169 |
|
| 170 |
|
|
|
|
| 295 |
#: Never forward this token to model inference backends.
|
| 296 |
HF_DATASET_TOKEN: str = HF_WRITE_TOKEN or HF_TOKEN
|
| 297 |
|
| 298 |
+
#: Classified type for HF_TOKEN β used by startup validation and log output.
|
| 299 |
+
#:
|
| 300 |
+
#: Source priority:
|
| 301 |
+
#: 1. Explicit ``HF_TOKEN_TYPE`` env var (``fine-grained`` | ``read`` | ``write``).
|
| 302 |
+
#: 2. Length-based heuristic: tokens β₯ 52 chars are classified ``"fine-grained"``;
|
| 303 |
+
#: shorter classic tokens that cannot be distinguished return ``"unknown"``.
|
| 304 |
+
#:
|
| 305 |
+
#: Set ``HF_TOKEN_TYPE=read`` or ``HF_TOKEN_TYPE=fine-grained`` in Space secrets
|
| 306 |
+
#: to enable accurate least-privilege startup warnings.
|
| 307 |
+
HF_TOKEN_TYPE: str = _classify_token_type(
|
| 308 |
+
HF_TOKEN,
|
| 309 |
+
declared_type=os.environ.get("HF_TOKEN_TYPE"),
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
#: Classified type for HF_WRITE_TOKEN.
|
| 313 |
+
#: Source priority mirrors HF_TOKEN_TYPE above.
|
| 314 |
+
#: Accepted values: ``"fine-grained"``, ``"write"``.
|
| 315 |
+
#: Returns ``"unknown"`` when the token is absent or type cannot be inferred.
|
| 316 |
+
HF_WRITE_TOKEN_TYPE: str = _classify_token_type(
|
| 317 |
+
HF_WRITE_TOKEN,
|
| 318 |
+
declared_type=os.environ.get("HF_WRITE_TOKEN_TYPE"),
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
#: HF Serverless Inference API base URL (no trailing slash).
|
| 322 |
HF_BASE: str = os.environ.get("HF_BASE", DEFAULT_HF_BASE).rstrip("/")
|
| 323 |
|
|
|
|
| 425 |
|
| 426 |
_validate_env(BACKEND_URL, HF_TOKEN, HF_SPACES_MODEL_URL)
|
| 427 |
|
| 428 |
+
# Token-type least-privilege validation. Runs after _validate_env confirms
|
| 429 |
+
# that routing is viable. Issues are logged at WARNING or ERROR level but
|
| 430 |
+
# never block startup β the proxy starts in degraded mode so operators can
|
| 431 |
+
# read the log and fix the configuration without a redeploy loop.
|
| 432 |
+
_token_config_issues: list[str] = _validate_token_config(
|
| 433 |
+
HF_TOKEN,
|
| 434 |
+
HF_WRITE_TOKEN,
|
| 435 |
+
TRAINING_DATASET_REPO,
|
| 436 |
+
hf_token_type=HF_TOKEN_TYPE,
|
| 437 |
+
hf_write_token_type=HF_WRITE_TOKEN_TYPE,
|
| 438 |
+
)
|
| 439 |
+
for _issue in _token_config_issues:
|
| 440 |
+
_log_level = logging.ERROR if _issue.startswith("ERROR:") else logging.WARNING
|
| 441 |
+
logger.log(_log_level, "Startup token-config check: %s", _issue)
|
| 442 |
+
|
| 443 |
if not BACKEND_URL and not HF_TOKEN:
|
| 444 |
logger.warning(
|
| 445 |
"HF_TOKEN is not set. Requests to standard HF Inference API models "
|
|
|
|
| 511 |
BACKEND_URL or None,
|
| 512 |
HF_SPACES_MODEL_URL or None,
|
| 513 |
list(HF_SPACES_MODEL_NAMESPACES),
|
| 514 |
+
_token_log_fragment(HF_TOKEN, token_type=HF_TOKEN_TYPE),
|
| 515 |
+
_token_log_fragment(HF_WRITE_TOKEN, token_type=HF_WRITE_TOKEN_TYPE) if HF_WRITE_TOKEN else "<using-hf-token-fallback>",
|
| 516 |
DEFAULT_MODEL,
|
| 517 |
)
|
| 518 |
logger.info(
|
|
|
|
| 930 |
},
|
| 931 |
"tokens": {
|
| 932 |
# Never expose token values β only whether each slot is filled.
|
| 933 |
+
# hf_token_type / hf_write_token_type reflect the classified token
|
| 934 |
+
# class so operators can verify least-privilege configuration without
|
| 935 |
+
# reading Space logs. Possible values: fine-grained | read | write |
|
| 936 |
+
# unknown. "unknown" means the type could not be inferred β set
|
| 937 |
+
# HF_TOKEN_TYPE / HF_WRITE_TOKEN_TYPE in Space secrets to resolve.
|
| 938 |
+
"hf_token_set": bool(HF_TOKEN),
|
| 939 |
+
"hf_token_type": HF_TOKEN_TYPE,
|
| 940 |
+
"hf_write_token_set": bool(HF_WRITE_TOKEN),
|
| 941 |
+
"hf_write_token_type": HF_WRITE_TOKEN_TYPE,
|
| 942 |
# True when HF_WRITE_TOKEN is set; False means writes fall
|
| 943 |
# back to HF_TOKEN (single-token mode, less secure).
|
| 944 |
"least_privilege_mode": bool(HF_WRITE_TOKEN),
|