Spaces:
Paused
Paused
File size: 4,187 Bytes
17b22d9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | """Which token pays for a request.
The rule this file exists to enforce: **bill the viewer, not the Space owner,
whenever we possibly can.** A harness that quietly spends the owner's inference
credits on every visitor is one shared link away from an unpleasant invoice.
Precedence, highest first:
1. the signed-in viewer's Hugging Face OAuth token -> billed to the viewer
2. a key the viewer pasted into the UI this session -> billed to the viewer
3. the Space's own ``HF_TOKEN`` secret -> billed to the owner
Step 3 is off unless ``ASE_ALLOW_SPACE_CREDITS`` is set, so the fallback is a
deliberate act rather than a default. Locally it is on, because there is no
OAuth off-Space and a dev loop that cannot make a request is useless.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Credential:
token: str | None
mode: str # oauth | user-key | space-secret | none
who: str | None = None
detail: str = ""
@property
def bills_viewer(self) -> bool:
return self.mode in ("oauth", "user-key")
def to_json(self) -> dict:
return {
"mode": self.mode,
"who": self.who,
"billsViewer": self.bills_viewer,
"detail": self.detail,
}
def on_space() -> bool:
return bool(os.environ.get("SPACE_ID"))
def allow_space_credits() -> bool:
"""Falling back to the owner's token is opt-in on a Space, on by default off it."""
flag = os.environ.get("ASE_ALLOW_SPACE_CREDITS")
if flag is not None:
return flag.strip().lower() in ("1", "true", "yes", "on")
return not on_space()
def resolve(oauth_token: str | None = None,
oauth_user: str | None = None,
user_key: str | None = None) -> Credential:
"""Pick the credential for one request. Never logs or returns the value."""
if oauth_token:
return Credential(oauth_token, "oauth", oauth_user,
"Signed in with Hugging Face; inference is billed to you.")
if user_key:
return Credential(user_key, "user-key", None,
"Using the key you supplied; it is held for this session only.")
secret = (os.environ.get("HF_TOKEN")
or os.environ.get("HUGGING_FACE_HUB_TOKEN")
or _dev_key_file())
if secret and allow_space_credits():
return Credential(
secret, "space-secret", None,
"Falling back to the Space's own token — this bills the Space owner.",
)
if secret:
return Credential(
None, "none", None,
"A Space token exists but owner-billed requests are disabled. "
"Sign in, or supply your own key.",
)
return Credential(None, "none", None,
"No credential available. Supply a Hugging Face token.")
def _dev_key_file() -> str | None:
"""The shared ``key.txt`` this workspace keeps for local development.
Only consulted off a Space. On a Space the token arrives as a secret in the
environment and this path does not exist, so the lookup is dead code there
rather than a second way in.
"""
if on_space():
return None
explicit = os.environ.get("ASE_KEY_FILE")
if explicit:
# Authoritative when set, including when it points at nothing. The
# search below would otherwise reach outside a test's tmp_path and
# find the developer's real key, which is a test that passes for the
# wrong reason and a suite that behaves differently on CI.
candidates = [Path(explicit)]
else:
# The workspace deliberately keeps one gitignored key above the
# projects so several of them can share it. Walk up to find it.
here = Path(__file__).resolve()
candidates = [parent / "key.txt" for parent in here.parents[:5]]
for path in candidates:
try:
if path.is_file():
token = path.read_text().strip()
if token:
return token
except OSError:
continue
return None
|