Malaji71 Claude Opus 5 commited on
Commit
ff9a8d0
·
1 Parent(s): 3cc36aa

Registry sin fireworks: 12 entradas verificadas con llamada real + check_catalog endurecido

Browse files

Producción caída: todos los primarios iban por :fireworks-ai y devolvían
402 "Pay-as-you go is not enabled for provider fireworks-ai yet" (no es
falta de saldo: HF no puede facturar overage por esa vía). Y casi toda la
cadena de fallback estaba muerta: GLM-5.1:together, Kimi-K2.5:fireworks,
Kimi-K2.6:together y DeepSeek-V4-Flash:fireworks en 410 deprecated,
Llama-3.3-70B:groq en 404, Qwen2.5-7B:together en 400 non-serverless.

MODEL_REGISTRY reconstruido desde el catálogo del router. fireworks-ai
prohibido. 3 escalones por rol en 3 providers DISTINTOS, para que un corte
de facturación de un provider no tumbe el rol entero:

alpha GLM-5.2:deepinfra -> :novita -> :baseten
gamma Kimi-K2.6:deepinfra -> :novita -> :baseten
normalizer DeepSeek-V4-Flash:deepinfra -> V4-Flash-0731:together -> V3.2:novita
fast Qwen3.5-9B:deepinfra -> :ovhcloud -> Qwen2.5-72B:novita

Las 12 entradas verificadas con una llamada real (200 + content no vacío):
el catálogo no basta, marcaba fireworks como "live" mientras daba 402.
Descartadas por content vacío (el provider ignora reasoning_effort):
GLM-5.2:together, GLM-5:novita, DeepSeek-V4-Flash:novita, Qwen3-32B:nscale;
y Qwen3.5-9B:together por 400 Input validation error. Pricing al día del
catálogo; reasoning_effort="none" conservado en todos los híbridos.

check_catalog(client=None):
- verifica TODA la cadena de cada rol, no solo el primario, y loguea
"rol: N/M entradas en catálogo" + las que faltan por nombre
- STARTUP_PROBE=1 añade un probe de facturación (1 llamada de 5 tokens al
primario de cada rol) con WARNING "en catálogo pero NO facturable" ante
401/402/403; ningún fallo del probe impide el arranque
- pasa a requests: urllib con user-agent de Python recibe 403 de Cloudflare,
así que el chequeo anterior fallaba en silencio

Sin cambios en la lógica de degradación, el circuit breaker, agent.py ni el
RAG v2. Coste medido de una pasada Y completa con prompts reales: $0.0012
(antes ~$0.0025 en fireworks).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files changed (4) hide show
  1. ARCHITECTURE.md +18 -13
  2. app.py +1 -1
  3. requirements.txt +1 -0
  4. y_refiner.py +92 -28
ARCHITECTURE.md CHANGED
@@ -50,10 +50,10 @@ input del usuario (ES o EN)
50
  │ └─ _grammar_profile(examples) → spec gramatical medida (spaCy por cláusula)
51
 
52
  ├─ EN PARALELO (ThreadPoolExecutor, max_workers=2):
53
- │ α zai-org/GLM-5.2:fireworks-ai → canónico: spec + léxico (solo α ve el RAG)
54
- │ γ moonshotai/Kimi-K2.6:fireworks-ai → concreto: SOLO el concepto, quality-tags prohibidos
55
 
56
- ├─ N deepseek-ai/DeepSeek-V4-Flash:fireworks-ai → compilador (compose) o editor (polish)
57
 
58
  └─ contrato (YRefiner._check_contract) → cadena de degradación:
59
  N intento 1 → N intento 2 → α/γ directo (y_direct) → 1 LLM (degraded_fast) → fallback semántico
@@ -73,7 +73,7 @@ input del usuario (ES o EN)
73
  - `_check_contract(user_en, raw, polish)` — limpieza + validación; en polish añade el guardarraíl **`no_edit`**: jaccard de palabras output/input > 0.75 = incumplimiento (la edición nula era el óptimo del contrato de fidelidad v1 — lección clave).
74
  - Degradación parcial: si cae α o γ, el superviviente ocupa ambos slots de N (la prohibición "no copies verbatim" fuerza reescritura).
