From 25b7fa6db787326bdffc50b21554bd235d95d53b Mon Sep 17 00:00:00 2001 From: akseljoonas Date: Thu, 23 Apr 2026 13:16:30 +0300 Subject: [PATCH] refactor(opus-gate): rename to OPUS_ACCESS_ORG, default to ml-agent-explorers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Opus staff gate was named after HF employees (HF_EMPLOYEE_ORG, require_huggingface_org_member), which read as "only HF staff can use Opus". The actual intent is just "members of a specific HF Hub org" — and the org we want is ml-agent-explorers, the same join-org the WelcomeScreen already promotes for free HF inference credits. - HF_EMPLOYEE_ORG -> OPUS_ACCESS_ORG, default ml-agent-explorers - require_huggingface_org_member -> require_opus_access_org_member - _require_hf_for_anthropic -> _require_opus_org_for_anthropic - 403 copy now names the actual org and points at its Hub page Operators: set OPUS_ACCESS_ORG=huggingface on the Space if you want to keep the old staff-only behavior. Default unlocks Opus for anyone who joins ml-agent-explorers via the existing WelcomeScreen tile. --- backend/dependencies.py | 19 +++++++++++++------ backend/routes/agent.py | 32 ++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/backend/dependencies.py b/backend/dependencies.py index 97a4e286..41b49055 100644 --- a/backend/dependencies.py +++ b/backend/dependencies.py @@ -16,7 +16,12 @@ OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co") AUTH_ENABLED = bool(os.environ.get("OAUTH_CLIENT_ID", "")) -HF_EMPLOYEE_ORG = os.environ.get("HF_EMPLOYEE_ORG", "huggingface") +# The HF org whose members are allowed to select Opus. Default is +# ``ml-agent-explorers`` — the same join-org promoted on the WelcomeScreen +# that grants free HF inference credits. "HF org" here means any org on the +# Hub, not HF employees specifically; override with OPUS_ACCESS_ORG if you +# want to restrict further (e.g. to ``huggingface`` for staff-only). +OPUS_ACCESS_ORG = os.environ.get("OPUS_ACCESS_ORG", "ml-agent-explorers") # Simple in-memory token cache: token -> (user_info, expiry_time) _token_cache: dict[str, tuple[dict[str, Any], float]] = {} @@ -232,18 +237,20 @@ def _extract_token(request: Request) -> str | None: return request.cookies.get("hf_access_token") -async def require_huggingface_org_member(request: Request) -> bool: - """Return True if the caller is a member of the ``huggingface`` org. +async def require_opus_access_org_member(request: Request) -> bool: + """Return True if the caller is a member of the configured Opus-access org. Used to gate endpoints that can push a session onto an Anthropic model - billed to the Space's ``ANTHROPIC_API_KEY``. Returns True unconditionally - in dev mode so local testing isn't blocked. + billed to the Space's ``ANTHROPIC_API_KEY``. Defaults to + ``ml-agent-explorers`` (the join-org promoted on the WelcomeScreen); + set ``OPUS_ACCESS_ORG`` to lock it down further. Returns True + unconditionally in dev mode so local testing isn't blocked. """ if not AUTH_ENABLED: return True token = _extract_token(request) if not token: return False - return await check_org_membership(token, HF_EMPLOYEE_ORG) + return await check_org_membership(token, OPUS_ACCESS_ORG) diff --git a/backend/routes/agent.py b/backend/routes/agent.py index d8b3d775..cf237b3f 100644 --- a/backend/routes/agent.py +++ b/backend/routes/agent.py @@ -10,7 +10,7 @@ import os from typing import Any -from dependencies import get_current_user, require_huggingface_org_member +from dependencies import get_current_user, require_opus_access_org_member from fastapi import ( APIRouter, Depends, @@ -68,26 +68,28 @@ ] -async def _require_hf_for_anthropic(request: Request, model_id: str) -> None: - """403 if a non-``huggingface``-org user tries to select an Anthropic model. +async def _require_opus_org_for_anthropic(request: Request, model_id: str) -> None: + """403 if the caller isn't in the Opus-access org (default + ``ml-agent-explorers``) but tries to select an Anthropic model. Anthropic models are billed to the Space's ``ANTHROPIC_API_KEY``; every other model in ``AVAILABLE_MODELS`` is routed through HF Router and billed via ``X-HF-Bill-To``. The gate only fires for ``anthropic/*`` so - non-HF users can still freely switch between the free models. + non-members can still freely switch between the free models. Pattern: https://github.com/huggingface/ml-intern/pull/63 """ if not model_id.startswith("anthropic/"): return - if not await require_huggingface_org_member(request): + if not await require_opus_access_org_member(request): raise HTTPException( status_code=403, detail={ "error": "anthropic_restricted", "message": ( - "Opus is gated to HF staff. Pick a free model — " - "Kimi K2.6, MiniMax M2.7, or GLM 5.1 — instead." + "Opus is gated to ml-agent-explorers members. Join the " + "org on huggingface.co/ml-agent-explorers, or pick a " + "free model — Kimi K2.6, MiniMax M2.7, or GLM 5.1." ), }, ) @@ -309,10 +311,11 @@ async def create_session( if model and model not in valid_ids: raise HTTPException(status_code=400, detail=f"Unknown model: {model}") - # Opus is gated to HF staff (PR #63). Only fires when the resolved model - # is Anthropic; free models pass through. + # Opus is gated to ml-agent-explorers members (see OPUS_ACCESS_ORG in + # dependencies.py). Only fires when the resolved model is Anthropic; + # free models pass through. resolved_model = model or session_manager.config.model_name - await _require_hf_for_anthropic(request, resolved_model) + await _require_opus_org_for_anthropic(request, resolved_model) try: session_id = await session_manager.create_session( @@ -355,7 +358,7 @@ async def restore_session_summary( raise HTTPException(status_code=400, detail=f"Unknown model: {model}") resolved_model = model or session_manager.config.model_name - await _require_hf_for_anthropic(request, resolved_model) + await _require_opus_org_for_anthropic(request, resolved_model) try: session_id = await session_manager.create_session( @@ -402,8 +405,9 @@ async def set_session_model( (including other browser tabs) are unaffected. Model switches don't charge quota — the Claude-quota gate only fires at message-submit time. - Switching TO an Anthropic model requires HF org membership (PR #63); - free-model switches are unrestricted. + Switching TO an Anthropic model requires OPUS_ACCESS_ORG membership + (default ml-agent-explorers, pattern from PR #63); free-model switches + are unrestricted. """ _check_session_access(session_id, user) model_id = body.get("model") @@ -412,7 +416,7 @@ async def set_session_model( valid_ids = {m["id"] for m in AVAILABLE_MODELS} if model_id not in valid_ids: raise HTTPException(status_code=400, detail=f"Unknown model: {model_id}") - await _require_hf_for_anthropic(request, model_id) + await _require_opus_org_for_anthropic(request, model_id) agent_session = session_manager.sessions.get(session_id) if not agent_session: raise HTTPException(status_code=404, detail="Session not found")