Forward signed-in HF identity to allocator
Browse files
README.md
CHANGED
|
@@ -97,7 +97,11 @@ Three modes, picked by env (`/api/config` tells the client which one is active):
|
|
| 97 |
- **`LOAD_BALANCER_URL` env** — the original flow: the browser POSTs the
|
| 98 |
same-origin `/api/session` proxy, the server forwards to the LB, and the
|
| 99 |
browser dials the per-session compute URL the LB hands back. The LB address
|
| 100 |
-
never reaches the browser; the Settings URL field is hidden.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
- **Neither** — **Settings → Speech-to-speech server URL**: paste a full
|
| 102 |
`connect_url` (`wss://host/v1/realtime?...`) or a bare host like `localhost:8080`
|
| 103 |
(the app adds `/v1/realtime`), and the browser connects to it directly.
|
|
|
|
| 97 |
- **`LOAD_BALANCER_URL` env** — the original flow: the browser POSTs the
|
| 98 |
same-origin `/api/session` proxy, the server forwards to the LB, and the
|
| 99 |
browser dials the per-session compute URL the LB hands back. The LB address
|
| 100 |
+
never reaches the browser; the Settings URL field is hidden. When a visitor
|
| 101 |
+
signs in with Hugging Face, the proxy also forwards their OAuth access token
|
| 102 |
+
to the LB through `X-Reachy-Mini-Authorization`, matching the Reachy Mini
|
| 103 |
+
client. Anonymous requests carry no token, and the token is never returned to
|
| 104 |
+
browser JavaScript.
|
| 105 |
- **Neither** — **Settings → Speech-to-speech server URL**: paste a full
|
| 106 |
`connect_url` (`wss://host/v1/realtime?...`) or a bare host like `localhost:8080`
|
| 107 |
(the app adds `/v1/realtime`), and the browser connects to it directly.
|
auth.py
CHANGED
|
@@ -106,6 +106,19 @@ def current_user(request):
|
|
| 106 |
return _field(current_oauth(request), "user_info")
|
| 107 |
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
def _user_org_names(user) -> "set[str]":
|
| 110 |
"""The user's organisations from the OAuth userinfo, by username/name/id."""
|
| 111 |
names = set()
|
|
|
|
| 106 |
return _field(current_oauth(request), "user_info")
|
| 107 |
|
| 108 |
|
| 109 |
+
def current_access_token(request) -> "str | None":
|
| 110 |
+
"""The signed-in user's HF OAuth access token, or None.
|
| 111 |
+
|
| 112 |
+
Keep this server-side: it is used to attribute load-balancer session
|
| 113 |
+
requests to the signed-in HF account and must never be returned to the
|
| 114 |
+
browser.
|
| 115 |
+
"""
|
| 116 |
+
token = _field(current_oauth(request), "access_token")
|
| 117 |
+
if token is None:
|
| 118 |
+
return None
|
| 119 |
+
return str(token).strip() or None
|
| 120 |
+
|
| 121 |
+
|
| 122 |
def _user_org_names(user) -> "set[str]":
|
| 123 |
"""The user's organisations from the OAuth userinfo, by username/name/id."""
|
| 124 |
names = set()
|
server.py
CHANGED
|
@@ -81,6 +81,7 @@ SERPER_URL = "https://google.serper.dev/search"
|
|
| 81 |
# Cap results so the tool output stays small enough to feed back to the model.
|
| 82 |
MAX_RESULTS = 5
|
| 83 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
| 84 |
|
| 85 |
app = FastAPI(title="s2s-demo")
|
| 86 |
|
|
@@ -250,7 +251,11 @@ async def session(request: Request):
|
|
| 250 |
url = f"{LOAD_BALANCER_URL.rstrip('/')}/session"
|
| 251 |
try:
|
| 252 |
async with httpx.AsyncClient(timeout=15.0) as http:
|
| 253 |
-
lb = await http.post(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
except httpx.RequestError as exc:
|
| 255 |
logger.warning("Load balancer unreachable: %r", exc)
|
| 256 |
raise HTTPException(status_code=502, detail="Speech service unreachable.")
|
|
@@ -286,6 +291,24 @@ async def session(request: Request):
|
|
| 286 |
return await _finalize_grant(data, keys, tier, tracked, set_cookie)
|
| 287 |
|
| 288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
@app.get("/api/queue/{queue_id}")
|
| 290 |
async def queue_status(queue_id: str, request: Request):
|
| 291 |
"""Poll a waiting ticket: relay the position, or — when the head of the line
|
|
|
|
| 81 |
# Cap results so the tool output stays small enough to feed back to the model.
|
| 82 |
MAX_RESULTS = 5
|
| 83 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 84 |
+
LB_USER_AGENT = "hf-realtime-voice-space"
|
| 85 |
|
| 86 |
app = FastAPI(title="s2s-demo")
|
| 87 |
|
|
|
|
| 251 |
url = f"{LOAD_BALANCER_URL.rstrip('/')}/session"
|
| 252 |
try:
|
| 253 |
async with httpx.AsyncClient(timeout=15.0) as http:
|
| 254 |
+
lb = await http.post(
|
| 255 |
+
url,
|
| 256 |
+
headers=_load_balancer_headers(request),
|
| 257 |
+
content="{}",
|
| 258 |
+
)
|
| 259 |
except httpx.RequestError as exc:
|
| 260 |
logger.warning("Load balancer unreachable: %r", exc)
|
| 261 |
raise HTTPException(status_code=502, detail="Speech service unreachable.")
|
|
|
|
| 291 |
return await _finalize_grant(data, keys, tier, tracked, set_cookie)
|
| 292 |
|
| 293 |
|
| 294 |
+
def _load_balancer_headers(request: Request) -> dict[str, str]:
|
| 295 |
+
"""Headers for the server-to-server session allocation request.
|
| 296 |
+
|
| 297 |
+
Reachy Mini uses this dedicated header for an optional HF user token. The
|
| 298 |
+
load balancer fingerprints it immediately and resolves the account through
|
| 299 |
+
whoami asynchronously, so allocation remains fast. Anonymous visitors send
|
| 300 |
+
no credential header.
|
| 301 |
+
"""
|
| 302 |
+
headers = {
|
| 303 |
+
"Content-Type": "application/json",
|
| 304 |
+
"User-Agent": LB_USER_AGENT,
|
| 305 |
+
}
|
| 306 |
+
token = auth.current_access_token(request)
|
| 307 |
+
if token:
|
| 308 |
+
headers["X-Reachy-Mini-Authorization"] = f"Bearer {token}"
|
| 309 |
+
return headers
|
| 310 |
+
|
| 311 |
+
|
| 312 |
@app.get("/api/queue/{queue_id}")
|
| 313 |
async def queue_status(queue_id: str, request: Request):
|
| 314 |
"""Poll a waiting ticket: relay the position, or — when the head of the line
|