75
  - Telemetría: `self.session` (agregados: `n_ok_first/retry`, `y_direct`, `degraded_fast`, `fallback_sem`, `cost_total_usd`) + `self.records` (deque 200: latencias por nodo, `failed_node`, `final_step`, `depth`, `category`, coste).
76
- - **`check_catalog()`**: al arrancar, 1 GET a `router.huggingface.co/v1/models` y log de qué primarios siguen live (mitigación de desaparición de providers).
77
  - **Constantes**: `TIMEOUTS` (α/γ 30s, N 20s), `GLOBAL_BUDGET_S=75`, `MAX_TOKENS=300` los tres nodos (160 truncaba pulidos largos).
78
 
79
  ### 2.3 `agent.py` — nombres clave
@@ -97,18 +97,20 @@ input del usuario (ES o EN)
97
  - `_call_llm` usa `call_with_fallback(role="fast")` — el modo rápido también tiene cadena de fallback.
98
  - **UI mínima**: textbox de idea + botón "Refinar prompt" + prompt refinado + referencia estructural + estado/métricas. Sin categorías, sin proporciones, sin generador de imagen, sin banderas ni emojis en labels. Cabecera: solo "PromptCraft v3.1 / Compositional Transposition Engine."
99
 
100
- ### 2.5 Modelos y precios (verificados en el router, 2026-07-02)
 
 
101
 
102
  | Rol | Primario | Fallbacks | Precio in/out por 1M |
103
  |---|---|---|---|
104
- | α | `zai-org/GLM-5.2:fireworks-ai` | GLM-5.1:together → GLM-4.7:deepinfra | $1.40/$4.40 |
105
- | γ | `moonshotai/Kimi-K2.6:fireworks-ai` | Llama-3.3-70B:groq → Kimi-K2.5:fireworks → Kimi-K2.6:together (último: no honra el flag anti-thinking) | $0.95/$4.00 |
106
- | N | `deepseek-ai/DeepSeek-V4-Flash:fireworks-ai` | Qwen3.5-9B:deepinfraQwen2.5-7B:together | $0.14/$0.28 |
107
- | fast | `Qwen/Qwen2.5-7B-Instruct:together` | Qwen3.5-9B:deepinfra | $0.30/$0.30 |
108
 
109
- **Coste real medido**: ~$0.0013–0.003 por refinamiento Y (sube con anti-repetición/pulido por más tokens de input). Con los $2/mes de créditos Pro: ~700–1.500 refinamientos/mes. **Latencia**: primera generación ~8–15s; regeneraciones ~4–8s (cachés).
110
 
111
- ---
112
 
113
  ## 3. Dataset e índice
114
 
