abidlabs HF Staff Aksel Joonas Reedi akseljoonas commited on
Commit
7a76ad1
·
unverified ·
1 Parent(s): f893532

Use `huggingface_hub.get_token()` for auth fallback (#126)

Browse files

* Use cached HF login token for router auth fallback

Let HF router requests resolve credentials from huggingface_hub's local login cache when INFERENCE_TOKEN, session token, and HF_TOKEN are absent. This keeps CLI and plugin flows working after `hf auth login` without requiring explicit env exports.

Made-with: Cursor

* Consolidate Hugging Face token resolution

Centralize HF token cleanup and lookup so router calls, CLI startup, and backend request paths do not drift after adding huggingface_hub.get_token() fallback. The backend request helper intentionally keeps browser/user token precedence separate from local cached CLI auth.

Constraint: huggingface_hub.get_token() already handles HF_TOKEN, HUGGING_FACE_HUB_TOKEN, and the local hf auth login cache.

Rejected: Use cached login tokens for backend request auth | would let server-local credentials stand in for the browser user.

Confidence: high

Scope-risk: narrow

Directive: Keep router-token fallback and request-user token extraction separate; only router/CLI paths should use cached login fallback.

Tested: UV_CACHE_DIR=/tmp/uv-cache uv run --extra dev pytest tests/unit/test_llm_params.py -q

Tested: UV_CACHE_DIR=/tmp/uv-cache uv run --extra dev pytest tests/unit/test_hf_access.py tests/unit/test_user_quotas.py -q

Tested: python -m py_compile agent/core/hf_tokens.py agent/core/llm_params.py agent/main.py backend/routes/agent.py backend/dependencies.py

Tested: unset HF_TOKEN/INFERENCE_TOKEN cached-token live HfApi.whoami smoke

* Preserve session user id while deduping HF token lookup

The token helper cleanup should not change session identity plumbing. Restore the existing username helper and pass user_id into both interactive and headless submission_loop calls, while removing only the redundant _get_hf_token wrapper.

Constraint: PR #146 relies on user_id being passed into Session for saved-session ownership.

Rejected: Inline HfApi.whoami at each call site | reintroduces duplicate username lookup code and obscures the token-only cleanup.

Confidence: high

Scope-risk: narrow

Directive: Do not remove user_id from submission_loop call sites when editing CLI auth/token code.

Tested: python -m py_compile agent/main.py agent/core/hf_tokens.py agent/core/llm_params.py backend/routes/agent.py backend/dependencies.py

Tested: UV_CACHE_DIR=/tmp/uv-cache uv run --extra dev pytest tests/unit/test_llm_params.py -q

Tested: UV_CACHE_DIR=/tmp/uv-cache uv run --extra dev pytest tests/unit/test_cli_rendering.py tests/unit/test_hf_access.py tests/unit/test_user_quotas.py -q

---------

Co-authored-by: Aksel Joonas Reedi <125026660+akseljoonas@users.noreply.github.com>
Co-authored-by: akseljoonas <aksel.reedi@gmail.com>

agent/core/hf_tokens.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face token resolution helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any
7
+
8
+
9
+ def clean_hf_token(token: str | None) -> str | None:
10
+ """Normalize token strings the same way huggingface_hub does."""
11
+ if token is None:
12
+ return None
13
+ return token.replace("\r", "").replace("\n", "").strip() or None
14
+
15
+
16
+ def get_cached_hf_token() -> str | None:
17
+ """Return the token from huggingface_hub's normal env/cache lookup."""
18
+ try:
19
+ from huggingface_hub import get_token
20
+
21
+ return get_token()
22
+ except Exception:
23
+ return None
24
+
25
+
26
+ def resolve_hf_token(
27
+ *candidates: str | None,
28
+ include_cached: bool = True,
29
+ ) -> str | None:
30
+ """Return the first non-empty explicit token, then optionally HF cache."""
31
+ for token in candidates:
32
+ cleaned = clean_hf_token(token)
33
+ if cleaned:
34
+ return cleaned
35
+ if include_cached:
36
+ return get_cached_hf_token()
37
+ return None
38
+
39
+
40
+ def resolve_hf_router_token(session_hf_token: str | None = None) -> str | None:
41
+ """Resolve the token used for Hugging Face Router LLM calls.
42
+
43
+ App-specific precedence:
44
+ 1. INFERENCE_TOKEN: shared hosted-Space inference token.
45
+ 2. session_hf_token: the active user/session token.
46
+ 3. huggingface_hub.get_token(): HF_TOKEN/HUGGING_FACE_HUB_TOKEN or
47
+ local ``hf auth login`` cache.
48
+ """
49
+ return resolve_hf_token(os.environ.get("INFERENCE_TOKEN"), session_hf_token)
50
+
51
+
52
+ def get_hf_bill_to() -> str | None:
53
+ """Return X-HF-Bill-To only when a shared inference token is active."""
54
+ if clean_hf_token(os.environ.get("INFERENCE_TOKEN")):
55
+ return os.environ.get("HF_BILL_TO", "smolagents")
56
+ return None
57
+
58
+
59
+ def bearer_token_from_header(auth_header: str | None) -> str | None:
60
+ """Extract a cleaned bearer token from an Authorization header."""
61
+ if not auth_header or not auth_header.startswith("Bearer "):
62
+ return None
63
+ return clean_hf_token(auth_header[7:])
64
+
65
+
66
+ def resolve_hf_request_token(
67
+ request: Any,
68
+ *,
69
+ include_env_fallback: bool = True,
70
+ ) -> str | None:
71
+ """Resolve a user token from a FastAPI request.
72
+
73
+ This intentionally does not use the local ``hf auth login`` cache. Backend
74
+ request paths should act as the browser user from Authorization/cookie, or
75
+ fall back only to an explicit server ``HF_TOKEN`` in dev/server contexts.
76
+ """
77
+ token = bearer_token_from_header(request.headers.get("Authorization", ""))
78
+ if token:
79
+ return token
80
+ token = clean_hf_token(request.cookies.get("hf_access_token"))
81
+ if token:
82
+ return token
83
+ if include_env_fallback:
84
+ return clean_hf_token(os.environ.get("HF_TOKEN"))
85
+ return None
agent/core/llm_params.py CHANGED
@@ -5,7 +5,12 @@ can import it without pulling in the whole agent loop / tool router and
5
  creating circular imports.
6
  """
7
 
8
- import os
 
 
 
 
 
9
 
10
 
11
  def _patch_litellm_effort_validation() -> None:
@@ -129,7 +134,8 @@ def _resolve_llm_params(
129
  1. INFERENCE_TOKEN env — shared key on the hosted Space (inference is
130
  free for users, billed to the Space owner via ``X-HF-Bill-To``).
131
  2. session.hf_token — the user's own token (CLI / OAuth / cache file).
132
- 3. HF_TOKEN envbelt-and-suspenders fallback for CLI users.
 
133
  """
134
  if model_name.startswith("anthropic/"):
135
  params: dict = {"model": model_name}
@@ -175,18 +181,13 @@ def _resolve_llm_params(
175
  return params
176
 
177
  hf_model = model_name.removeprefix("huggingface/")
178
- api_key = (
179
- os.environ.get("INFERENCE_TOKEN")
180
- or session_hf_token
181
- or os.environ.get("HF_TOKEN")
182
- )
183
  params = {
184
  "model": f"openai/{hf_model}",
185
  "api_base": "https://router.huggingface.co/v1",
186
  "api_key": api_key,
187
  }
188
- if os.environ.get("INFERENCE_TOKEN"):
189
- bill_to = os.environ.get("HF_BILL_TO", "smolagents")
190
  params["extra_headers"] = {"X-HF-Bill-To": bill_to}
191
  if reasoning_effort:
192
  hf_level = "low" if reasoning_effort == "minimal" else reasoning_effort
 
5
  creating circular imports.
6
  """
7
 
8
+ from agent.core.hf_tokens import get_hf_bill_to, resolve_hf_router_token
9
+
10
+
11
+ def _resolve_hf_router_token(session_hf_token: str | None = None) -> str | None:
12
+ """Backward-compatible private wrapper used by tests and older imports."""
13
+ return resolve_hf_router_token(session_hf_token)
14
 
15
 
16
  def _patch_litellm_effort_validation() -> None:
 
134
  1. INFERENCE_TOKEN env — shared key on the hosted Space (inference is
135
  free for users, billed to the Space owner via ``X-HF-Bill-To``).
136
  2. session.hf_token — the user's own token (CLI / OAuth / cache file).
137
+ 3. huggingface_hub cache``HF_TOKEN`` / ``HUGGING_FACE_HUB_TOKEN`` /
138
+ local ``hf auth login`` cache.
139
  """
140
  if model_name.startswith("anthropic/"):
141
  params: dict = {"model": model_name}
 
181
  return params
182
 
183
  hf_model = model_name.removeprefix("huggingface/")
184
+ api_key = _resolve_hf_router_token(session_hf_token)
 
 
 
 
185
  params = {
186
  "model": f"openai/{hf_model}",
187
  "api_base": "https://router.huggingface.co/v1",
188
  "api_key": api_key,
189
  }
190
+ if bill_to := get_hf_bill_to():
 
191
  params["extra_headers"] = {"X-HF-Bill-To": bill_to}
192
  if reasoning_effort:
193
  hf_level = "low" if reasoning_effort == "minimal" else reasoning_effort
agent/main.py CHANGED
@@ -23,6 +23,7 @@ from prompt_toolkit import PromptSession
23
  from agent.config import load_config
24
  from agent.core.agent_loop import submission_loop
25
  from agent.core import model_switcher
 
26
  from agent.core.session import OpType
27
  from agent.core.tools import ToolRouter
28
  from agent.utils.reliability_checks import check_training_script_save_pattern
@@ -69,28 +70,6 @@ def _safe_get_args(arguments: dict) -> dict:
69
  return args if isinstance(args, dict) else {}
70
 
71
 
72
- def _get_hf_token() -> str | None:
73
- """Get HF token from environment, huggingface_hub API, or cached token file."""
74
- token = os.environ.get("HF_TOKEN")
75
- if token:
76
- return token
77
- try:
78
- from huggingface_hub import HfApi
79
- api = HfApi()
80
- token = api.token
81
- if token:
82
- return token
83
- except Exception:
84
- pass
85
- # Fallback: read the cached token file directly
86
- token_path = Path.home() / ".cache" / "huggingface" / "token"
87
- if token_path.exists():
88
- token = token_path.read_text().strip()
89
- if token:
90
- return token
91
- return None
92
-
93
-
94
  def _get_hf_user(token: str | None) -> str | None:
95
  """Resolve the HF username for a token, if available."""
96
  if not token:
@@ -769,7 +748,7 @@ async def _handle_slash_command(
769
  normalized = arg.removeprefix("huggingface/")
770
  session = session_holder[0] if session_holder else None
771
  await model_switcher.probe_and_switch_model(
772
- normalized, config, session, console, _get_hf_token(),
773
  )
774
  return None
775
 
@@ -838,7 +817,7 @@ async def main():
838
  prompt_session = PromptSession()
839
 
840
  # HF token — required, prompt if missing
841
- hf_token = _get_hf_token()
842
  if not hf_token:
843
  hf_token = await _prompt_and_save_hf_token(prompt_session)
844
 
@@ -1054,7 +1033,7 @@ async def headless_main(
1054
  logging.basicConfig(level=logging.WARNING)
1055
  _configure_runtime_logging()
1056
 
1057
- hf_token = _get_hf_token()
1058
  if not hf_token:
1059
  print("ERROR: No HF token found. Set HF_TOKEN or run `huggingface-cli login`.", file=sys.stderr)
1060
  sys.exit(1)
 
23
  from agent.config import load_config
24
  from agent.core.agent_loop import submission_loop
25
  from agent.core import model_switcher
26
+ from agent.core.hf_tokens import resolve_hf_token
27
  from agent.core.session import OpType
28
  from agent.core.tools import ToolRouter
29
  from agent.utils.reliability_checks import check_training_script_save_pattern
 
70
  return args if isinstance(args, dict) else {}
71
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def _get_hf_user(token: str | None) -> str | None:
74
  """Resolve the HF username for a token, if available."""
75
  if not token:
 
748
  normalized = arg.removeprefix("huggingface/")
749
  session = session_holder[0] if session_holder else None
750
  await model_switcher.probe_and_switch_model(
751
+ normalized, config, session, console, resolve_hf_token(),
752
  )
753
  return None
754
 
 
817
  prompt_session = PromptSession()
818
 
819
  # HF token — required, prompt if missing
820
+ hf_token = resolve_hf_token()
821
  if not hf_token:
822
  hf_token = await _prompt_and_save_hf_token(prompt_session)
823
 
 
1033
  logging.basicConfig(level=logging.WARNING)
1034
  _configure_runtime_logging()
1035
 
1036
+ hf_token = resolve_hf_token()
1037
  if not hf_token:
1038
  print("ERROR: No HF token found. Set HF_TOKEN or run `huggingface-cli login`.", file=sys.stderr)
1039
  sys.exit(1)
backend/dependencies.py CHANGED
@@ -12,6 +12,8 @@ from typing import Any
12
  import httpx
13
  from fastapi import HTTPException, Request, status
14
 
 
 
15
  from agent.core.hf_access import fetch_whoami_v2, jobs_access_from_whoami
16
 
17
  logger = logging.getLogger(__name__)
@@ -157,9 +159,8 @@ async def get_current_user(request: Request) -> dict[str, Any]:
157
  return DEV_USER
158
 
159
  # Try Authorization header
160
- auth_header = request.headers.get("Authorization", "")
161
- if auth_header.startswith("Bearer "):
162
- token = auth_header[7:]
163
  user = await _extract_user_from_token(token)
164
  if user:
165
  return user
@@ -183,9 +184,9 @@ def _extract_token(request: Request) -> str | None:
183
 
184
  Mirrors the lookup order used by ``get_current_user``.
185
  """
186
- auth_header = request.headers.get("Authorization", "")
187
- if auth_header.startswith("Bearer "):
188
- return auth_header[7:]
189
  return request.cookies.get("hf_access_token")
190
 
191
 
@@ -202,4 +203,3 @@ async def require_huggingface_org_member(request: Request) -> bool:
202
  if not token:
203
  return False
204
  return await check_org_membership(token, HF_EMPLOYEE_ORG)
205
-
 
12
  import httpx
13
  from fastapi import HTTPException, Request, status
14
 
15
+ from agent.core.hf_tokens import bearer_token_from_header
16
+
17
  from agent.core.hf_access import fetch_whoami_v2, jobs_access_from_whoami
18
 
19
  logger = logging.getLogger(__name__)
 
159
  return DEV_USER
160
 
161
  # Try Authorization header
162
+ token = bearer_token_from_header(request.headers.get("Authorization", ""))
163
+ if token:
 
164
  user = await _extract_user_from_token(token)
165
  if user:
166
  return user
 
184
 
185
  Mirrors the lookup order used by ``get_current_user``.
186
  """
187
+ token = bearer_token_from_header(request.headers.get("Authorization", ""))
188
+ if token:
189
+ return token
190
  return request.cookies.get("hf_access_token")
191
 
192
 
 
203
  if not token:
204
  return False
205
  return await check_org_membership(token, HF_EMPLOYEE_ORG)
 
backend/routes/agent.py CHANGED
@@ -33,6 +33,7 @@ from session_manager import MAX_SESSIONS, AgentSession, SessionCapacityError, se
33
  import user_quotas
34
 
35
  from agent.core.hf_access import get_jobs_access
 
36
  from agent.core.llm_params import _resolve_llm_params
37
 
38
  logger = logging.getLogger(__name__)
@@ -332,10 +333,8 @@ async def generate_title(
332
  reasoning model — reasoning_effort=low keeps the reasoning budget small
333
  so the 60-token output budget isn't consumed before the title is written.
334
  """
335
- api_key = (
336
- os.environ.get("INFERENCE_TOKEN")
337
- or (user.get("hf_token") if isinstance(user, dict) else None)
338
- or os.environ.get("HF_TOKEN")
339
  )
340
  try:
341
  response = await acompletion(
@@ -391,14 +390,7 @@ async def create_session(
391
  Returns 503 if the server or user has reached the session limit.
392
  """
393
  # Extract the user's HF token (Bearer header, HttpOnly cookie, or env var)
394
- hf_token = None
395
- auth_header = request.headers.get("Authorization", "")
396
- if auth_header.startswith("Bearer "):
397
- hf_token = auth_header[7:]
398
- if not hf_token:
399
- hf_token = request.cookies.get("hf_access_token")
400
- if not hf_token:
401
- hf_token = os.environ.get("HF_TOKEN")
402
 
403
  # Optional model override. Empty body falls back to the config default.
404
  model: str | None = None
@@ -444,14 +436,7 @@ async def restore_session_summary(
444
  if not isinstance(messages, list) or not messages:
445
  raise HTTPException(status_code=400, detail="Missing 'messages' array")
446
 
447
- hf_token = None
448
- auth_header = request.headers.get("Authorization", "")
449
- if auth_header.startswith("Bearer "):
450
- hf_token = auth_header[7:]
451
- if not hf_token:
452
- hf_token = request.cookies.get("hf_access_token")
453
- if not hf_token:
454
- hf_token = os.environ.get("HF_TOKEN")
455
 
456
  model = body.get("model")
457
  valid_ids = {m["id"] for m in AVAILABLE_MODELS}
@@ -545,14 +530,7 @@ async def get_user_quota(user: dict = Depends(get_current_user)) -> dict:
545
  @router.get("/user/jobs-access")
546
  async def get_jobs_access_info(request: Request, user: dict = Depends(get_current_user)) -> dict:
547
  """Return whether the current token can run HF Jobs and under which namespaces."""
548
- token = None
549
- auth_header = request.headers.get("Authorization", "")
550
- if auth_header.startswith("Bearer "):
551
- token = auth_header[7:]
552
- if not token:
553
- token = request.cookies.get("hf_access_token")
554
- if not token:
555
- token = os.environ.get("HF_TOKEN")
556
 
557
  access = await get_jobs_access(token or "")
558
  return {
 
33
  import user_quotas
34
 
35
  from agent.core.hf_access import get_jobs_access
36
+ from agent.core.hf_tokens import resolve_hf_request_token, resolve_hf_router_token
37
  from agent.core.llm_params import _resolve_llm_params
38
 
39
  logger = logging.getLogger(__name__)
 
333
  reasoning model — reasoning_effort=low keeps the reasoning budget small
334
  so the 60-token output budget isn't consumed before the title is written.
335
  """
336
+ api_key = resolve_hf_router_token(
337
+ user.get("hf_token") if isinstance(user, dict) else None
 
 
338
  )
339
  try:
340
  response = await acompletion(
 
390
  Returns 503 if the server or user has reached the session limit.
391
  """
392
  # Extract the user's HF token (Bearer header, HttpOnly cookie, or env var)
393
+ hf_token = resolve_hf_request_token(request)
 
 
 
 
 
 
 
394
 
395
  # Optional model override. Empty body falls back to the config default.
396
  model: str | None = None
 
436
  if not isinstance(messages, list) or not messages:
437
  raise HTTPException(status_code=400, detail="Missing 'messages' array")
438
 
439
+ hf_token = resolve_hf_request_token(request)
 
 
 
 
 
 
 
440
 
441
  model = body.get("model")
442
  valid_ids = {m["id"] for m in AVAILABLE_MODELS}
 
530
  @router.get("/user/jobs-access")
531
  async def get_jobs_access_info(request: Request, user: dict = Depends(get_current_user)) -> dict:
532
  """Return whether the current token can run HF Jobs and under which namespaces."""
533
+ token = resolve_hf_request_token(request)
 
 
 
 
 
 
 
534
 
535
  access = await get_jobs_access(token or "")
536
  return {
tests/unit/test_llm_params.py CHANGED
@@ -1,4 +1,9 @@
1
- from agent.core.llm_params import UnsupportedEffortError, _resolve_llm_params
 
 
 
 
 
2
 
3
 
4
  def test_openai_xhigh_effort_is_forwarded():
@@ -23,3 +28,80 @@ def test_openai_max_effort_is_still_rejected():
23
  assert "OpenAI doesn't accept effort='max'" in str(exc)
24
  else:
25
  raise AssertionError("Expected UnsupportedEffortError for max effort")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from agent.core.hf_tokens import resolve_hf_request_token
2
+ from agent.core.llm_params import (
3
+ UnsupportedEffortError,
4
+ _resolve_hf_router_token,
5
+ _resolve_llm_params,
6
+ )
7
 
8
 
9
  def test_openai_xhigh_effort_is_forwarded():
 
28
  assert "OpenAI doesn't accept effort='max'" in str(exc)
29
  else:
30
  raise AssertionError("Expected UnsupportedEffortError for max effort")
31
+
32
+
33
+ def test_hf_router_token_prefers_inference_token(monkeypatch):
34
+ monkeypatch.setenv("INFERENCE_TOKEN", " inference-token ")
35
+ monkeypatch.setenv("HF_TOKEN", "hf-token")
36
+
37
+ assert _resolve_hf_router_token("session-token") == "inference-token"
38
+
39
+
40
+ def test_hf_router_token_prefers_session_over_hf_cache(monkeypatch):
41
+ monkeypatch.delenv("INFERENCE_TOKEN", raising=False)
42
+ monkeypatch.setenv("HF_TOKEN", "hf-token")
43
+
44
+ assert _resolve_hf_router_token(" session-token ") == "session-token"
45
+
46
+
47
+ def test_hf_router_token_uses_hf_token_env_via_huggingface_hub(monkeypatch):
48
+ monkeypatch.delenv("INFERENCE_TOKEN", raising=False)
49
+ monkeypatch.setenv("HF_TOKEN", " hf-token ")
50
+
51
+ assert _resolve_hf_router_token(None) == "hf-token"
52
+
53
+
54
+ def test_hf_router_token_uses_huggingface_hub_cache(monkeypatch):
55
+ import huggingface_hub
56
+
57
+ monkeypatch.delenv("INFERENCE_TOKEN", raising=False)
58
+ monkeypatch.delenv("HF_TOKEN", raising=False)
59
+ monkeypatch.setattr(huggingface_hub, "get_token", lambda: "cached-token")
60
+
61
+ assert _resolve_hf_router_token(None) == "cached-token"
62
+
63
+
64
+ def test_hf_router_token_swallows_huggingface_hub_errors(monkeypatch):
65
+ import huggingface_hub
66
+
67
+ def fail():
68
+ raise RuntimeError("cache unavailable")
69
+
70
+ monkeypatch.delenv("INFERENCE_TOKEN", raising=False)
71
+ monkeypatch.delenv("HF_TOKEN", raising=False)
72
+ monkeypatch.setattr(huggingface_hub, "get_token", fail)
73
+
74
+ assert _resolve_hf_router_token(None) is None
75
+
76
+
77
+ def test_hf_router_params_set_bill_to_only_for_inference_token(monkeypatch):
78
+ monkeypatch.setenv("INFERENCE_TOKEN", "inference-token")
79
+ monkeypatch.setenv("HF_BILL_TO", "test-org")
80
+
81
+ params = _resolve_llm_params("moonshotai/Kimi-K2.6")
82
+
83
+ assert params["api_key"] == "inference-token"
84
+ assert params["extra_headers"] == {"X-HF-Bill-To": "test-org"}
85
+
86
+
87
+ def test_hf_request_token_keeps_browser_user_precedence(monkeypatch):
88
+ class Request:
89
+ headers = {"Authorization": "Bearer browser-token"}
90
+ cookies = {"hf_access_token": "cookie-token"}
91
+
92
+ monkeypatch.setenv("HF_TOKEN", "server-token")
93
+
94
+ assert resolve_hf_request_token(Request()) == "browser-token"
95
+
96
+
97
+ def test_hf_request_token_does_not_use_cached_login(monkeypatch):
98
+ import huggingface_hub
99
+
100
+ class Request:
101
+ headers = {}
102
+ cookies = {}
103
+
104
+ monkeypatch.delenv("HF_TOKEN", raising=False)
105
+ monkeypatch.setattr(huggingface_hub, "get_token", lambda: "cached-token")
106
+
107
+ assert resolve_hf_request_token(Request()) is None