@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.
- package/dist/precios/index.d.ts +15 -3
- package/dist/precios/index.js +15 -3
- package/dist/precios/tabla-de-precios.d.ts +71 -3
- package/dist/precios/tabla-de-precios.js +108 -16
- package/dist/precios/tarifas-de-openrouter.d.ts +99 -0
- package/dist/precios/tarifas-de-openrouter.js +154 -0
- package/package.json +2 -2
package/dist/precios/index.d.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lo que cuesta un token, por modelo.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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';
|
package/dist/precios/index.js
CHANGED
|
@@ -17,11 +17,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
/**
|
|
18
18
|
* Lo que cuesta un token, por modelo.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
/**
|
|
164
|
-
|
|
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
|
-
/**
|
|
310
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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 `
|
|
366
|
-
// tambien se encarece, que es lo que dice la tabla de Google —su fila
|
|
367
|
-
// cache esta partida por tamano de prompt igual que las otras dos.
|
|
368
|
-
const cacheReadCost = (cacheReadInputTokens / 1000000) *
|
|
369
|
-
const cacheCreationCost = (cacheCreationInputTokens / 1000000) *
|
|
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.
|
|
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.
|
|
29
|
+
"@hostwebhook/node-types": ">=1.68.0 <2"
|
|
30
30
|
}
|
|
31
31
|
}
|