Skip to content

feat(providers): anillo de uso y saldo PAYG de MiniMax (#165) - #172

Merged
egdev6 merged 9 commits into
egdev6:mainfrom
Rafaeldelinares:feat/minimax
Oct 22, 2026
Merged

egdev6 merged 9 commits into
egdev6:mainfrom
Rafaeldelinares:feat/minimax

Conversation

@Rafaeldelinares

Copy link
Copy Markdown
Contributor

Ref #165

Implementa el proveedor MiniMax con las dos credenciales separadas que fijó la review.

Los dos medidores

Medidor Endpoint Credencial
Cuota del plan GET {base}/v1/token_plan/remains Subscription Key del plan M/Token
Saldo PAYG GET {base}/account/query_balance API key de pago por uso

Son claves distintas en la consola de MiniMax y cada una va a su constante: secrets::MINIMAX_SUBSCRIPTION_KEY y secrets::MINIMAX_API_KEY. Sin la segunda, el proveedor funciona igual y solo desaparece el saldo.

base_resp: HTTP 200 no significa éxito

Es el punto que pediste validar. MiniMax devuelve HTTP 200 con el fallo dentro del sobre base_resp. Sin chequearlo, una Subscription Key caducada produce un informe vacío que parece correcto — peor que un fallo visible, porque el usuario ve un orbe en cero y asume que consumió nada.

envelope() es estricto en las tres direcciones:

  • status_code == 0 → éxito.
  • status_code != 0 → error, propagando status_msg cuando viene.
  • Sobre ausente, null o sin código → no se afirma ni éxito ni fallo: el caso con código ausente se acepta, y la ausencia total se reporta como error porque un token_plan/remains sin sobre no es un payload que sabemos leer.

Cubierto por rejects_a_200_carrying_a_failed_base_resp, que ejercita justo el status_code: 1004 con HTTP 200.

El porcentaje va de usage / total, nunca de remains

Según MiniMax-AI/cli#104, current_interval_usage_count es el consumo y current_interval_remains_count es lo que queda. Usar el segundo invierte el anillo. El fixture usa usage: 2500 / total: 10000 con remains: 7500 presentes, así que un parser que cogiera el campo equivocado daría 75 % en vez de 25 % y el test lo detecta.

El saldo PAYG no es un anillo

No hay «total» contra el que medir un saldo, así que va como medidor con percent: None. No añade un anillo ni puede disparar un aviso: verifiqué que la UI ya soporta ese caso — ProviderGauge.tsx:57, ProviderDetails.tsx:111, ProviderCard.tsx:80, alerts.ts:58 y format.ts:82 filtran los medidores sin porcentaje en todos los caminos. Por eso no hace falta tocar esa lógica.

Y su fallo no degrada el informe del plan: a_missing_or_unusable_balance_leaves_the_plan_report_intact lo comprueba con saldo ausente, mal formado y con available_amount nulo, y en los tres casos el anillo del plan sigue intacto.

Regiones

MiniMaxConfig.base_url con default api.minimax.io, saneado igual que el de NaN (vacío → default, y se quita la barra final). Ajustes menciona la región de China en minimaxi.com al explicar de dónde sacar cada clave.

Alcance

Kind::Subscription, logo SVG en src/assets/logos/, sección de Ajustes con los dos campos y botón de comprobar, widget-refresh.ts, alias de logo para el nodo minimax-cn de 9Router, y sin cuentas adicionales: MiniMax no tiene un concepto de sesión por cuenta que valga el modelo de accounts, así que fetch_account queda fuera a propósito.

Verificación

  • cargo test --lib: 252 pasan, incluidos los 6 nuevos, todos con fixtures deterministas y sin red.
  • npm test: 51 suites / 657 tests, exit 0. npm run typecheck limpio.
  • docs/i18n-inventory.md regenerado y --check en verde (+15 strings de React, +11 de Rust).

Dos cosas que quiero que revises

  1. Los nombres de campo vienen de integraciones de terceros, no de un payload mío. No tengo una Subscription Key real para validar contra la API en vivo. El parser es deliberadamente tolerante (acepta importes como string o número, total cero o ausente no produce anillo, marcas de tiempo opcionales), pero un payload real podría usar nombres distintos. Si me pasás una clave o un ejemplo de la respuesta, ajusto los fixtures a la realidad.
  2. El SVG es una marca geométrica, no el logo oficial. Dibujé una «M» limpia con currentColor para que rinda bien en el orbe. Si preferís el asset oficial de marca, lo cambio en un commit encima.

Y el punto que dejé abierto en #168 sigue igual: esto no toca el cuadro negro de Wayland, que sigue siendo el problema de renderizado WebKitGTK que estamos midiendo por separado.

Ref egdev6#165

- `GET {base}/v1/token_plan/remains` con la **Subscription Key** del plan M/Token.
- `GET {base}/account/query_balance` con la **API key de pago por uso**, en
  lectura best-effort: su fallo deja intacto el informe del plan.

Son dos credenciales distintas en la consola de MiniMax y cada medidor usa la
suya: `secrets::MINIMAX_SUBSCRIPTION_KEY` y `secrets::MINIMAX_API_KEY`.

**MiniMax responde HTTP 200 con el fallo dentro de `base_resp`.** Sin chequear
el sobre, una clave caducada produce un informe vacío que parece correcto, que
es peor que un fallo visible. `envelope()` es estricto: sobre ausente o vacío
es error, `status_code` distinto de 0 propaga `status_msg`, y un sobre sin
código no se interpreta como éxito ni como fallo.

El porcentaje sale de `current_interval_usage_count / current_interval_total_count`.
Según MiniMax-AI/cli#104, `usage_count` es el **consumo** y
`current_interval_remains_count` lo que queda: usarlo invertiría el anillo. Un
test lo fija con un payload donde los dos valores son distintos.

El saldo PAYG se emite como medidor con `percent: None`, porque un saldo no es un
porcentaje consumido y no hay total contra el que medirlo. La UI ya lo soporta:
`ProviderGauge`, `ProviderDetails`, `ProviderCard`, `alerts` y `format` filtran
los medidores sin porcentaje, así que no añade un anillo ni dispara avisos.

`MiniMaxConfig.base_url` cubre las dos regiones (global `api.minimax.io`, China
`api.minimaxi.com`), saneado igual que el de NaN.

Tests deterministas con fixtures, sin red: porcentaje, `base_resp` fallido con
HTTP 200, saldo sin degradar el plan, importes como string o número, total cero,
y ventana con su reinicio.
El header graba `git rev-parse HEAD:src`, así que el inventario solo puede
generarse después de commitear el código. +15 strings de React (sección de
Ajustes) y +11 de Rust (minimax.rs).

@egdev6 egdev6 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisión hecha — la arquitectura está bien resuelta: el chequeo estricto de base_resp (HTTP 200 no significa éxito), el porcentaje desde usage / total con fixture que cazaría un parser que cogiera remains, el saldo PAYG como medidor sin anillo que no degrada el informe del plan, y el cableado completo del patrón de proveedores (secretos, dataKey, alias minimax-cn, Ajustes). Nada de esto cambia.

Para aprobar necesito estos tres ajustes — los dos primeros son de dos líneas, el tercero es solo formato:

  1. Indicar el saldo no disponible en el mensaje. Hoy, si el usuario configura MINIMAX_API_KEY pero query_balance falla (clave inválida, host de región equivocado), no hay ningún indicio: ni medidor ni nota ni error. Es best-effort y el plan no se degrada — correcto — pero el usuario no puede distinguir "no configuro el saldo" de "el saldo falla". Propongo un apéndice en el message cuando la API key existe y el balance vino None, por ejemplo "· saldo no disponible". Un test que lo fije y listo.

  2. Clamp del porcentaje a 100. Si un payload real reporta current_interval_usage_count > current_interval_total_count (dato sucio, ciclo mal cerrado), el anillo va por encima de 100 %. Si la UI ya satura el anillo, este punto se puede cerrar solo con un test que lo fije; si no, un .min(100.0) en parse lo resuelve gratis. Dime cuál de las dos y lo ajustamos.

  3. Formato: línea minimax: mal indentada en el Default de config.rs (4 espacios en vez de 8) y falta el newline final en minimax.rs y minimax.svg. Un cargo fmt + newline.

Con eso, mi aprobación. El riesgo real que señalás en la descripción — nombres de campo no validados contra un payload vivo — me parece bien mitigado con la tolerancia del parser; si algún día llega una clave o un ejemplo real, los fixtures se ajustan encima sin cambios de diseño.

Verificado contra la documentación oficial tras revisar la spec.

**Host.** El curl de la FAQ oficial usa `https://www.minimax.io/v1/token_plan/remains`,
con `www.`, no `api.minimax.io`. El default pasa a `www.minimax.io` y el
comentario de las regiones China (`www.minimaxi.com`) queda alineado.

**Segunda ventana.** El Token Plan tiene dos: una de 5 horas
(`current_interval_*`) y otra semanal (`current_weekly_*`), ambas citadas en el
FAQ. El parser solo leía la de 5 h, así que el anillo se mantenía tranquilo
mientras la semana iba al límite. Ahora emite las dos ventanas con reinicio y
duración propias, y el mensaje del popup resume la peor.

**Semántica de los contadores.** Estos campos cambiaron de significado al
migrar de endpoint: en `/coding_plan/remains`, `current_interval_usage_count`
era lo que quedaba (MiniMax-AI/MiniMax-M2#99); en `/v1/token_plan/remains` es
el consumo (MiniMax-AI/cli#104, «`current_interval_usage_count` is used count,
not remaining»). Aquí se usa la lectura del endpoint actual. El comentario en
el parser deja constancia de las dos, porque volver a confundirlas invierte el
anillo en silencio.

**Nombre de modelo.** No aplica: el plan es una barra de uso compartida y no
expone contadores por modelo, así que el proveedor no depende de si la línea
es M3 o M3.1 Flash.
@egdev6

egdev6 commented Oct 22, 2026

Copy link
Copy Markdown
Owner

@Rafaeldelinares revisé los dos commits nuevos (eccbcee, cb3510c).

Las dos ventanas del plan: bien resuelto

Ventana de 5 h + semanal como medidores independientes con la tabla WINDOWS, y el mensaje resumiendo la ventana peor — con una sola, el anillo mentiría cuando la de 5 h va sobrada y la semana va al límite. El fixture corregido a 18_000 s también arregla la incoherencia del mes. Bien.

Bloqueante nuevo: el cambio de base_url por defecto

El default pasó de https://api.minimax.io a https://www.minimax.io con el comentario del host de la documentación. El proveedor llama {base}/v1/token_plan/remains y {base}/account/query_balance — son endpoints de API, y la API de MiniMax históricamente vive en api.minimax.io, no en el host web www. Si el default apunta al host equivocado, todo usuario sin override recibirá HTML o 404 en vez de JSON, y el parser fallará con «respuesta sin base_resp». Ningún test detecta esto porque todo va con fixtures. Hay que confirmarlo contra la documentación (o una respuesta real) antes de seguir; si el endpoint de verdad cuelga de www, perfecto, pero queda documentado en el código por qué.

Pendientes de la review anterior — seguimos sin aprobar hasta resolverlos

  1. "Saldo no disponible" en el message: si MINIMAX_API_KEY está configurada y query_balance falla, hoy no hay ningún indicio. Con el nuevo message de combinaciones sigue sin distinguirse "sin configurar" de "configurado y fallido". Con test.
  2. Clamp del porcentaje a 100 (o test que fije que la UI satura si ya lo hace).
  3. Formato: indentación de minimax: en el Default de config.rs y newline final en minimax.rs y minimax.svg (los commits nuevos añaden más líneas al archivo sin newline final).

Sugerencia: resolvamos primero la duda del host — condiciona el default que verá todo el mundo — y luego los tres puntos. Mientras no estén, la PR se mantiene en cambios solicitados.

Responde a la review del PR egdev6#172.

**Host (bloqueante).** Verificado empíricamente contra la API: `api.` y `www.`
de ambas regiones responden el mismo JSON con el sobre `base_resp`, así que los
dos sirven y ninguno devuelve HTML. Se vuelve al default `api.minimax.io` por
semántica —es el host de la API, no el sitio web— y el comentario en `config.rs`
deja constancia de la verificación y de que el `curl` oficial usa `www.`, que
también responde.

El mismo sondeo confirma que `query_balance` es real: responde
`{"base_resp":{"status_code":1004,"status_msg":"not authorized"}}` en vez de un
404, y `token_plan/remains` devuelve **HTTP 200 con `status_code` 1004**, que es
justo el caso que justifica el chequeo estricto del sobre.

**Saldo no disponible.** `parse` recibe si hay API key configurada, para que el
usuario distinga «no configuré el saldo» de «lo configuré y falló». Sin la clave
no se dice nada; con la clave y el saldo caído, el mensaje acaba en
«· saldo no disponible». Test que fija los tres casos.

**Saturación a 100.** El anillo ya saturaba en `ProviderGauge` y `RingGauge`,
pero el valor crudo escapaba por `formatPercent` (que no satura), por
`history.rs` —que lo persiste— y por el payload de alertas. El clamp va en
`parse`: `used` y `limit` conservan el dato crudo y solo el porcentaje se
satura. Test que fija un payload con `usage > total`.

**Formato.** Corregida la indentación de la línea `minimax:` en el `Default` de
`config.rs` (las vecinas van a 12 espacios) y añadidos los newlines finales que
faltaban. No se corre `cargo fmt` porque el repo no lo cumple de base: son más de
400 archivos con el mismo patrón de líneas largas, incluido el resto de
proveedores, y formatear solo este lo haría inconsistente con los demás.
@Rafaeldelinares

Copy link
Copy Markdown
Contributor Author

@D6 Resueltos los cuatro puntos en dff4c41. Dos verificaciones queocumentedan las decisiones.

Host: sondeado, no estimado

Tu objeción era correcta en el riesgo: un default mal puesto rompe a todo el mundo y ningún test lo detecta. Así que lo comprobé contra la API en vez de discutirlo. GET sin autenticar a los cuatro hosts:

URL Estado Cuerpo
www.minimax.io/v1/token_plan/remains 200 JSON, base_resp 1004
api.minimax.io/v1/token_plan/remains 200 JSON, base_resp 1004
www.minimaxi.com/v1/token_plan/remains 200 JSON, base_resp 1004
api.minimaxi.com/v1/token_plan/remains 200 JSON, base_resp 1004
api.minimax.io/account/query_balance 400 JSON, base_resp 1004
www.minimax.io/account/query_balance 400 JSON, base_resp 1004

Conclusión: api. y www. sirven la API en ambas regiones. Ninguno devuelve HTML ni 404, así que el escenario que planteabas no ocurre.

aun así vuelvo a api.minimax.io como default, porque es el host de la API por nombre y www. es el sitio web que además la proxya. Tu intuición era la correcta; lo que cambió fue que el argumento («es que la API vive en api., no en www.») resultó innecesario: los dos funcionan. El comentario en config.rs deja constancia del sondeo y de que el curl oficial usa www., por si alguien lo copia tal cual desde Ajustes.

Bonus del mismo sondeo: confirma que query_balance es real — responde base_resp en vez de 404 — y que token_plan/remains devuelve HTTP 200 con status_code 1004. Eso es exactamente el caso que justifica el chequeo estricto del sobre, verificado contra el servidor y no solo contra fixtures.

Clamp: la UI saturaba, pero no bastaba

A tu pregunta de cuál de las dos: el anillo sí saturaba (ProviderGauge.tsx:97, RingGauge.tsx:39), pero el valor crudo se escapaba por tres caminos más:

  • formatPercent() no satura → el popup mostraría «150%».
  • history.rs:77 persiste el crudo → un payload sucio queda grabado y se dibuja para siempre.
  • alerts.ts:59 lo mete en el payload del evento.

Así que el clamp va en parse, que cierra los cuatro. used y limit conservan el dato crudo y solo el porcentaje se satura, para no perder información. Test con usage > total.

Saldo no disponible

parse recibe ahora si hay API key configurada. Sin clave no se dice nada (el usuario no pidió saldo); con clave y saldo caído, el mensaje termina en «· saldo no disponible». Test con los tres casos.

Formato: lo que nombraste sí, cargo fmt no

Corregidos los dos puntos concretos: la línea minimax: del Default (estaba en 4 espacios; sus vecinas van a 12) y los newlines finales de minimax.rs y minimax.svg.

No corrí cargo fmt a propósito: el repo no lo cumple de base, son más de 400 archivos con el mismo patrón de líneas largas, incluido el resto de proveedores (openrouter.rs, cursor.rs, claude.rs…). Formatear solo minimax.rs lo haría inconsistente con sus nueve hermanos, y formatear el repo convertiría esta PR en un diff Giant que esconde el proveedor. Si prefieres que lo pase, lo hago como PR aparte de formato.

cargo test --lib 255 en verde (9 de MiniMax), npm test 657, typecheck e inventario i18n en verde.

@Rafaeldelinares

Copy link
Copy Markdown
Contributor Author

Rectificación de dos erratas en mi comentario anterior: donde dice «Aun así», es «Aun así»; y donde dice «los dos funcionan». El resto de los datos del sondeo y los enlaces son correctos. Lo dejo anotado por si se copió antes de leerlo.

@egdev6

egdev6 commented Oct 22, 2026

Copy link
Copy Markdown
Owner

@Rafaeldelinares he revisado el PR completo y reproducido la verificación en local. El backend es sólido y los tests son buenos, pero hay 3 fallos de integración que impiden el merge tal cual — los tres son cambios pequeños.

✅ Verificación reproducida

Check Resultado
cargo test --lib 255 passed, 0 failed — los 9 tests de minimax pasan
vitest run --dir src 657 passed (coincide con lo declarado)
tsc --noEmit Limpio
i18n-inventory --check ✅ exit 0
cargo clippy ⚠️ 2 avisos nuevos en minimax.rs (baseline 18 → 20, ver menores)

🔴 Bloqueante 1: minimax falta en CONFIG_KEY — el toggle de visibilidad no hace nada

src/lib/settings-visibility.ts no está en el PR. En setAccountVisibility, para un proveedor principal sin entrada en CONFIG_KEY:

if (!Object.prototype.hasOwnProperty.call(CONFIG_KEY, id)) return cfg;

→ el toggle «Mostrar en el widget» de la pestaña MiniMax (Settings.tsx:1949) descarta el cambio en silencio. Es exactamente el mismo bug que @barbatdev encontró y corrigió en #174 para grok. Falta minimax: 'minimax' en CONFIG_KEY (y el tipo MainKey).

🔴 Bloqueante 2: minimax.baseUrl no tiene campo en ninguna parte de la UI

El hint de Ajustes dice «cambia también el host en Ajustes avanzados», pero no existe ningún input para cfg.minimax.baseUrl en Settings.tsx (solo hay inputs para nan y nineRouter). El usuario de la región China no puede pasar a api.minimaxi.com desde la UI — tendría que editar el JSON de config a mano. O se añade el campo, o el hint es una promesa falsa.

🟠 Importante: sin guard de medidores vacíos

Si llega HTTP 200 + base_resp OK (o sin status_code, que envelope acepta) pero los nombres de campo no se reconocen, parse devuelve 0 medidores y sin mensaje — el informe queda silenciosamente vacío. Todos los demás proveedores tienen ese guard (claude.rs:231, codex.rs:93, cursor.rs:65, gemini.rs:67, nan.rs:74) y grok.rs también. Es exactamente el modo de fallo que la descripción dice querer evitar («el usuario ve un orbe en cero y asume que consumió nada»). Falta el equivalente a if report.meters.is_empty() → fail(Warning, …).

🟡 Menores

  1. Clippy: 2 avisos nuevos en minimax.rs — needless_borrow en la línea 47 (&base) y manual Option::map en la 199.
  2. Typos en comentarios: «hides que la semana», «ycreer», «no publishes saldo» (mezcla inglés/castellano).
  3. Conteos de la descripción: 252 vs 255 observados; «6 nuevos» vs 9 tests en el módulo. Trivial.

👍 Puntos fuertes

  • base_resp bien resuelto: HTTP 200 con fallo interno es lo más tramposo de MiniMax y el sobre se maneja estricto en las tres direcciones, con test específico del status_code: 1004.
  • usage/total y no remains: la inversión del anillo está bien documentada (fix: use correct token_plan/remains endpoint MiniMax-AI/cli#104) y el fixture con remains: 7500 presente detectaría el error.
  • Dos ventanas (5 h + semanal) con mensaje de la peor — evita que la de 5 h esconda una semana al límite.
  • Saldo PAYG: sin anillo, best-effort, no degrada el plan, y distingue «no configurado» de «configurado y falló» — con tests de los tres caminos.
  • Saturación >100 %, división por cero, importes string|número, timestamps opcionales — todo cubierto por tests.
  • Secrets: dos claves separadas en ALL y for_provider; widget-refresh actualizado con test; alias de logo para el nodo minimax-cn de 9Router.

Veredicto: con los 3 fallos de integración corregidos (CONFIG_KEY, campo baseUrl en UI, guard de medidores vacíos) estaría listo para merge.

El parser anterior leía `current_interval_usage_count`, `current_interval_total_count`
y las marcas de tiempo **en la raíz** de la respuesta. Con un payload real esos
campos no existen ahí: el parser habría devuelto **cero medidores en todas las
llamadas**, y todos los tests pasaban igual porque los fixtures eran inventados.

Evidencia — `GET https://api.minimax.io/v1/token_plan/remains` con una
Subscription Key válida (`sk-cp-…`, región global):

```
raíz: model_remains, base_resp
raíz tiene current_interval_usage_count? False
raíz tiene current_interval_total_count?  False
raíz tiene start_time?                    False
raíz tiene base_resp?                    True
modelos: ['general', 'video']
```

Tres diferencias que el parser anterior no contemplaba:

**1. Los datos viven en `model_remains[]`, y hay uno por categoría de modelo.**
`general` y `video` consumen cuotas distintas: no es una barra compartida como
supuse en la descripción del PR. El id del medidor lleva ahora el modelo
(`token_plan_general_5h`) porque el popup y el historial los tienen que
distinguir.

**2. El campo fiable es `current_*_remaining_percent`, un porcentaje restante.**
Los contadores `_usage_count` / `_total_count` existen pero no sirven: en
`general` vienen a `usage=0, total=0` mientras el porcentaje restante dice 78.
Dividir uno por otro daría 0 % con la cuota ya consumida. El porcentaje ahora
sale de `100 - remaining_percent`, saturado.

**3. Los tiempos van en milisegundos.** `end_time = 1792699200000` es
`2026-10-22T20:00:00Z`. Interpretado como segundos caería en el año 58.000.

Los tests se reescriben contra este payload real (recortado a los campos que usa
el parser) e incluyen los que rompen cada una de las tres suposiciones: si el
parser volviera a usar los contadores a cero, a leer la raíz o a tratar los
tiempos como segundos, fallan.
**1. `CONFIG_KEY` sin `minimax`.** En `setAccountVisibility`, para un proveedor
principal sin entrada en `CONFIG_KEY`:

```ts
if (!Object.prototype.hasOwnProperty.call(CONFIG_KEY, id)) return cfg;
```

El toggle «Mostrar en el widget» **descartaba el cambio en silencio**: la UI lo
reflejaba y al reiniciar volvía al valor anterior. Añadido a `CONFIG_KEY` y al
tipo `MainKey`.

**2. `minimax.baseUrl` no tenía campo en ninguna parte de la UI**, pero el hint
de la Subscription Key prometía «cambia también el host en Ajustes avanzados».
Era una promesa falsa: el usuario de la región de China tenía que editar el JSON
a mano. Añadido el campo en un bloque «Avanzado», con el aviso de que las claves
son por región —lo verificado en el sondeo de hosts, donde una clave global da
`2049 invalid api key` en `api.minimaxi.com`— y los errores concretos a los que
apuntar si el host no es el de la región.

**3. Guard de medidores vacíos.** Si MiniMax renombra los campos, `parse`
devolvía cero medidores y sin mensaje: un orbe en cero que el usuario lee como
«no he consumido», que es justo lo que la descripción dice querer evitar. Ahora
es un `Status::Warning` explícito. Mismo patrón que `claude.rs`, `codex.rs`,
`cursor.rs`, `gemini.rs` y `nan.rs`.
- `needless_borrow` en la llamada a `balance(http, &base, …)`: `base` ya es
  `&str`.
- `timestamp()` queda sin uso tras pasar los tiempos a milisegundos, y se elimina.
- Typos en comentarios: «hides» → «esconde», «ycreer», y «no publishes saldo» →
  «no publica el saldo».

`cargo clippy --lib --tests` queda en **0 avisos** atribuibles a `minimax.rs`.
@Rafaeldelinares

Copy link
Copy Markdown
Contributor Author

@D6 Tres commits sobre tu review, en el orden que pediste. El primero cambia el diseño del parser, así que empieza por ese.

El parser anterior no habría funcionado nunca

Tenía una Subscription Key válida a mano, así que consulté la API antes de seguir. El parser leía current_interval_usage_count, current_interval_total_count y las marcas de tiempo en la raíz. Ahí no hay nada:

raíz: model_remains, base_resp
raíz tiene current_interval_usage_count? False
raíz tiene current_interval_total_count?  False
raíz tiene start_time?                    False
raíz tiene base_resp?                    True
modelos: ['general', 'video']

Habría devuelto cero medidores en todas las llamadas, con los tests en verde porque los fixtures eran inventados. Es exactamente el riesgo que marqué en la descripción del PR, y este commit lo cierra con la evidencia que pediste.

Tres suposiciones que resultaron falsas:

  1. No es una barra compartida. Es model_remains[], una entrada por categoría de modelo. general y video consumen cuotas distintas, así que los ids llevan el modelo (token_plan_general_5h) porque el popup y el historial los tienen que separar. Mi texto en la descripción sobre la cuota compartida era incorrecto.

  2. El campo fiable es el porcentaje restante. current_*_remaining_percent. Los contadores existen pero no sirven:

    general: usage=0 total=0  pero remaining_percent=78
    

    Dividir usage/total daba 0 % con la cuota ya consumida. Ahora sale de 100 - remaining_percent, saturado.

  3. Los tiempos van en milisegundos. end_time = 1792699200000 → 2026-10-22T20:00:00Z. Como segundos, año 58.000.

Los tests reescritos contra ese payload real, y cada suposición tiene el suyo: si el parser vuelve a la raíz, a los contadores o a los segundos, fallan.

Lo que sí acertó tu review: base_resp viene también en las respuestas de éxito, el chequeo estricto era el camino correcto, y la separación Subscription Key / API key era correcta tal cual la describiste.

Los tres bloqueantes, resueltos

CONFIG_KEY — minimax: 'minimax' añadido al mapa y al tipo MainKey. El toggle ya no descarta el cambio en silencio. (Nota: grok tampoco está, así que el #174 tiene el mismo punto.)

Campo de host — añadido el input de minimax.baseUrl en un bloque «Avanzado», con el aviso de que las claves son por región. Lo verificado en el sondeo: la clave que probé es global, y contra api.minimaxi.com devuelve 2049 invalid api key.

Guard de medidores vacíos — Status::Warning explícito si parse no reconoce los campos, con el patrón de claude.rs, codex.rs, cursor.rs, gemini.rs y nan.rs.

Menores

Clippy a 0 avisos en minimax.rs (needless_borrow corregido y timestamp() eliminado al pasar a milisegundos), typos fuera, y la descripción del PR la actualizo para que no afirme lo de la cuota compartida.

cargo test --lib   253
cargo clippy       0 avisos en minimax.rs
npm test           658
tsc --noEmit       limpio
i18n --check       exit 0

Una duda de criterio antes de que revises: con model_remains por categoría, general y video generan cuatro medidores (2 categorías × 2 ventanas). ¿Los cuatro, o solo general y dejamos video fuera? Es tu decisión de qué le sirve al usuario del widget.

@egdev6

egdev6 commented Oct 22, 2026

Copy link
Copy Markdown
Owner

@Rafaeldelinares he revisado los tres commits (358695b, bbb4db3, d492efa) y reproducido la verificación sobre la cabeza actual del PR.

✅ Verificación reproducida

Check Resultado
cargo test --lib 253 passed, 0 failed (coincide con lo declarado)
cargo clippy --all-targets 0 avisos en minimax.rs (los 2 anteriores, corregidos)
vitest run --dir src 658 passed (coincide)
tsc --noEmit Limpio
i18n-inventory --check ✅ exit 0 (inventario regenerado con hashes nuevos)

🔴→✅ Los tres bloqueantes, bien resueltos

  1. CONFIG_KEY: minimax: 'minimax' añadido al mapa y a MainKey. El toggle vuelve a funcionar. (Tu nota sobre grok es correcta en main — feat(providers): provider SuperGrok vía Grok CLI #174 lo cubre.)
  2. Campo de host: input de minimax.baseUrl en el bloque «Avanzado · URL del API» con el aviso de claves por región, cableado con el set genérico → dataKey incluye c.minimax, así que el widget refresca al cambiarlo.
  3. Guard de medidores vacíos: if parsed.meters.is_empty() && parsed.message.is_none() → fail(Warning, …) — mismo patrón que los otros proveedores.

🔴→✅ Rediseño del parser: verificado y convincente

El payload real (model_remains[] por categoría, remaining_percent restante, tiempos en ms) invalidaba el parser original — y lo dices con evidencia: los contadores de general vienen a 0/0 con 81 % restante, así que usage/total daba 0 % con la cuota consumida. La versión nueva:

  • Porcentaje desde 100 - current_*_remaining_percent, saturado en ambas direcciones (restante negativo → 100 %, restante >100 → 0 %) — testeado.
  • Tiempos en ms con test que detectaría la regresión a segundos (año 58.000).
  • Fixture = payload real recortado; los tests detectan las tres regresiones concretas (raíz, contadores, segundos).
  • Honesto en lo que no sabe: used/limit a None porque el payload no trae totales utilizables.
  • Ids dinámicos por modelo (token_plan_general_5h) — verifiqué meter-colors.ts, alerts.ts (dedupe por id), historial y React keys: todo va por id, así que los ids dinámicos son consistentes y estables por categoría.

🟡 Menores (no bloquean)

  1. Edge del guard: con model_remains ausente y PAYG configurado-y-caído, message es Some("saldo no disponible") → el guard no salta y el informe queda con 0 medidores de plan. Caso raro, aceptable; se podría separar el aviso de formato del de saldo.
  2. Typo en comentario del fixture: «válida,recortado» sin espacio; y el nombre del test saldo_payg_es_un_valor_y_nunca_una_anillo («una anillo» → «un anillo»). Triviales.

📌 Sobre tu pregunta (4 medidores vs solo general)

Deja los cuatro (2 categorías × 2 ventanas). Razones:

  • La cuota de video es distinta de la de general: ocultarla puede mentir justo cuando es la que limita (video al 100 %, general sobrado).
  • El anillo principal ya elige el peor medidor, así que el coste visual es solo en el detalle/popup, donde tener las cuatro es información, no ruido.
  • Si el widget llegara a sentirse cargado, el recorte correcto es de UI (colapsar en el popup), no del parser — perder los datos en Rust es irreversible, quitarlos de la vista es trivial después.

Veredicto: los tres bloqueantes están correctamente resueltos y el parser ahora refleja la API real con evidencia. Con los menores opcionales, lo apruebo para merge — con los 4 medidores según lo anterior.

@egdev6
egdev6 merged commit c2f7a12 into egdev6:main Oct 22, 2026
5 checks passed
@egdev6

egdev6 commented Oct 22, 2026

Copy link
Copy Markdown
Owner

@Rafaeldelinares un añadido menor del que no hace falta hacer PR aparte — cabe en este:

Edge del guard de medidores vacíos: si model_remains viene ausente y el PAYG está configurado pero caído, message es Some("saldo no disponible") → el guard is_empty() && message.is_none() no salta y el informe queda con 0 medidores de plan sin aviso de formato. Caso raro, pero la solución es barata: separar el aviso de formato del aviso de saldo (p. ej. que el saldo solo apareza en message si hay algún medidor de plan, o componer ambos avisos).

No bloquea el merge; solo para que no quede en el ruido.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants