@hostwebhook/platform-contracts 0.8.1 → 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 +132 -6
- package/dist/precios/tabla-de-precios.js +173 -25
- 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,11 +47,52 @@
|
|
|
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
|
-
* -
|
|
53
|
-
*
|
|
54
|
-
*
|
|
70
|
+
* - (RESUELTO, ver `umbralEntrada`) Contexto largo. Ya se modela en los diez
|
|
71
|
+
* modelos que lo publican: los siete de OpenAI 5.4/5.5/5.6 a partir de
|
|
72
|
+
* 272.000 tokens de entrada, y `gemini-3.1-pro` / `gemini-2.5-pro` a partir
|
|
73
|
+
* de 200.000. Es un ACANTILADO: la tarifa alta cubre la petición entera.
|
|
74
|
+
* - (!) Lo que del contexto largo SIGUE sin modelarse: en `gpt-5.5` y en
|
|
75
|
+
* `gpt-5.4` la ficha dice «for the full SESSION», no por petición. Basta que
|
|
76
|
+
* UNA llamada de la sesión cruce 272k para que toda la sesión vaya a tarifa
|
|
77
|
+
* alta. `calculateCost` es una función pura por llamada y no tiene noción de
|
|
78
|
+
* sesión: ahí INFRAVALORA, y se dice en vez de fingir que se modela.
|
|
79
|
+
* - ✅ Anthropic NO tiene tramos, y conviene dejarlo escrito en afirmativo:
|
|
80
|
+
* su doc dice que «Claude 4.6 and later models include the full 1M token
|
|
81
|
+
* context window at standard pricing». La regla que circula por terceros
|
|
82
|
+
* diciendo que Sonnet 4.5 cobra 2x/1.5x por encima de 200k es FALSA. No
|
|
83
|
+
* reintroducirla.
|
|
84
|
+
* - (!) `gpt-5.6-sol` (y su alias `gpt-5.6`) está a precio PROMOCIONAL: la
|
|
85
|
+
* propia tabla dice «available at least through November 21, 2026», y no
|
|
86
|
+
* publica a cuánto revierte. Un tope calibrado sobre 4.00/20.00 tiene fecha
|
|
87
|
+
* de caducidad documentada.
|
|
88
|
+
* - (!) Google factura los tokens de RAZONAMIENTO a precio de salida — el
|
|
89
|
+
* encabezado de su columna dice «Output price (including thinking tokens)».
|
|
90
|
+
* Quien multiplique sólo los tokens visibles por `output` se queda corto, y
|
|
91
|
+
* el razonamiento no se ve en la respuesta pero sí en la factura.
|
|
92
|
+
* - (!) Claude 4.7 en adelante usa un tokenizador que produce ~30% MÁS tokens
|
|
93
|
+
* para el mismo texto, y eso incluye Sonnet 5, Opus 5, Opus 4.8 y Opus 4.7,
|
|
94
|
+
* no sólo Fable 5. Sonnet 5 a $2 frente a Sonnet 4.6 a $3 parece un 33%
|
|
95
|
+
* más barato; con un 30% más de tokens el ahorro real se acerca a cero.
|
|
55
96
|
* - Audio: `gemini-2.5-flash` y `3.1-flash-lite` cobran más el audio que el
|
|
56
97
|
* texto. Aquí todo va a tarifa de texto.
|
|
57
98
|
* - Anthropic: `inference_geo` "us" multiplica por 1,1, y el modo rápido de
|
|
@@ -67,7 +108,42 @@
|
|
|
67
108
|
export interface ModelPricing {
|
|
68
109
|
input: number;
|
|
69
110
|
output: number;
|
|
111
|
+
/**
|
|
112
|
+
* Tokens de ENTRADA por encima de los cuales la petición cambia de tarifa.
|
|
113
|
+
* Ausente = tarifa plana en toda la ventana, que es el caso de Anthropic y de
|
|
114
|
+
* casi todo lo demás.
|
|
115
|
+
*
|
|
116
|
+
* El nombre dice qué se mide: los dos proveedores que tienen tramos lo miden
|
|
117
|
+
* sobre el PROMPT, nunca sobre entrada+salida. En Google la palabra literal de
|
|
118
|
+
* las tres filas de su tabla es «prompts».
|
|
119
|
+
*
|
|
120
|
+
* (!) La comparación es ESTRICTA: OpenAI escribe «>272K» y Google «> 200k».
|
|
121
|
+
* Exactamente 272.000 va a tarifa base.
|
|
122
|
+
*/
|
|
123
|
+
umbralEntrada?: number;
|
|
124
|
+
/**
|
|
125
|
+
* (!) NO es facturación marginal: es un ACANTILADO. La tarifa alta se aplica
|
|
126
|
+
* a TODOS los tokens de la petición, no sólo a los que exceden el umbral.
|
|
127
|
+
*
|
|
128
|
+
* Un prompt de 272.001 tokens en `gpt-5.5` se cobra ENTERO a $10, no 272.000
|
|
129
|
+
* a $5 más uno a $10. Cruzar el umbral por un token duplica la factura de
|
|
130
|
+
* entrada de esa llamada y encarece un 50 % su salida aunque la salida sean
|
|
131
|
+
* cincuenta tokens.
|
|
132
|
+
*/
|
|
133
|
+
inputLargo?: number;
|
|
134
|
+
outputLargo?: number;
|
|
70
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* La tarifa que toca, según cuánta entrada lleve la petición.
|
|
138
|
+
*
|
|
139
|
+
* Se saca aparte de `calculateCost` porque quien quiera estimar ANTES de llamar
|
|
140
|
+
* —la reserva del tope de gasto del Chat Trigger— necesita la misma decisión sin
|
|
141
|
+
* tener aún los tokens de salida.
|
|
142
|
+
*/
|
|
143
|
+
export declare function tarifaDelTramo(precio: ModelPricing, tokensDeEntrada: number): {
|
|
144
|
+
input: number;
|
|
145
|
+
output: number;
|
|
146
|
+
};
|
|
71
147
|
export declare const MODEL_PRICING: Record<string, ModelPricing>;
|
|
72
148
|
/**
|
|
73
149
|
* Qué entrada de la tabla pone precio a este modelo, o `null` cuando de verdad
|
|
@@ -102,8 +178,36 @@ export declare const MODEL_PRICING: Record<string, ModelPricing>;
|
|
|
102
178
|
* llamante un número inventado con pinta de bueno.
|
|
103
179
|
*/
|
|
104
180
|
export declare function findPricingKey(model: string): string | null;
|
|
105
|
-
/**
|
|
106
|
-
|
|
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;
|
|
107
211
|
/**
|
|
108
212
|
* Coste en USD, redondeado a 6 decimales (precisión de micro-dólar).
|
|
109
213
|
*
|
|
@@ -132,8 +236,30 @@ export declare function isModelPriced(model: string): boolean;
|
|
|
132
236
|
* Antes se sumaba `reasoningTokens * precio_de_salida` ENCIMA de
|
|
133
237
|
* `outputTokens` para openai y google: eso cobraba dos veces el mismo
|
|
134
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.
|
|
135
261
|
*/
|
|
136
|
-
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;
|
|
137
263
|
/** Los modelos que la tabla sabe poner en precio, en orden de declaración. */
|
|
138
264
|
export declare function getKnownModels(): string[];
|
|
139
265
|
/**
|
|
@@ -48,11 +48,52 @@
|
|
|
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
|
-
* -
|
|
54
|
-
*
|
|
55
|
-
*
|
|
71
|
+
* - (RESUELTO, ver `umbralEntrada`) Contexto largo. Ya se modela en los diez
|
|
72
|
+
* modelos que lo publican: los siete de OpenAI 5.4/5.5/5.6 a partir de
|
|
73
|
+
* 272.000 tokens de entrada, y `gemini-3.1-pro` / `gemini-2.5-pro` a partir
|
|
74
|
+
* de 200.000. Es un ACANTILADO: la tarifa alta cubre la petición entera.
|
|
75
|
+
* - (!) Lo que del contexto largo SIGUE sin modelarse: en `gpt-5.5` y en
|
|
76
|
+
* `gpt-5.4` la ficha dice «for the full SESSION», no por petición. Basta que
|
|
77
|
+
* UNA llamada de la sesión cruce 272k para que toda la sesión vaya a tarifa
|
|
78
|
+
* alta. `calculateCost` es una función pura por llamada y no tiene noción de
|
|
79
|
+
* sesión: ahí INFRAVALORA, y se dice en vez de fingir que se modela.
|
|
80
|
+
* - ✅ Anthropic NO tiene tramos, y conviene dejarlo escrito en afirmativo:
|
|
81
|
+
* su doc dice que «Claude 4.6 and later models include the full 1M token
|
|
82
|
+
* context window at standard pricing». La regla que circula por terceros
|
|
83
|
+
* diciendo que Sonnet 4.5 cobra 2x/1.5x por encima de 200k es FALSA. No
|
|
84
|
+
* reintroducirla.
|
|
85
|
+
* - (!) `gpt-5.6-sol` (y su alias `gpt-5.6`) está a precio PROMOCIONAL: la
|
|
86
|
+
* propia tabla dice «available at least through November 21, 2026», y no
|
|
87
|
+
* publica a cuánto revierte. Un tope calibrado sobre 4.00/20.00 tiene fecha
|
|
88
|
+
* de caducidad documentada.
|
|
89
|
+
* - (!) Google factura los tokens de RAZONAMIENTO a precio de salida — el
|
|
90
|
+
* encabezado de su columna dice «Output price (including thinking tokens)».
|
|
91
|
+
* Quien multiplique sólo los tokens visibles por `output` se queda corto, y
|
|
92
|
+
* el razonamiento no se ve en la respuesta pero sí en la factura.
|
|
93
|
+
* - (!) Claude 4.7 en adelante usa un tokenizador que produce ~30% MÁS tokens
|
|
94
|
+
* para el mismo texto, y eso incluye Sonnet 5, Opus 5, Opus 4.8 y Opus 4.7,
|
|
95
|
+
* no sólo Fable 5. Sonnet 5 a $2 frente a Sonnet 4.6 a $3 parece un 33%
|
|
96
|
+
* más barato; con un 30% más de tokens el ahorro real se acerca a cero.
|
|
56
97
|
* - Audio: `gemini-2.5-flash` y `3.1-flash-lite` cobran más el audio que el
|
|
57
98
|
* texto. Aquí todo va a tarifa de texto.
|
|
58
99
|
* - Anthropic: `inference_geo` "us" multiplica por 1,1, y el modo rápido de
|
|
@@ -67,25 +108,44 @@
|
|
|
67
108
|
*/
|
|
68
109
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
69
110
|
exports.MODEL_ALTERNATIVES = exports.MODEL_PRICING = void 0;
|
|
111
|
+
exports.tarifaDelTramo = tarifaDelTramo;
|
|
70
112
|
exports.findPricingKey = findPricingKey;
|
|
71
113
|
exports.isModelPriced = isModelPriced;
|
|
72
114
|
exports.calculateCost = calculateCost;
|
|
73
115
|
exports.getKnownModels = getKnownModels;
|
|
74
116
|
exports.getSuggestedAlternative = getSuggestedAlternative;
|
|
117
|
+
/**
|
|
118
|
+
* La tarifa que toca, según cuánta entrada lleve la petición.
|
|
119
|
+
*
|
|
120
|
+
* Se saca aparte de `calculateCost` porque quien quiera estimar ANTES de llamar
|
|
121
|
+
* —la reserva del tope de gasto del Chat Trigger— necesita la misma decisión sin
|
|
122
|
+
* tener aún los tokens de salida.
|
|
123
|
+
*/
|
|
124
|
+
function tarifaDelTramo(precio, tokensDeEntrada) {
|
|
125
|
+
if (precio.umbralEntrada === undefined) {
|
|
126
|
+
return { input: precio.input, output: precio.output };
|
|
127
|
+
}
|
|
128
|
+
if (tokensDeEntrada <= precio.umbralEntrada) {
|
|
129
|
+
return { input: precio.input, output: precio.output };
|
|
130
|
+
}
|
|
131
|
+
return { input: precio.inputLargo, output: precio.outputLargo };
|
|
132
|
+
}
|
|
75
133
|
exports.MODEL_PRICING = {
|
|
76
134
|
// OpenAI — GPT series
|
|
77
|
-
'gpt-5.6-sol': { input: 4.00, output: 20.00 },
|
|
135
|
+
'gpt-5.6-sol': { input: 4.00, output: 20.00, umbralEntrada: 272000, inputLargo: 8.00, outputLargo: 30.00 },
|
|
78
136
|
// `gpt-5.6` a secas es el alias documentado de Sol, y sin esta entrada caia
|
|
79
137
|
// por prefijo en `gpt-5` — otro modelo y otro precio.
|
|
80
|
-
'gpt-5.6': { input: 4.00, output: 20.00 },
|
|
81
|
-
'gpt-5.6-terra': { input: 2.00, output: 12.00 },
|
|
82
|
-
'gpt-5.6-luna': { input: 0.20, output: 1.20 },
|
|
83
|
-
'gpt-5.5-pro': { input: 30.00, output: 180.00 },
|
|
84
|
-
'gpt-5.5': { input: 5.00, output: 30.00 },
|
|
85
|
-
'gpt-5.4-pro': { input: 30.00, output: 180.00 },
|
|
86
|
-
'gpt-5.4': { input: 2.50, output: 15.00 },
|
|
138
|
+
'gpt-5.6': { input: 4.00, output: 20.00, umbralEntrada: 272000, inputLargo: 8.00, outputLargo: 30.00 },
|
|
139
|
+
'gpt-5.6-terra': { input: 2.00, output: 12.00, umbralEntrada: 272000, inputLargo: 4.00, outputLargo: 18.00 },
|
|
140
|
+
'gpt-5.6-luna': { input: 0.20, output: 1.20, umbralEntrada: 272000, inputLargo: 0.40, outputLargo: 1.80 },
|
|
141
|
+
'gpt-5.5-pro': { input: 30.00, output: 180.00, umbralEntrada: 272000, inputLargo: 60.00, outputLargo: 270.00 },
|
|
142
|
+
'gpt-5.5': { input: 5.00, output: 30.00, umbralEntrada: 272000, inputLargo: 10.00, outputLargo: 45.00 },
|
|
143
|
+
'gpt-5.4-pro': { input: 30.00, output: 180.00, umbralEntrada: 272000, inputLargo: 60.00, outputLargo: 270.00 },
|
|
144
|
+
'gpt-5.4': { input: 2.50, output: 15.00, umbralEntrada: 272000, inputLargo: 5.00, outputLargo: 22.50 },
|
|
87
145
|
'gpt-5.4-mini': { input: 0.75, output: 4.50 },
|
|
88
146
|
'gpt-5.4-nano': { input: 0.20, output: 1.25 },
|
|
147
|
+
// (!) SIN tramo, y es deliberado: es el unico *-pro de la familia 5.x cuya
|
|
148
|
+
// ficha no publica umbral de contexto largo. Verificado fila a fila.
|
|
89
149
|
'gpt-5.2-pro': { input: 21.00, output: 168.00 },
|
|
90
150
|
'gpt-5.2': { input: 1.75, output: 14.00 },
|
|
91
151
|
'gpt-5.1': { input: 1.25, output: 10.00 },
|
|
@@ -169,10 +229,10 @@ exports.MODEL_PRICING = {
|
|
|
169
229
|
'gemini-3.6-flash': { input: 0.75, output: 3.75 },
|
|
170
230
|
'gemini-3.5-flash-lite': { input: 0.30, output: 2.50 },
|
|
171
231
|
'gemini-3.5-flash': { input: 1.50, output: 9.00 },
|
|
172
|
-
'gemini-3.1-pro': { input: 2.00, output: 12.00 },
|
|
232
|
+
'gemini-3.1-pro': { input: 2.00, output: 12.00, umbralEntrada: 200000, inputLargo: 4.00, outputLargo: 18.00 },
|
|
173
233
|
'gemini-3.1-flash-lite': { input: 0.25, output: 1.50 },
|
|
174
234
|
'gemini-3-flash': { input: 0.50, output: 3.00 },
|
|
175
|
-
'gemini-2.5-pro': { input: 1.25, output: 10.00 },
|
|
235
|
+
'gemini-2.5-pro': { input: 1.25, output: 10.00, umbralEntrada: 200000, inputLargo: 2.50, outputLargo: 15.00 },
|
|
176
236
|
'gemini-2.5-flash': { input: 0.30, output: 2.50 },
|
|
177
237
|
'gemini-2.5-flash-lite': { input: 0.10, output: 0.40 },
|
|
178
238
|
'gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
@@ -264,8 +324,38 @@ function findPricingKey(model) {
|
|
|
264
324
|
}
|
|
265
325
|
return mejor;
|
|
266
326
|
}
|
|
267
|
-
/**
|
|
268
|
-
|
|
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;
|
|
269
359
|
return findPricingKey(model) !== null;
|
|
270
360
|
}
|
|
271
361
|
/* ── Coste ───────────────────────────────────────────────────────── */
|
|
@@ -297,20 +387,78 @@ function isModelPriced(model) {
|
|
|
297
387
|
* Antes se sumaba `reasoningTokens * precio_de_salida` ENCIMA de
|
|
298
388
|
* `outputTokens` para openai y google: eso cobraba dos veces el mismo
|
|
299
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.
|
|
300
412
|
*/
|
|
301
|
-
function calculateCost(model, inputTokens, outputTokens, cacheReadInputTokens = 0, cacheCreationInputTokens = 0, _reasoningTokens = 0) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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);
|
|
435
|
+
/**
|
|
436
|
+
* Los tokens cacheados CUENTAN para decidir el tramo: fisicamente son el
|
|
437
|
+
* prompt, y en un tope de gasto errar del lado caro es errar del lado seguro.
|
|
438
|
+
*
|
|
439
|
+
* (!) Ninguno de los dos proveedores lo dice explicitamente, asi que es una
|
|
440
|
+
* decision nuestra, no una cita. Hoy es INERTE —los llamadores pasan tres
|
|
441
|
+
* argumentos y el emisor de trazas nunca rellena los campos de cache—, pero
|
|
442
|
+
* hay que confirmarla antes de que alguien empiece a rellenarlos.
|
|
443
|
+
*/
|
|
444
|
+
const entradaParaElTramo = inputTokens + cacheReadInputTokens + cacheCreationInputTokens;
|
|
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;
|
|
309
452
|
// Caché: multiplicadores por proveedor
|
|
310
453
|
const cacheReadMul = CACHE_READ_MULTIPLIERS[provider] ?? 0.1;
|
|
311
454
|
const cacheCreationMul = CACHE_CREATION_MULTIPLIERS[provider] ?? 0;
|
|
312
|
-
|
|
313
|
-
|
|
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;
|
|
314
462
|
return (Math.round((inputCost + outputCost + cacheReadCost + cacheCreationCost) * 1000000) / 1000000);
|
|
315
463
|
}
|
|
316
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
|
}
|