@@ -171,6 +171,63 @@ async def _background_health_check(state: ProxyState, client: httpx.AsyncClient)
171171 pass
172172
173173
174+ # ── Upstream (NVIDIA) probe ────────────────────────────────────────────
175+ # The dashboard needs one unambiguous signal that answers "is it us or is it
176+ # NVIDIA?" A cheap GET /v1/models on one healthy key — short timeout, no RPM
177+ # budget spent on chat — is enough to tell the two apart. Results land in
178+ # state.upstream so /api/status and the web UI can render them.
179+
180+ _UPSTREAM_PROBE_INTERVAL = 30.0
181+ _UPSTREAM_PROBE_TIMEOUT = 8.0
182+ _UPSTREAM_SLOW_MS = 1500.0
183+
184+
185+ async def _probe_upstream (state : ProxyState , client : httpx .AsyncClient ) -> None :
186+ """One reachability probe of the NVIDIA endpoint."""
187+ keys = [k for k in state .keys if state .is_key_healthy (k ) and state .key_can_send_rpm (k )]
188+ if not keys :
189+ state .set_upstream_status ("unknown" , 0.0 , "no healthy key to probe with" )
190+ return
191+ key = keys [0 ]
192+ headers = {"Authorization" : f"Bearer { key } " , "User-Agent" : "openvidia/2.0" }
193+ t0 = time .monotonic ()
194+ try :
195+ resp = await client .request (
196+ "GET" ,
197+ UPSTREAM_BASE + "models" ,
198+ headers = headers ,
199+ timeout = httpx .Timeout (_UPSTREAM_PROBE_TIMEOUT ),
200+ )
201+ await resp .aclose ()
202+ latency = (time .monotonic () - t0 ) * 1000
203+ if resp .is_success :
204+ status = "slow" if latency > _UPSTREAM_SLOW_MS else "ok"
205+ detail = "" if status == "ok" else f"responded in { latency :.0f} ms"
206+ state .set_upstream_status (status , latency , detail )
207+ else :
208+ state .set_upstream_status (
209+ "down" ,
210+ latency ,
211+ f"upstream answered { resp .status_code } " ,
212+ )
213+ except httpx .TimeoutException :
214+ state .set_upstream_status (
215+ "down" , (time .monotonic () - t0 ) * 1000 , "upstream did not answer within 8s"
216+ )
217+ except httpx .HTTPError as e :
218+ state .set_upstream_status ("down" , (time .monotonic () - t0 ) * 1000 , f"network error: { e } " )
219+
220+
221+ async def _background_upstream_probe (state : ProxyState , client : httpx .AsyncClient ) -> None :
222+ """Every 30s, measure whether NVIDIA itself is reachable."""
223+ try :
224+ while True :
225+ await _probe_upstream (state , client )
226+ await asyncio .sleep (_UPSTREAM_PROBE_INTERVAL )
227+ except asyncio .CancelledError :
228+ pass
229+
230+
174231class BodyLimitMiddleware (BaseHTTPMiddleware ):
175232 async def dispatch (self , request : Request , call_next ):
176233 cl = request .headers .get ("content-length" )
@@ -279,12 +336,15 @@ async def _pre_warm():
279336
280337 asyncio .create_task (_pre_warm ())
281338 state .health_task = asyncio .create_task (_background_health_check (state , client ))
339+ state .upstream_task = asyncio .create_task (_background_upstream_probe (state , client ))
282340 if rs is not None :
283341 rs .on_remote = state .apply_remote_event
284342 await rs .start ()
285343 yield
286344 if state .health_task is not None :
287345 state .health_task .cancel ()
346+ if state .upstream_task is not None :
347+ state .upstream_task .cancel ()
288348 if rs is not None :
289349 await rs .close ()
290350 await client .aclose ()
@@ -366,6 +426,15 @@ async def models_handler():
366426 m ["slug" ] = m .get ("id" , "" )
367427 m ["display_name" ] = m .get ("id" , "" )
368428 data ["models" ] = models
429+ # Arricchisci ogni modello con la salute appresa dal
430+ # traffico reale (ModelHealth): la dashboard la usa per
431+ # marcare i modelli con circuito aperto o latenza alta
432+ # senza dover fare un secondo giro di API.
433+ for m in list (data .get ("data" ) or []) + list (data .get ("models" ) or []):
434+ mid = m .get ("id" , "" )
435+ h = state .model_health .get (mid )
436+ if h is not None :
437+ m ["health" ] = h .as_dict ()
369438 # Inietta l'alias "openvidia" in cima a entrambe le liste
370439 # così i picker dei CLI (Codex, opencode) lo mostrano come
371440 # opzione selezionabile. Il proxy lo risolve a runtime nel
0 commit comments