Spaces:
Running
Registry sin fireworks: 12 entradas verificadas con llamada real + check_catalog endurecido
Browse filesProducció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>
- ARCHITECTURE.md +18 -13
- app.py +1 -1
- requirements.txt +1 -0
- y_refiner.py +92 -28
|
@@ -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:
|
| 54 |
-
│ γ moonshotai/Kimi-K2.6:
|
| 55 |
│
|
| 56 |
-
├─ N deepseek-ai/DeepSeek-V4-Flash:
|
| 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
|
| 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 (
|
|
|
|
|
|
|
| 101 |
|
| 102 |
| Rol | Primario | Fallbacks | Precio in/out por 1M |
|
| 103 |
|---|---|---|---|
|
| 104 |
-
| α | `zai-org/GLM-5.2:
|
| 105 |
-
| γ | `moonshotai/Kimi-K2.6:
|
| 106 |
-
| N | `deepseek-ai/DeepSeek-V4-Flash:
|
| 107 |
-
| fast | `Qwen/
|
| 108 |
|
| 109 |
-
**
|
| 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
|
| 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:together → DeepSeek-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 |
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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:
|
| 22 |
-
{"model": "zai-org/GLM-5.
|
| 23 |
-
{"model": "zai-org/GLM-
|
| 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:
|
| 29 |
-
{"model": "
|
| 30 |
-
{"model": "moonshotai/Kimi-K2.
|
| 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:
|
| 35 |
-
{"model": "
|
| 36 |
-
{"model": "
|
| 37 |
],
|
| 38 |
# Cadena del escalón de degradación de 1 LLM (usada por LlamaRefiner)
|
| 39 |
"fast": [
|
| 40 |
-
{"model": "Qwen/
|
| 41 |
-
{"model": "Qwen/Qwen3.5-9B:
|
|
|
|
| 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
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
import json
|
| 148 |
-
import
|
|
|
|
|
|
|
| 149 |
try:
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|