@hostwebhook/platform-contracts 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,23 @@
1
1
  /**
2
2
  * Lo que cuesta un token, por modelo.
3
3
  *
4
- * Un solo módulo por ahora: `tabla-de-precios`. Está en su propia carpeta
5
- * porque lo que va a crecer aquí son las tarifas —contexto largo, audio,
6
- * lotes— y no las formas de contrato, que es de lo que va `servicios/`.
4
+ * Dos módulos, y la diferencia entre ellos es DE DÓNDE SALE EL PRECIO:
5
+ *
6
+ * - `tabla-de-precios`: la tabla que mantenemos a mano, contra la página de
7
+ * cada proveedor. Es la fuente para casi todo, y también donde vive el
8
+ * cálculo (`calculateCost`, `tarifaDelTramo`).
9
+ * - `tarifas-de-openrouter`: el precio que trae el propio proveedor. Sus
10
+ * ~425 modelos publican su tarifa junto a la lista, así que se convierten
11
+ * y se pasan por el parámetro `tarifa` de `calculateCost` en vez de
12
+ * copiarse a la tabla. Un proveedor futuro que también traiga su precio
13
+ * entra por aquí, con su propio conversor y el mismo parámetro.
14
+ *
15
+ * Están en su propia carpeta porque lo que va a crecer aquí son las tarifas
16
+ * —contexto largo, audio, lotes, proveedores que se tarifan solos— y no las
17
+ * formas de contrato, que es de lo que va `servicios/`.
7
18
  *
8
19
  * ⚠️ Aquí NO van los precios de venta ni los ids de Stripe. Ver la cabecera de
9
20
  * `tabla-de-precios.ts`: son cosas distintas que se llaman igual.
10
21
  */
11
22
  export * from './tabla-de-precios';
23
+ export * from './tarifas-de-openrouter';
@@ -17,11 +17,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  /**
18
18
  * Lo que cuesta un token, por modelo.
19
19
  *
20
- * Un solo módulo por ahora: `tabla-de-precios`. Está en su propia carpeta
21
- * porque lo que va a crecer aquí son las tarifas —contexto largo, audio,
22
- * lotes— y no las formas de contrato, que es de lo que va `servicios/`.
20
+ * Dos módulos, y la diferencia entre ellos es DE DÓNDE SALE EL PRECIO:
21
+ *
22
+ * - `tabla-de-precios`: la tabla que mantenemos a mano, contra la página de
23
+ * cada proveedor. Es la fuente para casi todo, y también donde vive el
24
+ * cálculo (`calculateCost`, `tarifaDelTramo`).
25
+ * - `tarifas-de-openrouter`: el precio que trae el propio proveedor. Sus
26
+ * ~425 modelos publican su tarifa junto a la lista, así que se convierten
27
+ * y se pasan por el parámetro `tarifa` de `calculateCost` en vez de
28
+ * copiarse a la tabla. Un proveedor futuro que también traiga su precio
29
+ * entra por aquí, con su propio conversor y el mismo parámetro.
30
+ *
31
+ * Están en su propia carpeta porque lo que va a crecer aquí son las tarifas
32
+ * —contexto largo, audio, lotes, proveedores que se tarifan solos— y no las
33
+ * formas de contrato, que es de lo que va `servicios/`.
23
34
  *
24
35
  * ⚠️ Aquí NO van los precios de venta ni los ids de Stripe. Ver la cabecera de
25
36
  * `tabla-de-precios.ts`: son cosas distintas que se llaman igual.
26
37
  */
27
38
  __exportStar(require("./tabla-de-precios"), exports);
39
+ __exportStar(require("./tarifas-de-openrouter"), exports);
@@ -47,6 +47,24 @@
47
47
  * Se conservan modelos retirados a propósito: las trazas históricas siguen
48
48
  * consultándose y tienen que seguir cuadrando.
49
49
  *
50
+ * ## Lo que esta tabla deliberadamente NO contiene: OpenRouter
51
+ *
52
+ * Sus ~425 modelos NO están aquí y no van a estarlo. No es un hueco: es que su
53
+ * API publica el precio DE CADA MODELO en la misma respuesta que la lista, así
54
+ * que copiarlo aquí sería mantener a mano lo que ya nos dan, y quedarse viejo
55
+ * en silencio 425 veces —que es el fallo del que va la sección de arriba—.
56
+ *
57
+ * Se cobran por el otro camino, el que abre el parámetro `tarifa` de
58
+ * `calculateCost` e `isModelPriced`: quien tiene la respuesta de OpenRouter la
59
+ * pasa por `tarifaDeOpenRouter` (fichero de al lado) y entrega el
60
+ * `ModelPricing` resultante. Mismo cálculo, mismos tramos, otro origen del
61
+ * dato.
62
+ *
63
+ * ⚠️ Y por eso el id de OpenRouter NUNCA se recorta para buscarlo aquí:
64
+ * `"anthropic/claude-sonnet-5"` no es `"claude-sonnet-5"`. Comprarle un
65
+ * modelo a un revendedor no cuesta lo que comprárselo al laboratorio, así que
66
+ * dejar que caiga en esta tabla cobraría el precio de otro trato.
67
+ *
50
68
  * ## Lo que esta tabla NO modela (aceptable para estimar coste)