@@ -144,7 +146,7 @@ El Space los descarga al arrancar (`ImprovedSemanticAgent._load_rag_v2()`, env `
144
 
145
  ## 4. Decisiones de diseño y lecciones (con contexto)
146
 
147
- 1. **`reasoning_effort: "none"` es obligatorio** para los híbridos 2026 del router (GLM-5.2, Kimi-K2.6/2.5, DeepSeek-V4, Qwen3.5): sin él, el razonamiento inline consume TODO el max_tokens y `content` sale vacío o lleno de deliberación. El router **rechaza** `chat_template_kwargs` y `reasoning.enabled` (400 "Extra inputs"). **Kimi-K2.6 servido por together no honra el flag** (content vacío siempre) por eso γ va por fireworks y together quedó al final de su cadena.
148
  2. **`max_retries=0` en el cliente OpenAI**: los reintentos son de `call_with_fallback`, no del SDK — si no, se multiplican.
149
  3. **Sin límite de longitud**: FLUX/MJ/SD3 (T5/LLM encoders) no se degradan con prompts largos. `too_long` eliminado del validador; la cadencia la marca la spec gramatical, no un techo.
150
  4. **Fidelidad ≠ identidad** (lección del modo pulido v1): un contrato que dice "conserva la redacción, no pierdas nada" tiene como óptimo la **edición nula** — el sistema devolvía el input intacto. Arreglo: se protege el *inventario* (objetos/acciones/relaciones/tono) y el orden, nunca la redacción; guardarraíl `no_edit` (jaccard >0.75) y reintento a temperatura MÁS alta. Regla general: *cada regla nueva en un prompt puede tener un óptimo degenerado que nadie pidió*.
@@ -156,7 +158,9 @@ El Space los descarga al arrancar (`ImprovedSemanticAgent._load_rag_v2()`, env `
156
 
157
  ## 5. Gotchas operativos (para no re-descubrirlos)
158
 
159
- - **Probar el router con `urllib` da 403 de Cloudflare** (bloqueo por user-agent de Python). Con el SDK de OpenAI funciona. No es problema de cuenta ni de provider.
 
 
160
  - **HF Jobs requiere token con permiso `job.write`** — el token fine-grained cacheado en `~/.cache/huggingface/token` no lo tiene; el dueño tiene uno aparte con Jobs habilitado.
161
  - **Gradio 6**: `show_copy_button` ya no existe en `Textbox` (crasheó un deploy). `ssr_mode=False` en `launch()` evita "Could not get API info".
162
  - **`huggingface_hub>=1.0` rompe `transformers`** (exige <1.0) — cuidado al actualizar el hub para usar `hf jobs` en un entorno compartido con el runtime del Space.
@@ -185,6 +189,7 @@ El Space los descarga al arrancar (`ImprovedSemanticAgent._load_rag_v2()`, env `
185
  | `22f26d9` | **Índice reconstruido: 100k vectores en GPU (HF Jobs/T4), receta consistente** |
186
  | `675b76b` | ARCHITECTURE.md (este documento) |
187
  | `020c862` | **RAG v2: 459.718 captions fotorealistas MJ v6 desde el repo-bucket `Malaji71/prompteador-rag`** |
 
188
 
189
  ---
190
 
 
50
  │ └─ _grammar_profile(examples) → spec gramatical medida (spaCy por cláusula)
51
 
52
  ├─ EN PARALELO (ThreadPoolExecutor, max_workers=2):
53
+ │ α zai-org/GLM-5.2:deepinfra → canónico: spec + léxico (solo α ve el RAG)
54
+ │ γ moonshotai/Kimi-K2.6:deepinfra → concreto: SOLO el concepto, quality-tags prohibidos
55
 
56
+ ├─ N deepseek-ai/DeepSeek-V4-Flash:deepinfra → compilador (compose) o editor (polish)
57
 
58
  └─ contrato (YRefiner._check_contract) → cadena de degradación:
59
  N intento 1 → N intento 2 → α/γ directo (y_direct) → 1 LLM (degraded_fast) → fallback semántico
 
73
  - `_check_contract(user_en, raw, polish)` — limpieza + validación; en polish añade el guardarraíl **`no_edit`**: jaccard de palabras output/input > 0.75 = incumplimiento (la edición nula era el óptimo del contrato de fidelidad v1 — lección clave).
74
  - Degradación parcial: si cae α o γ, el superviviente ocupa ambos slots de N (la prohibición "no copies verbatim" fuerza reescritura).
75
  - Telemetría: `self.session` (agregados: `n_ok_first/retry`, `y_direct`, `degraded_fast`, `fallback_sem`, `cost_total_usd`) + `self.records` (deque 200: latencias por nodo, `failed_node`, `final_step`, `depth`, `category`, coste).
76
+ - **`check_catalog(client=None)`**: al arrancar, 1 GET a `router.huggingface.co/v1/models` (vía `requests`; `urllib` recibe 403 de Cloudflare) y log de **cuántas entradas de CADA cadena** siguen en catálogo (`"rol: N/M entradas en catálogo"`), con WARNING nominal de las que faltan y ERROR si una cadena entera desaparece. Con **`STARTUP_PROBE=1`** añade un probe de facturación: 1 llamada real (`max_tokens=5`, `temperature=0`) al primer modelo de cada rol, con WARNING `"en catálogo pero NO facturable"` ante 401/402/403. El catálogo miente sobre facturación (fireworks salía `live` y devolvía 402), solo la llamada real lo distingue. Ningún fallo del probe impide el arranque.
77
  - **Constantes**: `TIMEOUTS` (α/γ 30s, N 20s), `GLOBAL_BUDGET_S=75`, `MAX_TOKENS=300` los tres nodos (160 truncaba pulidos largos).
78
 
79
  ### 2.3 `agent.py` — nombres clave
 
97
  - `_call_llm` usa `call_with_fallback(role="fast")` — el modo rápido también tiene cadena de fallback.
98
  - **UI mínima**: textbox de idea + botón "Refinar prompt" + prompt refinado + referencia estructural + estado/métricas. Sin categorías, sin proporciones, sin generador de imagen, sin banderas ni emojis en labels. Cabecera: solo "PromptCraft v3.1 / Compositional Transposition Engine."
99
 
100
+ ### 2.5 Modelos y precios (reconstruido y verificado con llamadas reales, 2026-08-26)
101
+
102
+ **Incidente que forzó la reconstrucción**: todos los primarios iban por `:fireworks-ai` y devolvían **HTTP 402** `"Pay-as-you go is not enabled for provider fireworks-ai yet"` — no era falta de saldo ($17.84 en créditos), HF no puede facturar el overage por esa vía. Y casi toda la cadena de fallback estaba muerta: `GLM-5.1:together`, `Kimi-K2.5:fireworks-ai`, `Kimi-K2.6:together`, `DeepSeek-V4-Flash:fireworks-ai` → **410 deprecated**; `Llama-3.3-70B-Instruct:groq` → **404**; `Qwen2.5-7B-Instruct:together` → **400 non-serverless**. **`:fireworks-ai` queda prohibido en el registry hasta nuevo aviso.**
103
 
104
  | Rol | Primario | Fallbacks | Precio in/out por 1M |
105
  |---|---|---|---|
106
+ | α | `zai-org/GLM-5.2:deepinfra` | GLM-5.2:novita → GLM-5.2:baseten | $0.75/$2.40 |
107
+ | γ | `moonshotai/Kimi-K2.6:deepinfra` | Kimi-K2.6:novita → Kimi-K2.6:baseten | $0.75/$3.50 |
108
+ | N | `deepseek-ai/DeepSeek-V4-Flash:deepinfra` | DeepSeek-V4-Flash-0731:togetherDeepSeek-V3.2:novita | $0.09/$0.18 |
109
+ | fast | `Qwen/Qwen3.5-9B:deepinfra` | Qwen3.5-9B:ovhcloud → Qwen2.5-72B-Instruct:novita | $0.10/$0.15 |
110
 
111
+ Las 12 entradas están verificadas con **una llamada real al router** (200 + `content` no vacío): el catálogo no basta, decía `live` para fireworks. Cada cadena usa **3 providers distintos** para que un corte de facturación de un provider no tumbe el rol entero. Descartadas por devolver 200 con `content` vacío (no honran `reasoning_effort`): `GLM-5.2:together`, `GLM-5:novita`, `DeepSeek-V4-Flash:novita`, `Qwen3-32B:nscale`; y `Qwen3.5-9B:together` por 400 `Input validation error`.
112
 
113
+ **Coste real medido** (pasada Y completa, compose, 1ª generación, prompts reales): **$0.0012** — α $0.00036 + γ $0.00078 + N $0.00006. Es ~2× más barato que el registry anterior en fireworks (α bajó de $1.40/$4.40 a $0.75/$2.40 y N de $0.14/$0.28 a $0.09/$0.18). Con los $2/mes de créditos Pro: **~1.600 refinamientos/mes** (las regeneraciones y el modo pulido suben algo por más tokens de input). **Latencia medida**: α ~1,5 s, γ ~8,5 s, N ~2,9 s.
114
 
115
  ## 3. Dataset e índice
116
 
 
146
 
147
  ## 4. Decisiones de diseño y lecciones (con contexto)
148
 
149
+ 1. **`reasoning_effort: "none"` es obligatorio** para los híbridos 2026 del router (GLM-5.2, Kimi-K2.6/2.5, DeepSeek-V4, Qwen3.5): sin él, el razonamiento inline consume TODO el max_tokens y `content` sale vacío o lleno de deliberación. El router **rechaza** `chat_template_kwargs` y `reasoning.enabled` (400 "Extra inputs"). **El flag se honra por provider, no por modelo**: el mismo Kimi-K2.6 lo respeta en deepinfra/novita/baseten y no en together; GLM-5.2 lo respeta en deepinfra/novita/baseten y no en together. Cualquier entrada nueva se prueba con una llamada real antes de entrar al registry.
150
  2. **`max_retries=0` en el cliente OpenAI**: los reintentos son de `call_with_fallback`, no del SDK — si no, se multiplican.
151
  3. **Sin límite de longitud**: FLUX/MJ/SD3 (T5/LLM encoders) no se degradan con prompts largos. `too_long` eliminado del validador; la cadencia la marca la spec gramatical, no un techo.
152
  4. **Fidelidad ≠ identidad** (lección del modo pulido v1): un contrato que dice "conserva la redacción, no pierdas nada" tiene como óptimo la **edición nula** — el sistema devolvía el input intacto. Arreglo: se protege el *inventario* (objetos/acciones/relaciones/tono) y el orden, nunca la redacción; guardarraíl `no_edit` (jaccard >0.75) y reintento a temperatura MÁS alta. Regla general: *cada regla nueva en un prompt puede tener un óptimo degenerado que nadie pidió*.
 
158
 
159
  ## 5. Gotchas operativos (para no re-descubrirlos)
160
 
161
+ - **Probar el router con `urllib` da 403 de Cloudflare** (bloqueo por user-agent de Python). Con `requests` (user-agent propio) o con el SDK de OpenAI funciona. No es problema de cuenta ni de provider. `check_catalog()` usa `requests` por esto.
162
+ - **El catálogo del router NO dice si un provider te factura**: fireworks-ai figuraba `status: "live"` mientras devolvía 402 en cada llamada. Un provider "live" puede estar cortado para tu cuenta. La única verificación válida es una llamada real (de ahí `STARTUP_PROBE=1`).
163
+ - **`content` vacío con `out=max_tokens` = el provider ignora `reasoning_effort`**: el thinking se comió el presupuesto. Es un fallo de *serving*, no de modelo — el mismo modelo en otro provider suele ir bien.
164
  - **HF Jobs requiere token con permiso `job.write`** — el token fine-grained cacheado en `~/.cache/huggingface/token` no lo tiene; el dueño tiene uno aparte con Jobs habilitado.
165
  - **Gradio 6**: `show_copy_button` ya no existe en `Textbox` (crasheó un deploy). `ssr_mode=False` en `launch()` evita "Could not get API info".
166
  - **`huggingface_hub>=1.0` rompe `transformers`** (exige <1.0) — cuidado al actualizar el hub para usar `hf jobs` en un entorno compartido con el runtime del Space.
 
189
  | `22f26d9` | **Índice reconstruido: 100k vectores en GPU (HF Jobs/T4), receta consistente** |
190
  | `675b76b` | ARCHITECTURE.md (este documento) |
191
  | `020c862` | **RAG v2: 459.718 captions fotorealistas MJ v6 desde el repo-bucket `Malaji71/prompteador-rag`** |
192
+ | `cd94eb7` | **Registry sin fireworks (402 en toda la cuenta): 12 entradas verificadas con llamada real, 3 providers distintos por rol, `check_catalog` sobre la cadena entera + `STARTUP_PROBE`** |
193
 
194
  ---
195
 
app.py CHANGED
@@ -329,7 +329,7 @@ def create_interface():
329
  try:
330
  refiner = LlamaRefiner()
331
  y_refiner = YRefiner(refiner)
332
- check_catalog()
333
  logger.info("✅ Todos los servicios inicializados correctamente")
334
  except Exception as e:
335
  logger.error(f"❌ Error crítico de inicialización: {e}", exc_info=True)
 
329
  try:
330
  refiner = LlamaRefiner()
331
  y_refiner = YRefiner(refiner)
332
+ check_catalog(refiner.openai_client)
333
  logger.info("✅ Todos los servicios inicializados correctamente")
334
  except Exception as e:
335
  logger.error(f"❌ Error crítico de inicialización: {e}", exc_info=True)
requirements.txt CHANGED
@@ -10,5 +10,6 @@ sentencepiece>=0.2.1 # ÚNICA versión con wheel cp313 (0.2.0 compila d
10
  spacy>=3.8.2,<4.0.0 # cp313 desde 3.8.2; 3.7.x NO soporta 3.13
11
  sacremoses>=0.1.1
12
  openai>=1.55.0
 
13
  pyarrow>=15.0.0 # captions del RAG v2 (parquet en repo-bucket)
14
  spaces
 
10
  spacy>=3.8.2,<4.0.0 # cp313 desde 3.8.2; 3.7.x NO soporta 3.13
11
  sacremoses>=0.1.1
12
  openai>=1.55.0
13
+ requests>=2.31.0 # check_catalog: urllib recibe 403 de Cloudflare
14
  pyarrow>=15.0.0 # captions del RAG v2 (parquet en repo-bucket)
15
  spaces
y_refiner.py CHANGED
@@ -16,29 +16,40 @@ logger = logging.getLogger(__name__)
16
  # Por rol: cadena [primario, fallback1, ...]. pricing = USD por 1M tokens (in, out).
17
  # reasoning_effort="none" desactiva el modo thinking de los híbridos 2026; sin
18
  # él, el razonamiento inline consume todo el max_tokens (verificado).
 
 
 
 
 
 
 
 
 
 
19
  MODEL_REGISTRY: Dict[str, List[Dict]] = {
 
20
  "alpha": [
21
- {"model": "zai-org/GLM-5.2:fireworks-ai", "pricing": (1.40, 4.40), "extra": {"reasoning_effort": "none"}},
22
- {"model": "zai-org/GLM-5.1:together", "pricing": (1.40, 4.40), "extra": {"reasoning_effort": "none"}},
23
- {"model": "zai-org/GLM-4.7:deepinfra", "pricing": (0.40, 1.75), "extra": {"reasoning_effort": "none"}},
24
  ],
25
- # γ primario en fireworks: el serving de together no honra reasoning_effort
26
- # y devuelve content vacío (verificado 2026-07-02), por eso va último
27
  "gamma": [
28
- {"model": "moonshotai/Kimi-K2.6:fireworks-ai", "pricing": (0.95, 4.00), "extra": {"reasoning_effort": "none"}},
29
- {"model": "meta-llama/Llama-3.3-70B-Instruct:groq", "pricing": (0.59, 0.79), "extra": {}},
30
- {"model": "moonshotai/Kimi-K2.5:fireworks-ai", "pricing": (0.60, 3.00), "extra": {"reasoning_effort": "none"}},
31
- {"model": "moonshotai/Kimi-K2.6:together", "pricing": (1.20, 4.50), "extra": {"reasoning_effort": "none"}},
32
  ],
 
 
33
  "normalizer": [
34
- {"model": "deepseek-ai/DeepSeek-V4-Flash:fireworks-ai", "pricing": (0.14, 0.28), "extra": {"reasoning_effort": "none"}},
35
- {"model": "Qwen/Qwen3.5-9B:deepinfra", "pricing": (0.10, 0.15), "extra": {"reasoning_effort": "none"}},
36
- {"model": "Qwen/Qwen2.5-7B-Instruct:together", "pricing": (0.30, 0.30), "extra": {}},
37
  ],
38
  # Cadena del escalón de degradación de 1 LLM (usada por LlamaRefiner)
39
  "fast": [
40
- {"model": "Qwen/Qwen2.5-7B-Instruct:together", "pricing": (0.30, 0.30), "extra": {}},
41
- {"model": "Qwen/Qwen3.5-9B:deepinfra", "pricing": (0.10, 0.15), "extra": {"reasoning_effort": "none"}},
 
42
  ],
43
  }
44
 
@@ -141,27 +152,80 @@ class NodeError(Exception):
141
  """Fallo de un nodo tras agotar su cadena de fallback."""
142
 
143
 
144
- def check_catalog() -> None:
145
- """Mitigación R4: al arrancar, 1 GET al catálogo del router y log de qué
146
- primarios del registry siguen servidos (no fatal si el GET falla)."""
 
 
 
 
 
 
 
147
  import json
148
- import urllib.request
 
 
149
  try:
150
- with urllib.request.urlopen("https://router.huggingface.co/v1/models", timeout=10) as r:
151
- catalog = json.loads(r.read())
152
- served = {}
 
 
 
153
  for m in catalog.get("data", []):
154
  served[m["id"]] = {p["provider"] for p in m.get("providers", [])
155
  if p.get("status") == "live"}
156
- for role, chain in MODEL_REGISTRY.items():
157
- primary = chain[0]["model"]
158
- model_id, _, provider = primary.partition(":")
159
- live = provider in served.get(model_id, set()) if provider else model_id in served
160
- icon = "✅" if live else "🚨"
161
- logger.info(f"{icon} Catálogo router — {role}: {primary} "
162
- f"{'live' if live else 'NO SERVIDO (usará fallback)'}")
163
  except Exception as e:
164
  logger.warning(f"⚠️ No se pudo verificar el catálogo del router: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
 
167
  # Circuit breaker de sesión: entradas con ≥3 fallos consecutivos se saltan.
 
16
  # Por rol: cadena [primario, fallback1, ...]. pricing = USD por 1M tokens (in, out).
17
  # reasoning_effort="none" desactiva el modo thinking de los híbridos 2026; sin
18
  # él, el razonamiento inline consume todo el max_tokens (verificado).
19
+ #
20
+ # Reconstruido 2026-08-26 tras la caída de fireworks-ai: TODOS los primarios
21
+ # devolvían HTTP 402 "Pay-as-you go is not enabled for provider fireworks-ai
22
+ # yet" (no es falta de saldo: HF no puede facturar el overage por esa vía) y
23
+ # casi toda la cadena de fallback estaba muerta (GLM-5.1:together,
24
+ # Kimi-K2.5/K2.6, DeepSeek-V4-Flash:fireworks → 410 deprecated;
25
+ # Llama-3.3-70B:groq → 404; Qwen2.5-7B:together → 400 non-serverless).
26
+ # Regla vigente: ":fireworks-ai" queda PROHIBIDO hasta nuevo aviso.
27
+ # Cada entrada de abajo está verificada con una llamada real al router
28
+ # (200 + content no vacío) y cada cadena usa 3 providers DISTINTOS.
29
  MODEL_REGISTRY: Dict[str, List[Dict]] = {
30
+ # α — GLM-5.2 en tres providers distintos (deepinfra es además el más barato)
31
  "alpha": [
32
+ {"model": "zai-org/GLM-5.2:deepinfra", "pricing": (0.75, 2.40), "extra": {"reasoning_effort": "none"}},
33
+ {"model": "zai-org/GLM-5.2:novita", "pricing": (1.40, 4.40), "extra": {"reasoning_effort": "none"}},
34
+ {"model": "zai-org/GLM-5.2:baseten", "pricing": (1.40, 4.40), "extra": {"reasoning_effort": "none"}},
35
  ],
 
 
36
  "gamma": [
37
+ {"model": "moonshotai/Kimi-K2.6:deepinfra", "pricing": (0.75, 3.50), "extra": {"reasoning_effort": "none"}},
38
+ {"model": "moonshotai/Kimi-K2.6:novita", "pricing": (0.80, 3.40), "extra": {"reasoning_effort": "none"}},
39
+ {"model": "moonshotai/Kimi-K2.6:baseten", "pricing": (0.95, 4.00), "extra": {"reasoning_effort": "none"}},
 
40
  ],
41
+ # N — DeepSeek-V4-Flash; el 3.º cae a V3.2 porque V4-Flash:novita no honra
42
+ # reasoning_effort (content vacío, verificado 2026-08-26)
43
  "normalizer": [
44
+ {"model": "deepseek-ai/DeepSeek-V4-Flash:deepinfra", "pricing": (0.09, 0.18), "extra": {"reasoning_effort": "none"}},
45
+ {"model": "deepseek-ai/DeepSeek-V4-Flash-0731:together", "pricing": (0.14, 0.28), "extra": {"reasoning_effort": "none"}},
46
+ {"model": "deepseek-ai/DeepSeek-V3.2:novita", "pricing": (0.269, 0.40), "extra": {"reasoning_effort": "none"}},
47
  ],
48
  # Cadena del escalón de degradación de 1 LLM (usada por LlamaRefiner)
49
  "fast": [
50
+ {"model": "Qwen/Qwen3.5-9B:deepinfra", "pricing": (0.10, 0.15), "extra": {"reasoning_effort": "none"}},
51
+ {"model": "Qwen/Qwen3.5-9B:ovhcloud", "pricing": (0.12, 0.18), "extra": {"reasoning_effort": "none"}},
52
+ {"model": "Qwen/Qwen2.5-72B-Instruct:novita", "pricing": (0.38, 0.40), "extra": {}},
53
  ],
54
  }
55
 
 
152
  """Fallo de un nodo tras agotar su cadena de fallback."""
153
 
154
 
155
+ def check_catalog(client=None) -> None:
156
+ """Mitigación R4: al arrancar, 1 GET al catálogo del router y log de cuántas
157
+ entradas de CADA cadena (no solo el primario) siguen servidas.
158
+
159
+ Con STARTUP_PROBE=1 añade un probe de facturación: 1 llamada real de
160
+ max_tokens=5 al primer modelo de cada rol. El catálogo miente sobre
161
+ facturación (fireworks aparecía "live" y devolvía 402), así que solo una
162
+ llamada real distingue "servido" de "facturable". Nada aquí puede impedir
163
+ el arranque: todo va envuelto en try/except.
164
+ """
165
  import json
166
+ import os
167
+
168
+ served = {}
169
  try:
170
+ # urllib con user-agent de Python recibe 403 de Cloudflare: usar requests
171
+ import requests
172
+ r = requests.get("https://router.huggingface.co/v1/models", timeout=10,
173
+ headers={"User-Agent": "PromptCraft/3.2 (+huggingface.co/spaces)"})
174
+ r.raise_for_status()
175
+ catalog = r.json()
176
  for m in catalog.get("data", []):
177
  served[m["id"]] = {p["provider"] for p in m.get("providers", [])
178
  if p.get("status") == "live"}
 
 
 
 
 
 
 
179
  except Exception as e:
180
  logger.warning(f"⚠️ No se pudo verificar el catálogo del router: {e}")
181
+ served = None
182
+
183
+ if served is not None:
184
+ for role, chain in MODEL_REGISTRY.items():
185
+ missing = []
186
+ for entry in chain:
187
+ model_id, _, provider = entry["model"].partition(":")
188
+ live = (provider in served.get(model_id, set()) if provider
189
+ else model_id in served)
190
+ if not live:
191
+ missing.append(entry["model"])
192
+ n_live, total = len(chain) - len(missing), len(chain)
193
+ icon = "✅" if not missing else ("⚠️" if n_live else "🚨")
194
+ logger.info(f"{icon} Catálogo router — {role}: {n_live}/{total} entradas en catálogo")
195
+ if missing:
196
+ logger.warning(f" {role}: NO servidas → {', '.join(missing)}")
197
+ if not n_live:
198
+ logger.error(f"🚨 {role}: cadena entera fuera del catálogo")
199
+
200
+ # ── Probe de facturación (opcional) ──────────────────────────────────────
201
+ if os.environ.get("STARTUP_PROBE") != "1":
202
+ return
203
+ if client is None:
204
+ logger.warning("⚠️ STARTUP_PROBE=1 pero no se pasó cliente; probe omitido")
205
+ return
206
+ for role, chain in MODEL_REGISTRY.items():
207
+ model = chain[0]["model"]
208
+ extra = dict(chain[0].get("extra", {}))
209
+ try:
210
+ resp = client.chat.completions.create(
211
+ model=model, messages=[{"role": "user", "content": "Reply with exactly: OK"}],
212
+ max_tokens=5, temperature=0, timeout=20,
213
+ extra_body=extra if extra else None,
214
+ )
215
+ text = (resp.choices[0].message.content or "").strip()
216
+ if text:
217
+ logger.info(f"✅ Probe — {role}: {model} responde y factura")
218
+ else:
219
+ logger.warning(f"⚠️ Probe — {role}: {model} devuelve content VACÍO "
220
+ f"(¿reasoning_effort ignorado por el provider?)")
221
+ except Exception as e:
222
+ status = getattr(e, "status_code", None)
223
+ if status in (401, 402, 403):
224
+ logger.warning(f"⚠️ Probe — {role}: {model} en catálogo pero NO facturable "
225
+ f"(HTTP {status}) — {str(e)[:120]}")
226
+ else:
227
+ logger.warning(f"⚠️ Probe — {role}: {model} falló (HTTP {status}) "
228
+ f"— {str(e)[:120]}")
229
 
230
 
231
  # Circuit breaker de sesión: entradas con ≥3 fallos consecutivos se saltan.