elmarcito commited on
Commit
2046de7
Β·
verified Β·
1 Parent(s): 96c47c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +242 -48
app.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
- πŸš€ NVIDIA AI Multi-Model Space
3
- - 38 modelli NVIDIA NIM VERIFICATI e FUNZIONANTI (test 2026-07-04)
 
4
  - Memoria breve (RAM) e lunga (SQLite)
5
  - API REST GET/POST + SSE Streaming
6
  - Server MCP integrato
@@ -52,7 +53,6 @@ HTTP_TIMEOUT = httpx.Timeout(120.0, connect=10.0)
52
 
53
  # ══════════════════════════════════════════════════════════════
54
  # CATALOGO β€” SOLO MODELLI VERIFICATI FUNZIONANTI (test 2026-07-04)
55
- # 38 modelli confermati OK β€” ordinati per velocitΓ 
56
  # ══════════════════════════════════════════════════════════════
57
 
58
  NVIDIA_MODELS = {
@@ -87,7 +87,7 @@ NVIDIA_MODELS = {
87
  "name": "⚑ Gemma 2 2B",
88
  "category": "LLM",
89
  "context": 8192,
90
- "description": "Compact Gemma β€” 212ms"
91
  },
92
  "nvidia/llama-3.1-nemotron-nano-8b-v1": {
93
  "name": "⚑ Nemotron Nano 8B v1",
@@ -151,7 +151,7 @@ NVIDIA_MODELS = {
151
  "name": "πŸš€ Gemma 3n E2B",
152
  "category": "LLM",
153
  "context": 8192,
154
- "description": "Gemma 3 Nano β€” 380ms"
155
  },
156
  "mistralai/mistral-nemotron": {
157
  "name": "πŸš€ Mistral Nemotron",
@@ -233,7 +233,7 @@ NVIDIA_MODELS = {
233
  "name": "πŸ’ͺ DiffusionGemma 26B",
234
  "category": "LLM",
235
  "context": 131072,
236
- "description": "Diffusion-based Gemma β€” 843ms"
237
  },
238
 
239
  # ═══════════════════════════════════════════════════════════
@@ -305,6 +305,67 @@ NVIDIA_MODELS = {
305
  },
306
  }
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  # ══════════════════════════════════════════════════════════════
309
  # MEMORIA
310
  # ══════════════════════════════════════════════════════════════
@@ -314,10 +375,26 @@ memory = MemoryManager(
314
  max_long_tokens=int(os.getenv("MAX_LONG_MEMORY_TOKENS", 50000))
315
  )
316
 
 
317
  # ═════════════════════════════════════════════��════════════════
318
- # CLIENT NVIDIA
319
  # ══════════════════════════════════════════════════════════════
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  async def nvidia_chat_stream(
322
  messages: list[dict],
323
  model: str = DEFAULT_MODEL,
@@ -328,6 +405,10 @@ async def nvidia_chat_stream(
328
  if not model or not str(model).strip():
329
  model = DEFAULT_MODEL
330
 
 
 
 
 
331
  headers = {
332
  "Authorization": f"Bearer {NVIDIA_API_KEY}",
333
  "Content-Type": "application/json",
@@ -348,12 +429,43 @@ async def nvidia_chat_stream(
348
  f"{NVIDIA_BASE_URL}/chat/completions",
349
  headers=headers, json=payload,
350
  ) as response:
 
 
351
  if response.status_code != 200:
352
  err = (await response.aread()).decode(errors="ignore")[:500]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  if response.status_code == 404:
354
- raise Exception(f"Modello '{model}' non disponibile")
355
  raise Exception(f"NVIDIA API {response.status_code}: {err}")
356
 
 
357
  async for line in response.aiter_lines():
358
  if line.startswith("data: "):
359
  data = line[6:]
@@ -379,36 +491,58 @@ async def nvidia_chat(
379
  if not model or not str(model).strip():
380
  model = DEFAULT_MODEL
381
 
 
 
 
 
382
  headers = {
383
  "Authorization": f"Bearer {NVIDIA_API_KEY}",
384
  "Content-Type": "application/json",
385
  }
386
- payload = {
387
- "model": model,
388
- "messages": messages,
389
- "temperature": temperature,
390
- "max_tokens": max_tokens,
391
- "top_p": top_p,
392
- "stream": False,
393
- }
394
 
395
- if client:
396
- resp = await client.post(
397
  f"{NVIDIA_BASE_URL}/chat/completions",
398
- headers=headers, json=payload,
 
 
 
 
 
 
 
 
399
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  else:
401
  async with httpx.AsyncClient(timeout=HTTP_TIMEOUT, limits=HTTP_LIMITS) as c:
402
- resp = await c.post(
403
- f"{NVIDIA_BASE_URL}/chat/completions",
404
- headers=headers, json=payload,
405
- )
406
-
407
- if resp.status_code != 200:
408
- if resp.status_code == 404:
409
- raise Exception(f"Modello '{model}' non disponibile")
410
- raise Exception(f"NVIDIA API {resp.status_code}: {resp.text[:500]}")
411
- return resp.json()["choices"][0]["message"]["content"]
412
 
413
 
414
  # ══════════════════════════════════════════════════════════════
@@ -418,7 +552,7 @@ async def nvidia_chat(
418
  app = FastAPI(
419
  title="πŸš€ NVIDIA AI Multi-Model API",
420
  description=f"{len(NVIDIA_MODELS)} modelli verificati e funzionanti",
421
- version="3.0.0",
422
  )
423
 
424
  app.add_middleware(
@@ -484,9 +618,10 @@ async def root():
484
  async def info():
485
  return {
486
  "service": "πŸš€ NVIDIA AI Multi-Model",
487
- "version": "3.0.0",
488
  "default_model": DEFAULT_MODEL,
489
  "models_available": len(NVIDIA_MODELS),
 
490
  "all_models_verified": True,
491
  "last_verified": "2026-07-04",
492
  }
@@ -519,6 +654,7 @@ async def list_models(category: Optional[str] = None):
519
  "total": len(models),
520
  "categories": sorted(set(m["category"] for m in NVIDIA_MODELS.values())),
521
  "default": DEFAULT_MODEL,
 
522
  }
523
 
524
 
@@ -526,7 +662,11 @@ async def list_models(category: Optional[str] = None):
526
  async def get_model_info(model_id: str):
527
  if model_id not in NVIDIA_MODELS:
528
  raise HTTPException(404, f"Modello '{model_id}' non nel catalogo")
529
- return {"model_id": model_id, **NVIDIA_MODELS[model_id]}
 
 
 
 
530
 
531
 
532
  # ══════════════════════════════════════════════════════════════
@@ -535,10 +675,14 @@ async def get_model_info(model_id: str):
535
 
536
  @app.get("/v1/debug/test-model")
537
  async def debug_test_model(model: str = Query(DEFAULT_MODEL)):
 
538
  try:
539
  start = time.time()
540
  response = await nvidia_chat(
541
- messages=[{"role": "user", "content": "Say 'ok'."}],
 
 
 
542
  model=model, max_tokens=10,
543
  )
544
  return {
@@ -546,35 +690,51 @@ async def debug_test_model(model: str = Query(DEFAULT_MODEL)):
546
  "model": model,
547
  "response": response,
548
  "elapsed_ms": round((time.time() - start) * 1000),
 
549
  }
550
  except Exception as e:
551
  return {"status": "❌ ERROR", "model": model, "error": str(e)}
552
 
553
 
 
 
 
 
 
 
 
 
 
 
554
  async def _test_single(mid: str, client: httpx.AsyncClient, sem: asyncio.Semaphore) -> dict:
555
  async with sem:
556
  start = time.time()
557
  try:
 
558
  resp = await nvidia_chat(
559
- messages=[{"role": "user", "content": "hi"}],
560
- model=mid, max_tokens=3, client=client,
 
 
 
561
  )
562
  return {
563
  "model": mid, "status": "βœ…",
564
  "elapsed_ms": round((time.time() - start) * 1000),
565
  "preview": (resp or "")[:30],
 
566
  }
567
  except Exception as e:
568
  return {
569
  "model": mid, "status": "❌",
570
  "elapsed_ms": round((time.time() - start) * 1000),
571
- "error": str(e)[:120],
572
  }
573
 
574
 
575
  @app.get("/v1/debug/test-all-parallel")
576
  async def debug_test_all_parallel(concurrency: int = Query(15, ge=1, le=50)):
577
- """⚑ Test parallelo async di tutti i modelli"""
578
  start = time.time()
579
  testable = list(NVIDIA_MODELS.keys())
580
  sem = asyncio.Semaphore(concurrency)
@@ -590,6 +750,7 @@ async def debug_test_all_parallel(concurrency: int = Query(15, ge=1, le=50)):
590
  "working": len(working),
591
  "broken": len(broken),
592
  "total_time_seconds": round(time.time() - start, 2),
 
593
  },
594
  "working_models_by_speed": working,
595
  "broken_models": broken,
@@ -690,7 +851,8 @@ async def _stream_response(req: ChatRequest, session_id: str):
690
  full = []
691
  try:
692
  yield {"event": "start", "data": json.dumps({
693
- "session_id": session_id, "model": req.model, "timestamp": time.time()
 
694
  })}
695
  async for chunk in nvidia_chat_stream(
696
  messages=messages, model=req.model,
@@ -768,7 +930,7 @@ async def delete_session(session_id: str):
768
  MCP_TOOLS = [
769
  {
770
  "name": "nvidia_chat",
771
- "description": "Chat with any verified NVIDIA NIM model with memory",
772
  "inputSchema": {
773
  "type": "object",
774
  "properties": {
@@ -851,7 +1013,7 @@ async def mcp_endpoint(request: Request):
851
  if method == "initialize":
852
  return _ok({
853
  "protocolVersion": "2024-11-05",
854
- "serverInfo": {"name": "nvidia-ai", "version": "3.0.0"},
855
  "capabilities": {
856
  "tools": {"listChanged": False},
857
  "resources": {"subscribe": False, "listChanged": False}
@@ -973,8 +1135,8 @@ code{{color:#7ec8e3}} table{{border-collapse:collapse;width:100%}}
973
  th,td{{padding:8px;border:1px solid #333;text-align:left}}
974
  th{{background:#16213e;color:#76b900}}
975
  </style></head><body>
976
- <h1>πŸš€ NVIDIA AI Multi-Model API v3.0</h1>
977
- <p><b>{len(NVIDIA_MODELS)} modelli VERIFICATI</b> e funzionanti (test 2026-07-04)</p>
978
 
979
  <h2>πŸ’¬ Chat POST</h2>
980
  <pre>curl -X POST /v1/chat -H "Content-Type: application/json" \\
@@ -986,6 +1148,9 @@ th{{background:#16213e;color:#76b900}}
986
  <h2>⚑ Test Parallelo</h2>
987
  <pre>curl "/v1/debug/test-all-parallel?concurrency=15"</pre>
988
 
 
 
 
989
  <h2>πŸ”Œ MCP</h2>
990
  <pre>curl -X POST /v1/mcp -H "Content-Type: application/json" \\
991
  -d '{{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{{}}}}'</pre>
@@ -1057,12 +1222,12 @@ with gr.Blocks(
1057
  css=".gradio-container{max-width:1400px !important;} footer{display:none !important;}"
1058
  ) as demo:
1059
  gr.Markdown(f"""
1060
- # πŸš€ NVIDIA AI Multi-Model
1061
- **{len(NVIDIA_MODELS)} modelli VERIFICATI** e funzionanti β€’ Memoria β€’ SSE β€’ MCP β€’ Async
1062
 
1063
  🏠 [UI](/) | πŸ“– [Docs](/api-docs) | πŸ”§ [Swagger](/docs) | πŸ”Œ [MCP](/v1/mcp)
1064
 
1065
- **Default:** `{DEFAULT_MODEL}` (⚑ fastest at 153ms)
1066
  """)
1067
 
1068
  with gr.Tab("πŸ’¬ Chat"):
@@ -1093,12 +1258,16 @@ with gr.Blocks(
1093
 
1094
  with gr.Tab("πŸ€– Modelli"):
1095
  gr.Markdown(f"### οΏ½οΏ½οΏ½οΏ½ Catalogo β€” {len(NVIDIA_MODELS)} modelli verificati")
1096
- data = [[mi["category"], mi["name"], mid, mi["context"], mi["description"]]
 
1097
  for mid, mi in NVIDIA_MODELS.items()]
1098
- gr.Dataframe(headers=["Cat", "Nome", "ID", "Context", "Descrizione"], value=data, interactive=False, wrap=True)
 
 
 
1099
 
1100
  with gr.Tab("πŸš€ Test Parallelo"):
1101
- gr.Markdown("### ⚑ Verifica live tutti i modelli in parallelo")
1102
  test_btn = gr.Button("πŸš€ Testa tutti i modelli", variant="primary")
1103
  test_output = gr.JSON(label="Risultati")
1104
 
@@ -1116,6 +1285,7 @@ with gr.Blocks(
1116
  "βœ… funzionanti": len(working),
1117
  "❌ non_funzionanti": len(broken),
1118
  "totale": len(results),
 
1119
  "working_by_speed": working,
1120
  "broken": broken,
1121
  }
@@ -1131,6 +1301,29 @@ with gr.Blocks(
1131
  stats_btn.click(get_memory_info, inputs=[mem_session], outputs=[mem_display])
1132
  clear_mem_btn.click(clear_session_memory, inputs=[mem_session], outputs=[mem_display])
1133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1134
  with gr.Tab("πŸ“– Docs"):
1135
  gr.Markdown("""
1136
  ## πŸ“– Documentazione
@@ -1145,6 +1338,7 @@ with gr.Blocks(
1145
  - `GET /v1/chat/stream` β€” SSE streaming
1146
  - `POST /v1/mcp` β€” MCP JSON-RPC
1147
  - `GET /v1/debug/test-all-parallel` β€” Test parallelo
 
1148
  """)
1149
 
1150
 
 
1
  """
2
+ πŸš€ NVIDIA AI Multi-Model Space v3.1
3
+ - 38 modelli NVIDIA NIM VERIFICATI e FUNZIONANTI
4
+ - Auto-adattamento messaggi per modelli senza 'system role'
5
  - Memoria breve (RAM) e lunga (SQLite)
6
  - API REST GET/POST + SSE Streaming
7
  - Server MCP integrato
 
53
 
54
  # ══════════════════════════════════════════════════════════════
55
  # CATALOGO β€” SOLO MODELLI VERIFICATI FUNZIONANTI (test 2026-07-04)
 
56
  # ══════════════════════════════════════════════════════════════
57
 
58
  NVIDIA_MODELS = {
 
87
  "name": "⚑ Gemma 2 2B",
88
  "category": "LLM",
89
  "context": 8192,
90
+ "description": "Compact Gemma β€” 212ms (no system role)"
91
  },
92
  "nvidia/llama-3.1-nemotron-nano-8b-v1": {
93
  "name": "⚑ Nemotron Nano 8B v1",
 
151
  "name": "πŸš€ Gemma 3n E2B",
152
  "category": "LLM",
153
  "context": 8192,
154
+ "description": "Gemma 3 Nano β€” 380ms (no system role)"
155
  },
156
  "mistralai/mistral-nemotron": {
157
  "name": "πŸš€ Mistral Nemotron",
 
233
  "name": "πŸ’ͺ DiffusionGemma 26B",
234
  "category": "LLM",
235
  "context": 131072,
236
+ "description": "Diffusion-based Gemma β€” 843ms (no system role)"
237
  },
238
 
239
  # ═══════════════════════════════════════════════════════════
 
305
  },
306
  }
307
 
308
+ # ══════════════════════════════════════════════════════════════
309
+ # MODELLI CHE NON SUPPORTANO IL ROLE "system"
310
+ # (verranno auto-convertiti: system β†’ prefisso del primo user msg)
311
+ # ══════════════════════════════════════════════════════════════
312
+
313
+ MODELS_NO_SYSTEM_ROLE = {
314
+ # Gemma family
315
+ "google/gemma-2-2b-it",
316
+ "google/gemma-3n-e2b-it",
317
+ "google/diffusiongemma-26b-a4b-it",
318
+ # Aggiornato dinamicamente in caso di errore "system role not supported"
319
+ }
320
+
321
+
322
+ def normalize_messages(messages: list[dict], model: str) -> list[dict]:
323
+ """
324
+ Adatta i messaggi al modello:
325
+ - Se il modello non supporta 'system', converte il system prompt
326
+ come prefisso del primo messaggio user.
327
+ - Rimuove messaggi vuoti.
328
+ """
329
+ if not messages:
330
+ return messages
331
+
332
+ # Se il modello supporta system, non fare nulla
333
+ if model not in MODELS_NO_SYSTEM_ROLE:
334
+ return messages
335
+
336
+ # Estrai tutti i system prompt
337
+ system_parts = []
338
+ non_system = []
339
+ for m in messages:
340
+ if m.get("role") == "system":
341
+ content = (m.get("content") or "").strip()
342
+ if content:
343
+ system_parts.append(content)
344
+ else:
345
+ non_system.append(m)
346
+
347
+ if not system_parts:
348
+ return non_system if non_system else messages
349
+
350
+ # Prependi il system al primo user message
351
+ system_text = "\n\n".join(system_parts)
352
+ prepended = False
353
+ for i, m in enumerate(non_system):
354
+ if m.get("role") == "user":
355
+ non_system[i] = {
356
+ "role": "user",
357
+ "content": f"[System instructions]\n{system_text}\n\n[User message]\n{m.get('content', '')}"
358
+ }
359
+ prepended = True
360
+ break
361
+
362
+ if not prepended:
363
+ # Nessun user msg trovato, aggiungi come user
364
+ non_system.insert(0, {"role": "user", "content": system_text})
365
+
366
+ return non_system
367
+
368
+
369
  # ══════════════════════════════════════════════════════════════
370
  # MEMORIA
371
  # ══════════════════════════════════════════════════════════════
 
375
  max_long_tokens=int(os.getenv("MAX_LONG_MEMORY_TOKENS", 50000))
376
  )
377
 
378
+
379
  # ═════════════════════════════════════════════��════════════════
380
+ # CLIENT NVIDIA (con auto-adattamento system role)
381
  # ══════════════════════════════════════════════════════════════
382
 
383
+ def _is_system_role_error(err_text: str) -> bool:
384
+ """Rileva errori dovuti a system role non supportato"""
385
+ if not err_text:
386
+ return False
387
+ err_lower = err_text.lower()
388
+ keywords = [
389
+ "system role not supported",
390
+ "does not support 'system'",
391
+ "does not support system",
392
+ "system role is not supported",
393
+ "role 'system' not allowed",
394
+ ]
395
+ return any(k in err_lower for k in keywords)
396
+
397
+
398
  async def nvidia_chat_stream(
399
  messages: list[dict],
400
  model: str = DEFAULT_MODEL,
 
405
  if not model or not str(model).strip():
406
  model = DEFAULT_MODEL
407
 
408
+ # 🎯 Auto-adatta i messaggi al modello
409
+ original_messages = messages
410
+ messages = normalize_messages(messages, model)
411
+
412
  headers = {
413
  "Authorization": f"Bearer {NVIDIA_API_KEY}",
414
  "Content-Type": "application/json",
 
429
  f"{NVIDIA_BASE_URL}/chat/completions",
430
  headers=headers, json=payload,
431
  ) as response:
432
+
433
+ # 🎯 AUTO-RETRY se system role non supportato
434
  if response.status_code != 200:
435
  err = (await response.aread()).decode(errors="ignore")[:500]
436
+ if _is_system_role_error(err) and model not in MODELS_NO_SYSTEM_ROLE:
437
+ print(f"[AUTO-FIX] Aggiunto {model} a MODELS_NO_SYSTEM_ROLE")
438
+ MODELS_NO_SYSTEM_ROLE.add(model)
439
+ # Ri-normalizza e riprova (con un nuovo stream)
440
+ payload["messages"] = normalize_messages(original_messages, model)
441
+ async with client.stream(
442
+ "POST",
443
+ f"{NVIDIA_BASE_URL}/chat/completions",
444
+ headers=headers, json=payload,
445
+ ) as retry_response:
446
+ if retry_response.status_code != 200:
447
+ err2 = (await retry_response.aread()).decode(errors="ignore")[:500]
448
+ raise Exception(f"NVIDIA API {retry_response.status_code}: {err2}")
449
+ async for line in retry_response.aiter_lines():
450
+ if line.startswith("data: "):
451
+ data = line[6:]
452
+ if data.strip() == "[DONE]":
453
+ return
454
+ try:
455
+ chunk = json.loads(data)
456
+ delta = chunk["choices"][0].get("delta", {})
457
+ if "content" in delta and delta["content"]:
458
+ yield delta["content"]
459
+ except (json.JSONDecodeError, KeyError, IndexError):
460
+ continue
461
+ return
462
+
463
+ # Altri errori
464
  if response.status_code == 404:
465
+ raise Exception(f"Modello '{model}' non disponibile (404)")
466
  raise Exception(f"NVIDIA API {response.status_code}: {err}")
467
 
468
+ # Success normale
469
  async for line in response.aiter_lines():
470
  if line.startswith("data: "):
471
  data = line[6:]
 
491
  if not model or not str(model).strip():
492
  model = DEFAULT_MODEL
493
 
494
+ # 🎯 Auto-adatta i messaggi al modello
495
+ original_messages = messages
496
+ messages = normalize_messages(messages, model)
497
+
498
  headers = {
499
  "Authorization": f"Bearer {NVIDIA_API_KEY}",
500
  "Content-Type": "application/json",
501
  }
 
 
 
 
 
 
 
 
502
 
503
+ async def _do_request(c: httpx.AsyncClient, msgs: list[dict]) -> httpx.Response:
504
+ return await c.post(
505
  f"{NVIDIA_BASE_URL}/chat/completions",
506
+ headers=headers,
507
+ json={
508
+ "model": model,
509
+ "messages": msgs,
510
+ "temperature": temperature,
511
+ "max_tokens": max_tokens,
512
+ "top_p": top_p,
513
+ "stream": False,
514
+ },
515
  )
516
+
517
+ async def _call(c: httpx.AsyncClient) -> str:
518
+ resp = await _do_request(c, messages)
519
+
520
+ # 🎯 AUTO-RETRY se system role non supportato
521
+ if resp.status_code != 200:
522
+ err_body = resp.text[:500]
523
+ if _is_system_role_error(err_body) and model not in MODELS_NO_SYSTEM_ROLE:
524
+ print(f"[AUTO-FIX] Aggiunto {model} a MODELS_NO_SYSTEM_ROLE")
525
+ MODELS_NO_SYSTEM_ROLE.add(model)
526
+ # Retry con messaggi rinormalizzati
527
+ retry_msgs = normalize_messages(original_messages, model)
528
+ resp = await _do_request(c, retry_msgs)
529
+ if resp.status_code == 200:
530
+ return resp.json()["choices"][0]["message"]["content"]
531
+
532
+ if resp.status_code == 404:
533
+ raise Exception(f"Modello '{model}' non disponibile")
534
+ raise Exception(f"NVIDIA API {resp.status_code}: {resp.text[:500]}")
535
+
536
+ try:
537
+ return resp.json()["choices"][0]["message"]["content"]
538
+ except (KeyError, IndexError, TypeError):
539
+ raise Exception(f"Risposta NVIDIA non valida per {model}: {resp.text[:200]}")
540
+
541
+ if client:
542
+ return await _call(client)
543
  else:
544
  async with httpx.AsyncClient(timeout=HTTP_TIMEOUT, limits=HTTP_LIMITS) as c:
545
+ return await _call(c)
 
 
 
 
 
 
 
 
 
546
 
547
 
548
  # ══════════════════════════════════════════════════════════════
 
552
  app = FastAPI(
553
  title="πŸš€ NVIDIA AI Multi-Model API",
554
  description=f"{len(NVIDIA_MODELS)} modelli verificati e funzionanti",
555
+ version="3.1.0",
556
  )
557
 
558
  app.add_middleware(
 
618
  async def info():
619
  return {
620
  "service": "πŸš€ NVIDIA AI Multi-Model",
621
+ "version": "3.1.0",
622
  "default_model": DEFAULT_MODEL,
623
  "models_available": len(NVIDIA_MODELS),
624
+ "models_no_system_role": sorted(MODELS_NO_SYSTEM_ROLE),
625
  "all_models_verified": True,
626
  "last_verified": "2026-07-04",
627
  }
 
654
  "total": len(models),
655
  "categories": sorted(set(m["category"] for m in NVIDIA_MODELS.values())),
656
  "default": DEFAULT_MODEL,
657
+ "models_no_system_role": sorted(MODELS_NO_SYSTEM_ROLE),
658
  }
659
 
660
 
 
662
  async def get_model_info(model_id: str):
663
  if model_id not in NVIDIA_MODELS:
664
  raise HTTPException(404, f"Modello '{model_id}' non nel catalogo")
665
+ return {
666
+ "model_id": model_id,
667
+ **NVIDIA_MODELS[model_id],
668
+ "supports_system_role": model_id not in MODELS_NO_SYSTEM_ROLE,
669
+ }
670
 
671
 
672
  # ══════════════════════════════════════════════════════════════
 
675
 
676
  @app.get("/v1/debug/test-model")
677
  async def debug_test_model(model: str = Query(DEFAULT_MODEL)):
678
+ """Testa un modello con system + user (come nella UI)"""
679
  try:
680
  start = time.time()
681
  response = await nvidia_chat(
682
+ messages=[
683
+ {"role": "system", "content": "You are a helpful assistant."},
684
+ {"role": "user", "content": "Say 'ok'."},
685
+ ],
686
  model=model, max_tokens=10,
687
  )
688
  return {
 
690
  "model": model,
691
  "response": response,
692
  "elapsed_ms": round((time.time() - start) * 1000),
693
+ "supports_system_role": model not in MODELS_NO_SYSTEM_ROLE,
694
  }
695
  except Exception as e:
696
  return {"status": "❌ ERROR", "model": model, "error": str(e)}
697
 
698
 
699
+ @app.get("/v1/debug/no-system-models")
700
+ async def debug_no_system_models():
701
+ """Lista modelli che non supportano il role 'system'"""
702
+ return {
703
+ "models_no_system_role": sorted(MODELS_NO_SYSTEM_ROLE),
704
+ "total": len(MODELS_NO_SYSTEM_ROLE),
705
+ "note": "Questi modelli ricevono automaticamente il system_prompt come prefisso del primo user message."
706
+ }
707
+
708
+
709
  async def _test_single(mid: str, client: httpx.AsyncClient, sem: asyncio.Semaphore) -> dict:
710
  async with sem:
711
  start = time.time()
712
  try:
713
+ # Test realistico: system + user (come la UI)
714
  resp = await nvidia_chat(
715
+ messages=[
716
+ {"role": "system", "content": "You are a helpful assistant."},
717
+ {"role": "user", "content": "Say 'ok'"},
718
+ ],
719
+ model=mid, max_tokens=5, client=client,
720
  )
721
  return {
722
  "model": mid, "status": "βœ…",
723
  "elapsed_ms": round((time.time() - start) * 1000),
724
  "preview": (resp or "")[:30],
725
+ "supports_system_role": mid not in MODELS_NO_SYSTEM_ROLE,
726
  }
727
  except Exception as e:
728
  return {
729
  "model": mid, "status": "❌",
730
  "elapsed_ms": round((time.time() - start) * 1000),
731
+ "error": str(e)[:150],
732
  }
733
 
734
 
735
  @app.get("/v1/debug/test-all-parallel")
736
  async def debug_test_all_parallel(concurrency: int = Query(15, ge=1, le=50)):
737
+ """⚑ Test parallelo async di tutti i modelli del catalogo"""
738
  start = time.time()
739
  testable = list(NVIDIA_MODELS.keys())
740
  sem = asyncio.Semaphore(concurrency)
 
750
  "working": len(working),
751
  "broken": len(broken),
752
  "total_time_seconds": round(time.time() - start, 2),
753
+ "models_no_system_role": sorted(MODELS_NO_SYSTEM_ROLE),
754
  },
755
  "working_models_by_speed": working,
756
  "broken_models": broken,
 
851
  full = []
852
  try:
853
  yield {"event": "start", "data": json.dumps({
854
+ "session_id": session_id, "model": req.model, "timestamp": time.time(),
855
+ "supports_system_role": req.model not in MODELS_NO_SYSTEM_ROLE,
856
  })}
857
  async for chunk in nvidia_chat_stream(
858
  messages=messages, model=req.model,
 
930
  MCP_TOOLS = [
931
  {
932
  "name": "nvidia_chat",
933
+ "description": "Chat with any verified NVIDIA NIM model with memory (auto-adapts system role)",
934
  "inputSchema": {
935
  "type": "object",
936
  "properties": {
 
1013
  if method == "initialize":
1014
  return _ok({
1015
  "protocolVersion": "2024-11-05",
1016
+ "serverInfo": {"name": "nvidia-ai", "version": "3.1.0"},
1017
  "capabilities": {
1018
  "tools": {"listChanged": False},
1019
  "resources": {"subscribe": False, "listChanged": False}
 
1135
  th,td{{padding:8px;border:1px solid #333;text-align:left}}
1136
  th{{background:#16213e;color:#76b900}}
1137
  </style></head><body>
1138
+ <h1>πŸš€ NVIDIA AI Multi-Model API v3.1</h1>
1139
+ <p><b>{len(NVIDIA_MODELS)} modelli VERIFICATI</b> + auto-adattamento system role</p>
1140
 
1141
  <h2>πŸ’¬ Chat POST</h2>
1142
  <pre>curl -X POST /v1/chat -H "Content-Type: application/json" \\
 
1148
  <h2>⚑ Test Parallelo</h2>
1149
  <pre>curl "/v1/debug/test-all-parallel?concurrency=15"</pre>
1150
 
1151
+ <h2>🎯 Modelli senza system role (auto-adattati)</h2>
1152
+ <pre>curl "/v1/debug/no-system-models"</pre>
1153
+
1154
  <h2>πŸ”Œ MCP</h2>
1155
  <pre>curl -X POST /v1/mcp -H "Content-Type: application/json" \\
1156
  -d '{{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{{}}}}'</pre>
 
1222
  css=".gradio-container{max-width:1400px !important;} footer{display:none !important;}"
1223
  ) as demo:
1224
  gr.Markdown(f"""
1225
+ # πŸš€ NVIDIA AI Multi-Model v3.1
1226
+ **{len(NVIDIA_MODELS)} modelli VERIFICATI** + auto-adattamento system role β€’ Memoria β€’ SSE β€’ MCP
1227
 
1228
  🏠 [UI](/) | πŸ“– [Docs](/api-docs) | πŸ”§ [Swagger](/docs) | πŸ”Œ [MCP](/v1/mcp)
1229
 
1230
+ **Default:** `{DEFAULT_MODEL}` (⚑ 153ms)
1231
  """)
1232
 
1233
  with gr.Tab("πŸ’¬ Chat"):
 
1258
 
1259
  with gr.Tab("πŸ€– Modelli"):
1260
  gr.Markdown(f"### οΏ½οΏ½οΏ½οΏ½ Catalogo β€” {len(NVIDIA_MODELS)} modelli verificati")
1261
+ data = [[mi["category"], mi["name"], mid, mi["context"], mi["description"],
1262
+ "❌" if mid in MODELS_NO_SYSTEM_ROLE else "βœ…"]
1263
  for mid, mi in NVIDIA_MODELS.items()]
1264
+ gr.Dataframe(
1265
+ headers=["Cat", "Nome", "ID", "Context", "Descrizione", "System Role"],
1266
+ value=data, interactive=False, wrap=True
1267
+ )
1268
 
1269
  with gr.Tab("πŸš€ Test Parallelo"):
1270
+ gr.Markdown("### ⚑ Verifica live tutti i modelli in parallelo (con system prompt)")
1271
  test_btn = gr.Button("πŸš€ Testa tutti i modelli", variant="primary")
1272
  test_output = gr.JSON(label="Risultati")
1273
 
 
1285
  "βœ… funzionanti": len(working),
1286
  "❌ non_funzionanti": len(broken),
1287
  "totale": len(results),
1288
+ "models_no_system_role_detected": sorted(MODELS_NO_SYSTEM_ROLE),
1289
  "working_by_speed": working,
1290
  "broken": broken,
1291
  }
 
1301
  stats_btn.click(get_memory_info, inputs=[mem_session], outputs=[mem_display])
1302
  clear_mem_btn.click(clear_session_memory, inputs=[mem_session], outputs=[mem_display])
1303
 
1304
+ with gr.Tab("🎯 System Role"):
1305
+ gr.Markdown(f"""
1306
+ ## 🎯 Auto-adattamento System Role
1307
+
1308
+ Alcuni modelli (come **Gemma 2**, **Gemma 3n**, **DiffusionGemma**) non supportano il ruolo `system`.
1309
+
1310
+ Questo Space **rileva automaticamente** l'errore e converte il `system_prompt` come **prefisso** del primo messaggio user.
1311
+
1312
+ ### πŸ“‹ Modelli attualmente in auto-adattamento:
1313
+ """)
1314
+ no_sys_display = gr.JSON(label="Modelli senza system role")
1315
+
1316
+ def get_no_sys():
1317
+ return {
1318
+ "models_no_system_role": sorted(MODELS_NO_SYSTEM_ROLE),
1319
+ "total": len(MODELS_NO_SYSTEM_ROLE),
1320
+ "note": "Aggiornato dinamicamente quando viene rilevato un errore 'system role not supported'"
1321
+ }
1322
+
1323
+ refresh_btn = gr.Button("πŸ”„ Aggiorna lista")
1324
+ refresh_btn.click(get_no_sys, outputs=[no_sys_display])
1325
+ demo.load(get_no_sys, outputs=[no_sys_display])
1326
+
1327
  with gr.Tab("πŸ“– Docs"):
1328
  gr.Markdown("""
1329
  ## πŸ“– Documentazione
 
1338
  - `GET /v1/chat/stream` β€” SSE streaming
1339
  - `POST /v1/mcp` β€” MCP JSON-RPC
1340
  - `GET /v1/debug/test-all-parallel` β€” Test parallelo
1341
+ - `GET /v1/debug/no-system-models` β€” Modelli senza system role
1342
  """)
1343
 
1344