51
69
  *
52
70
  * - (RESUELTO, ver `umbralEntrada`) Contexto largo. Ya se modela en los diez
@@ -160,8 +178,36 @@ export declare const MODEL_PRICING: Record<string, ModelPricing>;
160
178
  * llamante un número inventado con pinta de bueno.
161
179
  */
162
180
  export declare function findPricingKey(model: string): string | null;
163
- /** True cuando la tabla sabe de verdad cuánto vale este modelo. */
164
- export declare function isModelPriced(model: string): boolean;
181
+ /**
182
+ * True cuando sabemos de verdad cuánto vale este modelo.
183
+ *
184
+ * ## El segundo argumento: cuando el precio NO sale de la tabla
185
+ *
186
+ * Hay proveedores que traen su tarifa con el modelo —OpenRouter publica el
187
+ * precio de cada uno de sus ~425 en la misma respuesta que la lista—, y meter
188
+ * eso en `MODEL_PRICING` sería mantener a mano un dato que la API ya da. Para
189
+ * esos, quien llama trae la tarifa ya convertida y la pasa aquí.
190
+ *
191
+ * Los tres estados son distintos y hay que respetarlos:
192
+ *
193
+ * - `undefined` (no se pasa nada) → decide la tabla, como siempre. Ningún
194
+ * llamador de los de antes cambia de comportamiento.
195
+ * - un `ModelPricing` → **sí lo sabemos**, y lo sabemos mejor que la tabla:
196
+ * viene del proveedor. `true` sin mirar la tabla, aunque el modelo no esté
197
+ * —que es justo el caso: `"anthropic/claude-sonnet-5"` no está ni va a
198
+ * estar—.
199
+ * - `null` → se preguntó y **no se sabe**. `false`.
200
+ *
201
+ * ⚠️ Ese `null` no es adorno. `tarifaDeOpenRouter` devuelve `null` cuando el
202
+ * modelo no publica precio utilizable, siguiendo el mismo criterio que
203
+ * `findPricingKey`: antes decir «no lo sé» que devolver un número inventado.
204
+ * Encadenarlos —`isModelPriced(id, tarifaDeOpenRouter(m))`— conserva esa
205
+ * distinción de punta a punta. Convertir el `null` en `undefined` por el
206
+ * camino (un `?? undefined`, un `||`) la pierde: preguntaría a la tabla, la
207
+ * tabla tampoco lo sabría, y saldría el mismo `false` por casualidad hoy y no
208
+ * mañana.
209
+ */
210
+ export declare function isModelPriced(model: string, tarifa?: ModelPricing | null): boolean;
165
211
  /**
166
212
  * Coste en USD, redondeado a 6 decimales (precisión de micro-dólar).
167
213
  *
@@ -190,8 +236,30 @@ export declare function isModelPriced(model: string): boolean;
190
236
  * Antes se sumaba `reasoningTokens * precio_de_salida` ENCIMA de
191
237
  * `outputTokens` para openai y google: eso cobraba dos veces el mismo
192
238
  * pensamiento. El parámetro se conserva para no romper a quien ya lo pasa.
239
+ *
240
+ * ## `tarifa`: el precio que trae el proveedor
241
+ *
242
+ * Último y opcional, para que ningún llamador de los que ya existen cambie.
243
+ * Cuando se pasa, ES la tarifa: la tabla no se consulta y `model` sólo sirve
244
+ * ya para detectar el proveedor de los multiplicadores de caché. Es lo que
245
+ * usa OpenRouter, cuyos ~425 modelos publican su propio precio y por eso NO
246
+ * están en `MODEL_PRICING`; sirve igual para cualquier proveedor futuro que
247
+ * traiga el suyo.
248
+ *
249
+ * Los tres estados son los mismos que en `isModelPriced`: `undefined` decide
250
+ * la tabla, un objeto manda, y `null` significa «se preguntó y no se sabe» →
251
+ * coste 0, igual que un modelo ausente de la tabla, y con la misma trampa: 0
252
+ * no es gratis. Pregunta antes con `isModelPriced(model, tarifa)`.
253
+ *
254
+ * ⚠️ La tarifa que llega por parámetro pasa por `tarifaDelTramo` EXACTAMENTE
255
+ * igual que la de la tabla, y es a propósito que sea el mismo camino y no uno
256
+ * paralelo. Hoy nadie la usa con tramo —OpenRouter no publica umbrales, así
257
+ * que sus tarifas salen sin `umbralEntrada` y `tarifaDelTramo` las devuelve
258
+ * tal cual—, pero el día que un proveedor publique uno, funciona sin tocar
259
+ * nada. Un segundo camino sin tramos sería un acantilado que sólo se cobra por
260
+ * un lado.
193
261
  */
194
- export declare function calculateCost(model: string, inputTokens: number, outputTokens: number, cacheReadInputTokens?: number, cacheCreationInputTokens?: number, _reasoningTokens?: number): number;
262
+ export declare function calculateCost(model: string, inputTokens: number, outputTokens: number, cacheReadInputTokens?: number, cacheCreationInputTokens?: number, _reasoningTokens?: number, tarifa?: ModelPricing | null): number;
195
263
  /** Los modelos que la tabla sabe poner en precio, en orden de declaración. */
196
264
  export declare function getKnownModels(): string[];
197
265
  /**
@@ -48,6 +48,24 @@
48
48
  * Se conservan modelos retirados a propósito: las trazas históricas siguen
49
49
  * consultándose y tienen que seguir cuadrando.
50
50
  *
51
+ * ## Lo que esta tabla deliberadamente NO contiene: OpenRouter
52
+ *
53
+ * Sus ~425 modelos NO están aquí y no van a estarlo. No es un hueco: es que su
54
+ * API publica el precio DE CADA MODELO en la misma respuesta que la lista, así
55
+ * que copiarlo aquí sería mantener a mano lo que ya nos dan, y quedarse viejo
56
+ * en silencio 425 veces —que es el fallo del que va la sección de arriba—.
57
+ *
58
+ * Se cobran por el otro camino, el que abre el parámetro `tarifa` de
59
+ * `calculateCost` e `isModelPriced`: quien tiene la respuesta de OpenRouter la
60
+ * pasa por `tarifaDeOpenRouter` (fichero de al lado) y entrega el
61
+ * `ModelPricing` resultante. Mismo cálculo, mismos tramos, otro origen del
62
+ * dato.
63
+ *
64
+ * ⚠️ Y por eso el id de OpenRouter NUNCA se recorta para buscarlo aquí:
65
+ * `"anthropic/claude-sonnet-5"` no es `"claude-sonnet-5"`. Comprarle un
66
+ * modelo a un revendedor no cuesta lo que comprárselo al laboratorio, así que
67
+ * dejar que caiga en esta tabla cobraría el precio de otro trato.
68
+ *
51
69
  * ## Lo que esta tabla NO modela (aceptable para estimar coste)
52
70
  *
53
71
  * - (RESUELTO, ver `umbralEntrada`) Contexto largo. Ya se modela en los diez
@@ -306,8 +324,38 @@ function findPricingKey(model) {
306
324
  }
307
325
  return mejor;
308
326
  }
309
- /** True cuando la tabla sabe de verdad cuánto vale este modelo. */
310
- function isModelPriced(model) {
327
+ /**
328
+ * True cuando sabemos de verdad cuánto vale este modelo.
329
+ *
330
+ * ## El segundo argumento: cuando el precio NO sale de la tabla
331
+ *
332
+ * Hay proveedores que traen su tarifa con el modelo —OpenRouter publica el
333
+ * precio de cada uno de sus ~425 en la misma respuesta que la lista—, y meter
334
+ * eso en `MODEL_PRICING` sería mantener a mano un dato que la API ya da. Para
335
+ * esos, quien llama trae la tarifa ya convertida y la pasa aquí.
336
+ *
337
+ * Los tres estados son distintos y hay que respetarlos:
338
+ *
339
+ * - `undefined` (no se pasa nada) → decide la tabla, como siempre. Ningún
340
+ * llamador de los de antes cambia de comportamiento.
341
+ * - un `ModelPricing` → **sí lo sabemos**, y lo sabemos mejor que la tabla:
342
+ * viene del proveedor. `true` sin mirar la tabla, aunque el modelo no esté
343
+ * —que es justo el caso: `"anthropic/claude-sonnet-5"` no está ni va a
344
+ * estar—.
345
+ * - `null` → se preguntó y **no se sabe**. `false`.
346
+ *
347
+ * ⚠️ Ese `null` no es adorno. `tarifaDeOpenRouter` devuelve `null` cuando el
348
+ * modelo no publica precio utilizable, siguiendo el mismo criterio que
349
+ * `findPricingKey`: antes decir «no lo sé» que devolver un número inventado.
350
+ * Encadenarlos —`isModelPriced(id, tarifaDeOpenRouter(m))`— conserva esa
351
+ * distinción de punta a punta. Convertir el `null` en `undefined` por el
352
+ * camino (un `?? undefined`, un `||`) la pierde: preguntaría a la tabla, la
353
+ * tabla tampoco lo sabría, y saldría el mismo `false` por casualidad hoy y no
354
+ * mañana.
355
+ */
356
+ function isModelPriced(model, tarifa) {
357
+ if (tarifa !== undefined)
358
+ return tarifa !== null;
311
359
  return findPricingKey(model) !== null;
312
360
  }
313
361
  /* ── Coste ───────────────────────────────────────────────────────── */
@@ -339,13 +387,51 @@ function isModelPriced(model) {
339
387
  * Antes se sumaba `reasoningTokens * precio_de_salida` ENCIMA de
340
388
  * `outputTokens` para openai y google: eso cobraba dos veces el mismo
341
389
  * pensamiento. El parámetro se conserva para no romper a quien ya lo pasa.
390
+ *
391
+ * ## `tarifa`: el precio que trae el proveedor
392
+ *
393
+ * Último y opcional, para que ningún llamador de los que ya existen cambie.
394
+ * Cuando se pasa, ES la tarifa: la tabla no se consulta y `model` sólo sirve
395
+ * ya para detectar el proveedor de los multiplicadores de caché. Es lo que
396
+ * usa OpenRouter, cuyos ~425 modelos publican su propio precio y por eso NO
397
+ * están en `MODEL_PRICING`; sirve igual para cualquier proveedor futuro que
398
+ * traiga el suyo.
399
+ *
400
+ * Los tres estados son los mismos que en `isModelPriced`: `undefined` decide
401
+ * la tabla, un objeto manda, y `null` significa «se preguntó y no se sabe» →
402
+ * coste 0, igual que un modelo ausente de la tabla, y con la misma trampa: 0
403
+ * no es gratis. Pregunta antes con `isModelPriced(model, tarifa)`.
404
+ *
405
+ * ⚠️ La tarifa que llega por parámetro pasa por `tarifaDelTramo` EXACTAMENTE
406
+ * igual que la de la tabla, y es a propósito que sea el mismo camino y no uno
407
+ * paralelo. Hoy nadie la usa con tramo —OpenRouter no publica umbrales, así
408
+ * que sus tarifas salen sin `umbralEntrada` y `tarifaDelTramo` las devuelve
409
+ * tal cual—, pero el día que un proveedor publique uno, funciona sin tocar
410
+ * nada. Un segundo camino sin tramos sería un acantilado que sólo se cobra por
411
+ * un lado.
342
412
  */
343
- function calculateCost(model, inputTokens, outputTokens, cacheReadInputTokens = 0, cacheCreationInputTokens = 0, _reasoningTokens = 0) {
344
- const key = findPricingKey(model);
345
- if (!key)
346
- return 0;
347
- const pricing = exports.MODEL_PRICING[key];
348
- const provider = detectProvider(key);
413
+ function calculateCost(model, inputTokens, outputTokens, cacheReadInputTokens = 0, cacheCreationInputTokens = 0, _reasoningTokens = 0, tarifa) {
414
+ let pricing;
415
+ /* Sobre qué cadena se detecta el proveedor de los multiplicadores de caché:
416
+ con tabla, sobre la CLAVE que ganó (`claude-sonnet-4-6-20260101` resuelve
417
+ a `claude-sonnet-4-6`); con tarifa por parámetro no hay clave, así que
418
+ sobre el modelo pedido — que en OpenRouter lleva el laboratorio delante
419
+ (`anthropic/claude-sonnet-5`) y `detectProvider` reconoce igual. */
420
+ let paraDetectarProveedor;
421
+ if (tarifa !== undefined) {
422
+ if (tarifa === null)
423
+ return 0; // se preguntó y no se sabe
424
+ pricing = tarifa;
425
+ paraDetectarProveedor = model;
426
+ }
427
+ else {
428
+ const key = findPricingKey(model);
429
+ if (!key)
430
+ return 0;
431
+ pricing = exports.MODEL_PRICING[key];
432
+ paraDetectarProveedor = key;
433
+ }
434
+ const provider = detectProvider(paraDetectarProveedor);
349
435
  /**
350
436
  * Los tokens cacheados CUENTAN para decidir el tramo: fisicamente son el
351
437
  * prompt, y en un tope de gasto errar del lado caro es errar del lado seguro.
@@ -356,17 +442,23 @@ function calculateCost(model, inputTokens, outputTokens, cacheReadInputTokens =
356
442
  * hay que confirmarla antes de que alguien empiece a rellenarlos.
357
443
  */
358
444
  const entradaParaElTramo = inputTokens + cacheReadInputTokens + cacheCreationInputTokens;
359
- const tarifa = tarifaDelTramo(pricing, entradaParaElTramo);
360
- const inputCost = (inputTokens / 1000000) * tarifa.input;
361
- const outputCost = (outputTokens / 1000000) * tarifa.output;
445
+ /* `tarifaAplicada` y no `tarifa` a secas porque `tarifa` es ahora el
446
+ PARÁMETRO, y son dos cosas distintas a propósito: `tarifa` es lo que trajo
447
+ quien llama —o nada—, y esto es lo que se cobra de verdad tras decidir el
448
+ tramo. Las dos pasan por aquí, venga el precio de la tabla o de fuera. */
449
+ const tarifaAplicada = tarifaDelTramo(pricing, entradaParaElTramo);
450
+ const inputCost = (inputTokens / 1000000) * tarifaAplicada.input;
451
+ const outputCost = (outputTokens / 1000000) * tarifaAplicada.output;
362
452
  // Caché: multiplicadores por proveedor
363
453
  const cacheReadMul = CACHE_READ_MULTIPLIERS[provider] ?? 0.1;
364
454
  const cacheCreationMul = CACHE_CREATION_MULTIPLIERS[provider] ?? 0;
365
- // Sobre `tarifa.input`, no sobre `pricing.input`: en tramo alto la cache
366
- // tambien se encarece, que es lo que dice la tabla de Google —su fila de
367
- // cache esta partida por tamano de prompt igual que las otras dos.
368
- const cacheReadCost = (cacheReadInputTokens / 1000000) * tarifa.input * cacheReadMul;
369
- const cacheCreationCost = (cacheCreationInputTokens / 1000000) * tarifa.input * cacheCreationMul;
455
+ // Sobre `tarifaAplicada.input`, no sobre `pricing.input`: en tramo alto la
456
+ // cache tambien se encarece, que es lo que dice la tabla de Google —su fila
457
+ // de cache esta partida por tamano de prompt igual que las otras dos.
458
+ const cacheReadCost = (cacheReadInputTokens / 1000000) * tarifaAplicada.input * cacheReadMul;
459
+ const cacheCreationCost = (cacheCreationInputTokens / 1000000) *
460
+ tarifaAplicada.input *
461
+ cacheCreationMul;
370
462
  return (Math.round((inputCost + outputCost + cacheReadCost + cacheCreationCost) * 1000000) / 1000000);
371
463
  }
372
464
  /** Los modelos que la tabla sabe poner en precio, en orden de declaración. */
@@ -0,0 +1,99 @@
1
+ /**
2
+ * De lo que publica OpenRouter a un `ModelPricing` nuestro.
3
+ *
4
+ * ## Por qué hace falta convertir nada
5
+ *
6
+ * Porque las dos puntas miden distinto, y en las dos dimensiones a la vez:
7
+ *
8
+ * | | OpenRouter | `ModelPricing` |
9
+ * |-|-|-|
10
+ * | unidad | **por token** | **por millón de tokens** |
11
+ * | tipo | **cadena** | **número** |
12
+ *
13
+ * `{"prompt": "0.0000001"}` son $0,10 el millón. Un `parseFloat` sin el
14
+ * `* 1_000_000` cobra un millón de veces de menos, y como el resultado
15
+ * redondea a cero micro-dólares, un tope de gasto puesto encima no salta
16
+ * jamás. Es el mismo modo de fallo que un modelo ausente de la tabla: sale 0 y
17
+ * nadie se entera.
18
+ *
19
+ * ## La decisión que importa: `"0"` NO es lo mismo que ausente
20
+ *
21
+ * Los dos casos existen HOY, contados sobre la respuesta real (2026-08-31, 425
22
+ * modelos):
23
+ *
24
+ * - **21 modelos gratis de verdad**, los `:free`, con `pricing` a `"0"`. Eso
25
+ * es un precio CONOCIDO que resulta ser cero, y hay que poder cobrarlo como
26
+ * tal: `{ input: 0, output: 0 }`.
27
+ * - **5 modelos con `"-1"`**: `openrouter/auto`, `auto-beta`, `fusion`,
28
+ * `pareto-code` y `bodybuilder`. Son routers dentro del router — lo que
29
+ * cuesten depende de a dónde acaben enrutando, y OpenRouter lo dice con ese
30
+ * `-1`. Eso es **no lo sabemos**, y ahí se devuelve `null`.
31
+ *
32
+ * Un modelo sin `pricing`, con el campo a `null` o con basura no numérica cae
33
+ * también en `null`. Hoy no hay ninguno —los 425 traen `prompt` y `completion`—
34
+ * pero es una respuesta de red y nada nuestro lo garantiza mañana.
35
+ *
36
+ * El criterio es el que ya tiene `findPricingKey`: antes decir «no lo sé» que
37
+ * devolver un número inventado. Los dos casos acaban en un coste de 0 dólares
38
+ * si nadie mira, y significan lo contrario: «esta llamada no cuesta nada»
39
+ * frente a «esta llamada puede costar cualquier cosa». Quien ponga un tope
40
+ * tiene que preguntar con `isModelPriced(id, tarifa)`, que distingue el objeto
41
+ * del `null`, y no mirar el número.
42
+ *
43
+ * ⚠️ Un `-1` que pasara el filtro se convertiría en −$1.000.000 el millón: el
44
+ * gasto acumulado BAJARÍA al usarlo, y el tope se alejaría cuanto más se
45
+ * gastara. Cualquier cosa que no sea un número finito y ≥ 0 es desconocido.
46
+ *
47
+ * ## Lo que esta conversión NO trae, y se dice en vez de fingir
48
+ *
49
+ * - **La caché.** `input_cache_read`, `input_cache_write` y hasta
50
+ * `input_cache_write_1h` vienen en la respuesta y aquí se ignoran, porque
51
+ * `ModelPricing` no tiene dónde meterlos: el modelo de caché de
52
+ * `calculateCost` son multiplicadores por proveedor, no precios absolutos.
53
+ * Consecuencia concreta: a un modelo de OpenRouter la caché se le estima
54
+ * con el multiplicador que salga de `detectProvider` sobre su id, no con su
55
+ * precio real. Hoy es inerte —el emisor de trazas no rellena los campos de
56
+ * caché— pero deja de serlo el día que alguien los rellene. Cerrarlo es
57
+ * dar campos absolutos de caché a `ModelPricing`, no parchear esto.
58
+ * - **Tramos de contexto largo.** OpenRouter no publica umbrales, así que
59
+ * las tarifas salen SIN `umbralEntrada` y `tarifaDelTramo` las devuelve
60
+ * planas. No es que se ignore un tramo: es que no hay ninguno que leer.
61
+ * - **Lo que no se cobra por token**: `request` (precio por petición),
62
+ * `image`, `web_search`, `internal_reasoning`. Un modelo con `request` > 0
63
+ * sale INFRAVALORADO por lo que cueste la petición en sí.
64
+ */
65
+ import type { ModeloDeOpenRouter, PrecioDeOpenRouter } from '@hostwebhook/node-types';
66
+ import type { ModelPricing } from './tabla-de-precios';
67
+ /**
68
+ * La tarifa de un modelo de OpenRouter, o `null` cuando no se sabe.
69
+ *
70
+ * Acepta el modelo entero —que es lo que se tiene tras el fetch— o su bloque
71
+ * `pricing` suelto.
72
+ *
73
+ * ⚠️ Hacen falta LAS DOS mitades. Con `prompt` pero sin `completion` no se
74
+ * devuelve media tarifa poniendo la salida a 0: la salida es la cara —un 5x
75
+ * sobre la entrada es lo normal—, así que una tarifa a medias no infravalora
76
+ * un poco, infravalora justo por donde duele. O se sabe entero o no se sabe.
77
+ */
78
+ export declare function tarifaDeOpenRouter(modelo: ModeloDeOpenRouter | PrecioDeOpenRouter | null | undefined): ModelPricing | null;
79
+ /**
80
+ * Un índice `id → tarifa` a partir del catálogo entero.
81
+ *
82
+ * Para quien va a tarifar muchas llamadas contra una sola respuesta —el
83
+ * selector del dashboard, o un caché de proceso en la api— en vez de recorrer
84
+ * los 425 por cada llamada.
85
+ *
86
+ * ⚠️ Los modelos cuya tarifa no se conoce **NO entran en el mapa**, y esa
87
+ * ausencia es la información: `mapa.get(id)` devuelve `undefined`, que es
88
+ * precisamente lo que `calculateCost` interpreta como «mira la tabla» y NO
89
+ * como «no se sabe». Quien consulte este mapa tiene que convertir ese hueco en
90
+ * un `null` explícito antes de pasarlo:
91
+ *
92
+ * const tarifa = mapa.get(id) ?? null; // ← el `?? null` es obligatorio
93
+ * if (!isModelPriced(id, tarifa)) { … }
94
+ *
95
+ * Se deja así, y no metiendo `null`s en el mapa, porque un `Map` con valores
96
+ * nulos invita a `mapa.has(id)` como si fuera «lo sé», que es la pregunta
97
+ * equivocada.
98
+ */
99
+ export declare function tarifasDeOpenRouter(modelos: ModeloDeOpenRouter[] | null | undefined): Map<string, ModelPricing>;
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ /**
3
+ * De lo que publica OpenRouter a un `ModelPricing` nuestro.
4
+ *
5
+ * ## Por qué hace falta convertir nada
6
+ *
7
+ * Porque las dos puntas miden distinto, y en las dos dimensiones a la vez:
8
+ *
9
+ * | | OpenRouter | `ModelPricing` |
10
+ * |-|-|-|
11
+ * | unidad | **por token** | **por millón de tokens** |
12
+ * | tipo | **cadena** | **número** |
13
+ *
14
+ * `{"prompt": "0.0000001"}` son $0,10 el millón. Un `parseFloat` sin el
15
+ * `* 1_000_000` cobra un millón de veces de menos, y como el resultado
16
+ * redondea a cero micro-dólares, un tope de gasto puesto encima no salta
17
+ * jamás. Es el mismo modo de fallo que un modelo ausente de la tabla: sale 0 y
18
+ * nadie se entera.
19
+ *
20
+ * ## La decisión que importa: `"0"` NO es lo mismo que ausente
21
+ *
22
+ * Los dos casos existen HOY, contados sobre la respuesta real (2026-08-31, 425
23
+ * modelos):
24
+ *
25
+ * - **21 modelos gratis de verdad**, los `:free`, con `pricing` a `"0"`. Eso
26
+ * es un precio CONOCIDO que resulta ser cero, y hay que poder cobrarlo como
27
+ * tal: `{ input: 0, output: 0 }`.
28
+ * - **5 modelos con `"-1"`**: `openrouter/auto`, `auto-beta`, `fusion`,
29
+ * `pareto-code` y `bodybuilder`. Son routers dentro del router — lo que
30
+ * cuesten depende de a dónde acaben enrutando, y OpenRouter lo dice con ese
31
+ * `-1`. Eso es **no lo sabemos**, y ahí se devuelve `null`.
32
+ *
33
+ * Un modelo sin `pricing`, con el campo a `null` o con basura no numérica cae
34
+ * también en `null`. Hoy no hay ninguno —los 425 traen `prompt` y `completion`—
35
+ * pero es una respuesta de red y nada nuestro lo garantiza mañana.
36
+ *
37
+ * El criterio es el que ya tiene `findPricingKey`: antes decir «no lo sé» que
38
+ * devolver un número inventado. Los dos casos acaban en un coste de 0 dólares
39
+ * si nadie mira, y significan lo contrario: «esta llamada no cuesta nada»
40
+ * frente a «esta llamada puede costar cualquier cosa». Quien ponga un tope
41
+ * tiene que preguntar con `isModelPriced(id, tarifa)`, que distingue el objeto
42
+ * del `null`, y no mirar el número.
43
+ *
44
+ * ⚠️ Un `-1` que pasara el filtro se convertiría en −$1.000.000 el millón: el
45
+ * gasto acumulado BAJARÍA al usarlo, y el tope se alejaría cuanto más se
46
+ * gastara. Cualquier cosa que no sea un número finito y ≥ 0 es desconocido.
47
+ *
48
+ * ## Lo que esta conversión NO trae, y se dice en vez de fingir
49
+ *
50
+ * - **La caché.** `input_cache_read`, `input_cache_write` y hasta
51
+ * `input_cache_write_1h` vienen en la respuesta y aquí se ignoran, porque
52
+ * `ModelPricing` no tiene dónde meterlos: el modelo de caché de
53
+ * `calculateCost` son multiplicadores por proveedor, no precios absolutos.
54
+ * Consecuencia concreta: a un modelo de OpenRouter la caché se le estima
55
+ * con el multiplicador que salga de `detectProvider` sobre su id, no con su
56
+ * precio real. Hoy es inerte —el emisor de trazas no rellena los campos de
57
+ * caché— pero deja de serlo el día que alguien los rellene. Cerrarlo es
58
+ * dar campos absolutos de caché a `ModelPricing`, no parchear esto.
59
+ * - **Tramos de contexto largo.** OpenRouter no publica umbrales, así que
60
+ * las tarifas salen SIN `umbralEntrada` y `tarifaDelTramo` las devuelve
61
+ * planas. No es que se ignore un tramo: es que no hay ninguno que leer.
62
+ * - **Lo que no se cobra por token**: `request` (precio por petición),
63
+ * `image`, `web_search`, `internal_reasoning`. Un modelo con `request` > 0
64
+ * sale INFRAVALORADO por lo que cueste la petición en sí.
65
+ */
66
+ Object.defineProperty(exports, "__esModule", { value: true });
67
+ exports.tarifaDeOpenRouter = tarifaDeOpenRouter;
68
+ exports.tarifasDeOpenRouter = tarifasDeOpenRouter;
69
+ /** Tokens por unidad de `ModelPricing`. El nombre está para que el `* 1e6` de
70
+ * abajo no parezca una constante mágica: es exactamente este cambio de
71
+ * unidad, de por-token a por-millón. */
72
+ const TOKENS_POR_UNIDAD_DE_TARIFA = 1000000;
73
+ /**
74
+ * USD por millón a partir de un campo por token, o `null` si no se sabe.
75
+ *
76
+ * El redondeo a 9 decimales no es cosmético: `parseFloat('0.0000001') * 1e6`
77
+ * da `0.09999999999999999` en coma flotante, no `0.1`. Sin recortar, cada
78
+ * tarifa llegaría con una cola de basura que luego aparece en cualquier
79
+ * comparación y en cualquier importe mostrado. Nueve decimales borra la cola
80
+ * —que vive quince órdenes de magnitud más abajo— sin tocar el precio más
81
+ * ridículo que OpenRouter llegue a publicar.
82
+ */
83
+ function porMillon(valor) {
84
+ /* Se aceptan cadena y número. La API manda cadenas, pero un consumidor que
85
+ haya normalizado el JSON antes de llegar aquí no tiene por qué perder el
86
+ dato por eso. */
87
+ const crudo = typeof valor === 'string' ? Number.parseFloat(valor)
88
+ : typeof valor === 'number' ? valor
89
+ : NaN;
90
+ // NaN, Infinity y negativos: desconocido, no cero. Ver la cabecera.
91
+ if (!Number.isFinite(crudo) || crudo < 0)
92
+ return null;
93
+ const porMillonDeTokens = crudo * TOKENS_POR_UNIDAD_DE_TARIFA;
94
+ return Math.round(porMillonDeTokens * 1e9) / 1e9;
95
+ }
96
+ /**
97
+ * La tarifa de un modelo de OpenRouter, o `null` cuando no se sabe.
98
+ *
99
+ * Acepta el modelo entero —que es lo que se tiene tras el fetch— o su bloque
100
+ * `pricing` suelto.
101
+ *
102
+ * ⚠️ Hacen falta LAS DOS mitades. Con `prompt` pero sin `completion` no se
103
+ * devuelve media tarifa poniendo la salida a 0: la salida es la cara —un 5x
104
+ * sobre la entrada es lo normal—, así que una tarifa a medias no infravalora
105
+ * un poco, infravalora justo por donde duele. O se sabe entero o no se sabe.
106
+ */
107
+ function tarifaDeOpenRouter(modelo) {
108
+ if (!modelo || typeof modelo !== 'object')
109
+ return null;
110
+ const pricing = 'pricing' in modelo
111
+ ? modelo.pricing
112
+ : modelo;
113
+ if (!pricing || typeof pricing !== 'object')
114
+ return null;
115
+ const input = porMillon(pricing.prompt);
116
+ const output = porMillon(pricing.completion);
117
+ if (input === null || output === null)
118
+ return null;
119
+ /* Sin `umbralEntrada`: OpenRouter no publica tramos. Ver la cabecera. */
120
+ return { input, output };
121
+ }
122
+ /**
123
+ * Un índice `id → tarifa` a partir del catálogo entero.
124
+ *
125
+ * Para quien va a tarifar muchas llamadas contra una sola respuesta —el
126
+ * selector del dashboard, o un caché de proceso en la api— en vez de recorrer
127
+ * los 425 por cada llamada.
128
+ *
129
+ * ⚠️ Los modelos cuya tarifa no se conoce **NO entran en el mapa**, y esa
130
+ * ausencia es la información: `mapa.get(id)` devuelve `undefined`, que es
131
+ * precisamente lo que `calculateCost` interpreta como «mira la tabla» y NO
132
+ * como «no se sabe». Quien consulte este mapa tiene que convertir ese hueco en
133
+ * un `null` explícito antes de pasarlo:
134
+ *
135
+ * const tarifa = mapa.get(id) ?? null; // ← el `?? null` es obligatorio
136
+ * if (!isModelPriced(id, tarifa)) { … }
137
+ *
138
+ * Se deja así, y no metiendo `null`s en el mapa, porque un `Map` con valores
139
+ * nulos invita a `mapa.has(id)` como si fuera «lo sé», que es la pregunta
140
+ * equivocada.
141
+ */
142
+ function tarifasDeOpenRouter(modelos) {
143
+ const mapa = new Map();
144
+ if (!Array.isArray(modelos))
145
+ return mapa;
146
+ for (const modelo of modelos) {
147
+ if (!modelo || typeof modelo.id !== 'string' || modelo.id === '')
148
+ continue;
149
+ const tarifa = tarifaDeOpenRouter(modelo);
150
+ if (tarifa)
151
+ mapa.set(modelo.id, tarifa);
152
+ }
153
+ return mapa;
154
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/platform-contracts",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Contratos compartidos entre los servicios de HostWebhook: addons del plan, identidad interna, y las formas que cruzan una frontera de red",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,6 +26,6 @@
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@nestjs/common": ">=11.0.0 <12",
29
- "@hostwebhook/node-types": ">=1.67.0 <2"
29
+ "@hostwebhook/node-types": ">=1.68.0 <2"
30
30
  }
31
31
  }