@hostwebhook/platform-contracts 0.6.0 → 0.8.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/README.md CHANGED
@@ -72,10 +72,55 @@ nadie que la ate al otro lado es una lista que vuelve a divergir — que es
72
72
  exactamente de donde se venía: había seis copias de los operadores de filtro y
73
73
  ninguna igual a otra.
74
74
 
75
+ ## Precios de los modelos
76
+
77
+ Lo que los proveedores de LLM cobran por millón de tokens. Entrada y salida se
78
+ tarifan **por separado**, con precios que suelen llevar un 5x entre ellos.
79
+
80
+ ```ts
81
+ import {
82
+ calculateCost,
83
+ findPricingKey,
84
+ isModelPriced,
85
+ MODEL_PRICING,
86
+ } from '@hostwebhook/platform-contracts';
87
+
88
+ calculateCost('claude-sonnet-4-6', 1_000_000, 1_000_000); // → 18 USD
89
+ findPricingKey('claude-sonnet-4-6-20260101'); // → 'claude-sonnet-4-6'
90
+ findPricingKey('modelo-que-no-existe'); // → null
91
+ ```
92
+
93
+ ⚠️ **`null` no es cero.** `findPricingKey` devuelve `null` para un modelo que la
94
+ tabla no conoce, y entonces `calculateCost` da `0`. Ese 0 significa «no sé
95
+ cuánto vale», no «es gratis». Quien vaya a decidir algo con el número —un tope
96
+ de gasto, una factura, una pantalla— tiene que preguntar antes con
97
+ `isModelPriced()`. Tratar el 0 como gratis deja pasar sin límite justo los
98
+ modelos recién salidos, que son los caros.
99
+
100
+ La resolución **no adivina**: sólo vale una clave de la tabla que sea prefijo
101
+ del modelo pedido, y hasta un separador. Así `claude-sonnet-4-6-20260101`
102
+ encuentra su base, y `claude-opus-9` no hereda la tarifa de un vecino.
103
+
104
+ ### 🔴 Hay dos copias de esta tabla
105
+
106
+ Ésta es la fuente de verdad desde el 2026-08-31.
107
+ `hw-llm-traces/src/common/utils/pricing.ts` es la copia que hay que retirar; el
108
+ PR que la hace consumir este paquete va en ese repo y todavía no está hecho.
109
+ Mientras tanto, un cambio de tarifa se hace aquí **y** se copia allí.
110
+
111
+ ### Por qué aquí y no en la api
112
+
113
+ Los topes de gasto del Chat Trigger se comprueban en el camino caliente del
114
+ chat, una vez por turno. Preguntar la tarifa por red a `hw-llm-traces` metería
115
+ latencia en cada turno y, si ese servicio está caído o sin configurar, el tope
116
+ se caería **abierto** — que es lo mismo que no tenerlo.
117
+
75
118
  ## Qué no va aquí
76
119
 
77
- - Precios e ids de Stripe: viven en la configuración de la api, que es la única
78
- que habla con Stripe. Cambiarlos no puede obligar a publicar un paquete.
120
+ - Precios **de venta** e ids de Stripe: viven en la configuración de la api, que
121
+ es la única que habla con Stripe. Cambiarlos no puede obligar a publicar un
122
+ paquete. (No confundir con `precios/`, que es lo que los proveedores de LLM
123
+ nos cobran a nosotros — ver arriba.)
79
124
  - Límites por plan: `plan.constants.ts` en la api.
80
125
  - Etiquetas de pantalla: cómo se llama un operador para el usuario es cosa del
81
126
  dashboard (`lib/operadores.ts`). Aquí van las claves, no los textos.
package/dist/index.d.ts CHANGED
@@ -6,9 +6,17 @@
6
6
  * escribe dos veces, un día se escribe distinto.
7
7
  *
8
8
  * Lo que NO entra: nada específico de un servicio, nada con secretos, y nada
9
- * cuyo cambio deba poder desplegarse sin publicar un paquete (precios, ids de
10
- * Stripe, límites por plan).
9
+ * cuyo cambio deba poder desplegarse sin publicar un paquete (precios de
10
+ * VENTA, ids de Stripe, límites por plan).
11
+ *
12
+ * ⚠️ «Precios» ahí arriba significa lo que HostWebhook le COBRA a un cliente.
13
+ * `precios/` es la otra cosa que se llama igual: lo que los proveedores de LLM
14
+ * NOS cobran a nosotros por token. Entra porque no es una palanca comercial
15
+ * sino un dato de entrada para un cálculo, y porque la api lo necesita en el
16
+ * camino caliente del chat, donde no puede ir a preguntárselo a otro servicio
17
+ * por red. La cabecera de `precios/tabla-de-precios.ts` lo cuenta entero.
11
18
  */
12
19
  export * from './addons';
13
20
  export * from './operadores';
21
+ export * from './precios';
14
22
  export * from './servicios';
package/dist/index.js CHANGED
@@ -22,9 +22,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
22
22
  * escribe dos veces, un día se escribe distinto.
23
23
  *
24
24
  * Lo que NO entra: nada específico de un servicio, nada con secretos, y nada
25
- * cuyo cambio deba poder desplegarse sin publicar un paquete (precios, ids de
26
- * Stripe, límites por plan).
25
+ * cuyo cambio deba poder desplegarse sin publicar un paquete (precios de
26
+ * VENTA, ids de Stripe, límites por plan).
27
+ *
28
+ * ⚠️ «Precios» ahí arriba significa lo que HostWebhook le COBRA a un cliente.
29
+ * `precios/` es la otra cosa que se llama igual: lo que los proveedores de LLM
30
+ * NOS cobran a nosotros por token. Entra porque no es una palanca comercial
31
+ * sino un dato de entrada para un cálculo, y porque la api lo necesita en el
32
+ * camino caliente del chat, donde no puede ir a preguntárselo a otro servicio
33
+ * por red. La cabecera de `precios/tabla-de-precios.ts` lo cuenta entero.
27
34
  */
28
35
  __exportStar(require("./addons"), exports);
29
36
  __exportStar(require("./operadores"), exports);
37
+ __exportStar(require("./precios"), exports);
30
38
  __exportStar(require("./servicios"), exports);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Lo que cuesta un token, por modelo.
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/`.
7
+ *
8
+ * ⚠️ Aquí NO van los precios de venta ni los ids de Stripe. Ver la cabecera de
9
+ * `tabla-de-precios.ts`: son cosas distintas que se llaman igual.
10
+ */
11
+ export * from './tabla-de-precios';
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ /**
18
+ * Lo que cuesta un token, por modelo.
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/`.
23
+ *
24
+ * ⚠️ Aquí NO van los precios de venta ni los ids de Stripe. Ver la cabecera de
25
+ * `tabla-de-precios.ts`: son cosas distintas que se llaman igual.
26
+ */
27
+ __exportStar(require("./tabla-de-precios"), exports);
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Cuánto cuesta un millón de tokens, por modelo y en USD.
3
+ *
4
+ * ## 🔴 Ésta es la FUENTE DE VERDAD desde el 2026-08-31
5
+ *
6
+ * La tabla nació en `hw-llm-traces/src/common/utils/pricing.ts`. Esa copia
7
+ * **hay que retirarla**: mientras siga ahí existen dos tablas, y dos tablas de
8
+ * lo mismo divergen —es literalmente el fallo que este paquete existe para
9
+ * evitar—. El PR que hace que `hw-llm-traces` importe de aquí y borre su
10
+ * fichero va en ESE repo y todavía no está hecho.
11
+ *
12
+ * Hasta que lo esté: **cualquier cambio de tarifa se hace aquí y se copia
13
+ * allí**, no al revés.
14
+ *
15
+ * ## Por qué esto vive en un paquete y no en la api
16
+ *
17
+ * Porque la api va a poner topes de gasto en dólares al Chat Trigger, y eso
18
+ * ocurre en el camino caliente del chat, una vez por turno. Preguntárselo por
19
+ * red a `hw-llm-traces` metería latencia en cada turno, y peor: si el servicio
20
+ * de trazas está caído o sin configurar, el tope se caería ABIERTO. Un tope de
21
+ * gasto que falla abierto no es un tope.
22
+ *
23
+ * Es el mismo motivo por el que las listas de operadores están en este
24
+ * paquete y no pegadas a su evaluador: lo necesitan los dos lados y no puede
25
+ * escribirse dos veces.
26
+ *
27
+ * ⚠️ Ojo con la regla de al lado, que se parece y NO es ésta: los precios de
28
+ * VENTA y los ids de Stripe —lo que HostWebhook le cobra a un cliente— siguen
29
+ * fuera, en la configuración de la api. Esto de aquí es lo que los proveedores
30
+ * de LLM nos cobran a nosotros: es un dato de entrada para un cálculo, no una
31
+ * palanca comercial que haya que poder mover sin publicar.
32
+ *
33
+ * ## (!) ESTA TABLA CADUCA
34
+ *
35
+ * Última verificación: 2026-08-26, contra las páginas oficiales:
36
+ * OpenAI https://developers.openai.com/api/docs/pricing
37
+ * Anthropic https://platform.claude.com/docs/en/about-claude/pricing
38
+ * Google https://ai.google.dev/gemini-api/docs/pricing
39
+ *
40
+ * La versión anterior llevaba parada desde el 2026-04-18 —y el servicio sin
41
+ * redesplegar desde entonces—, así que para agosto le faltaban familias
42
+ * enteras: gpt-5.5, gpt-5.6, Opus 5, Sonnet 5, Gemini 3.5/3.6/3.7. Un modelo
43
+ * ausente ya no se cobra con la tarifa de un vecino —`findPricingKey` se niega
44
+ * a adivinar— pero sale a coste CERO y con clave `null`: revisa esta tabla
45
+ * cuando veas trazas sin precio.
46
+ *
47
+ * Se conservan modelos retirados a propósito: las trazas históricas siguen
48
+ * consultándose y tienen que seguir cuadrando.
49
+ *
50
+ * ## Lo que esta tabla NO modela (aceptable para estimar coste)
51
+ *
52
+ * - Contexto largo: `gemini-3.1-pro` y `gemini-2.5-pro` casi doblan tarifa
53
+ * por encima de 200k tokens de entrada. Se usa siempre la base, así que
54
+ * INFRAVALORA los prompts muy largos.
55
+ * - Audio: `gemini-2.5-flash` y `3.1-flash-lite` cobran más el audio que el
56
+ * texto. Aquí todo va a tarifa de texto.
57
+ * - Anthropic: `inference_geo` "us" multiplica por 1,1, y el modo rápido de
58
+ * Opus 5 / 4.8 cuesta $10/$50. No se modelan: HostWebhook no los usa.
59
+ * - Descuento del 50% de las APIs por lotes. Nadie lo usa todavía.
60
+ * - (!) Claude 4.7 en adelante usa un tokenizador nuevo que produce ~30% MÁS
61
+ * tokens para el mismo texto. El precio por token no cambia, pero la
62
+ * factura por la misma conversación sí.
63
+ *
64
+ * ⚠️ Quien ponga un tope de gasto sobre esto tiene que saber que INFRAVALORA
65
+ * en los casos de arriba. El tope real queda por encima del nominal.
66
+ */
67
+ export interface ModelPricing {
68
+ input: number;
69
+ output: number;
70
+ }
71
+ export declare const MODEL_PRICING: Record<string, ModelPricing>;
72
+ /**
73
+ * Qué entrada de la tabla pone precio a este modelo, o `null` cuando de verdad
74
+ * no lo sabemos.
75
+ *
76
+ * ## Por qué la regla es «la clave conocida es PREFIJO del modelo pedido»
77
+ *
78
+ * La versión anterior iba al revés: recortaba el modelo pedido y buscaba la
79
+ * primera clave que empezara por ese trozo. Dos fallos, los dos MEDIDOS
80
+ * ejecutando la función con 1M de entrada y 1M de salida:
81
+ *
82
+ * - `gpt-5.4-turbo-2026` recortaba a `gpt-5.4` y `find()` devolvía la PRIMERA
83
+ * clave declarada que empieza así —`gpt-5.4-pro`, a $30/$180—. Cobraba $210
84
+ * donde la base `gpt-5.4` cuesta $17.50. Doce veces de más, y el ORDEN DE
85
+ * DECLARACIÓN del objeto decidía la factura.
86
+ * - `claude-opus-5` recortaba a `claude-opus` y cobraba $30 con la tarifa de
87
+ * `claude-opus-4-7`. Un modelo ausente salía con un precio inventado y de
88
+ * aspecto plausible.
89
+ *
90
+ * La dirección correcta es la contraria: sólo vale una clave que sea prefijo
91
+ * DEL MODELO PEDIDO, y gana la más larga. Eso cubre el caso real —un snapshot
92
+ * con fecha, `claude-sonnet-4-6-20260101`— y se niega a adivinar cuando de
93
+ * verdad es un modelo nuevo. Mejor decir «no lo sé» que cobrar la tarifa de
94
+ * otro.
95
+ *
96
+ * La frontera importa: `gpt-4` NO puede prestar su precio a `gpt-45-turbo`,
97
+ * así que el carácter siguiente tiene que ser un separador.
98
+ *
99
+ * ⚠️ Ese `null` es INFORMACIÓN, no un hueco que rellenar. Hay código aguas
100
+ * abajo que necesita distinguir «gratis» de «no sé cuánto vale» —un tope de
101
+ * gasto, sin ir más lejos—. Quien lo «mejore» adivinando le devuelve al
102
+ * llamante un número inventado con pinta de bueno.
103
+ */
104
+ export declare function findPricingKey(model: string): string | null;
105
+ /** True cuando la tabla sabe de verdad cuánto vale este modelo. */
106
+ export declare function isModelPriced(model: string): boolean;
107
+ /**
108
+ * Coste en USD, redondeado a 6 decimales (precisión de micro-dólar).
109
+ *
110
+ * ⚠️ Devuelve 0 para modelos que no sabemos poner en precio: usa
111
+ * `isModelPriced()` o `findPricingKey()` para distinguir «gratis» de «no lo
112
+ * sé» ANTES de enseñarle un 0 a una persona —o de dejar pasar un turno de chat
113
+ * porque su coste «no llega al tope»—.
114
+ *
115
+ * ⚠️ Entrada y salida se tarifan POR SEPARADO, con precios distintos y a
116
+ * menudo con un factor de 5x entre ellos. Sumar tokens y multiplicar por un
117
+ * precio único da un número que no es el de nadie.
118
+ *
119
+ * ## (!) `outputTokens` YA incluye el razonamiento
120
+ *
121
+ * `reasoningTokens` es INFORMATIVO y no se cobra aparte, porque los dos
122
+ * proveedores que lo reportan ya lo tienen dentro de la salida:
123
+ *
124
+ * - OpenAI: "reasoning tokens ... are billed as output tokens";
125
+ * `completion_tokens_details.reasoning_tokens` es un desglose de
126
+ * `completion_tokens`, no un extra.
127
+ * - Google: `thoughtsTokenCount` va aparte de `candidatesTokenCount` en el
128
+ * JSON, así que quien emite tiene que sumarlos ANTES de mandar
129
+ * `outputTokens` — es lo que hace HostWebhook desde el arreglo del conteo
130
+ * de Gemini.
131
+ *
132
+ * Antes se sumaba `reasoningTokens * precio_de_salida` ENCIMA de
133
+ * `outputTokens` para openai y google: eso cobraba dos veces el mismo
134
+ * pensamiento. El parámetro se conserva para no romper a quien ya lo pasa.
135
+ */
136
+ export declare function calculateCost(model: string, inputTokens: number, outputTokens: number, cacheReadInputTokens?: number, cacheCreationInputTokens?: number, _reasoningTokens?: number): number;
137
+ /** Los modelos que la tabla sabe poner en precio, en orden de declaración. */
138
+ export declare function getKnownModels(): string[];
139
+ /**
140
+ * De un modelo caro a otro más barato de capacidad parecida.
141
+ *
142
+ * ⚠️ Es un mapa a mano y NO cubre toda la tabla: un modelo que no está aquí
143
+ * simplemente no tiene sugerencia. No es la lista de modelos válidos —esa es
144
+ * `MODEL_PRICING`—.
145
+ */
146
+ export declare const MODEL_ALTERNATIVES: Record<string, string>;
147
+ /**
148
+ * La alternativa más barata sugerida para este modelo y lo que se ahorraría,
149
+ * o `null` cuando no hay sugerencia.
150
+ *
151
+ * ⚠️ Busca por el nombre EXACTO en `MODEL_ALTERNATIVES` —no usa
152
+ * `findPricingKey`—, así que un snapshot con fecha no encuentra sugerencia
153
+ * aunque su modelo base sí la tenga. Se conserva tal cual: cambiarlo alteraría
154
+ * qué se le sugiere hoy a la gente.
155
+ */
156
+ export declare function getSuggestedAlternative(model: string, inputTokens: number, outputTokens: number): {
157
+ alternativeModel: string;
158
+ currentCost: number;
159
+ alternativeCost: number;
160
+ savingsUsd: number;
161
+ savingsPercent: number;
162
+ } | null;
@@ -0,0 +1,363 @@
1
+ "use strict";
2
+ /**
3
+ * Cuánto cuesta un millón de tokens, por modelo y en USD.
4
+ *
5
+ * ## 🔴 Ésta es la FUENTE DE VERDAD desde el 2026-08-31
6
+ *
7
+ * La tabla nació en `hw-llm-traces/src/common/utils/pricing.ts`. Esa copia
8
+ * **hay que retirarla**: mientras siga ahí existen dos tablas, y dos tablas de
9
+ * lo mismo divergen —es literalmente el fallo que este paquete existe para
10
+ * evitar—. El PR que hace que `hw-llm-traces` importe de aquí y borre su
11
+ * fichero va en ESE repo y todavía no está hecho.
12
+ *
13
+ * Hasta que lo esté: **cualquier cambio de tarifa se hace aquí y se copia
14
+ * allí**, no al revés.
15
+ *
16
+ * ## Por qué esto vive en un paquete y no en la api
17
+ *
18
+ * Porque la api va a poner topes de gasto en dólares al Chat Trigger, y eso
19
+ * ocurre en el camino caliente del chat, una vez por turno. Preguntárselo por
20
+ * red a `hw-llm-traces` metería latencia en cada turno, y peor: si el servicio
21
+ * de trazas está caído o sin configurar, el tope se caería ABIERTO. Un tope de
22
+ * gasto que falla abierto no es un tope.
23
+ *
24
+ * Es el mismo motivo por el que las listas de operadores están en este
25
+ * paquete y no pegadas a su evaluador: lo necesitan los dos lados y no puede
26
+ * escribirse dos veces.
27
+ *
28
+ * ⚠️ Ojo con la regla de al lado, que se parece y NO es ésta: los precios de
29
+ * VENTA y los ids de Stripe —lo que HostWebhook le cobra a un cliente— siguen
30
+ * fuera, en la configuración de la api. Esto de aquí es lo que los proveedores
31
+ * de LLM nos cobran a nosotros: es un dato de entrada para un cálculo, no una
32
+ * palanca comercial que haya que poder mover sin publicar.
33
+ *
34
+ * ## (!) ESTA TABLA CADUCA
35
+ *
36
+ * Última verificación: 2026-08-26, contra las páginas oficiales:
37
+ * OpenAI https://developers.openai.com/api/docs/pricing
38
+ * Anthropic https://platform.claude.com/docs/en/about-claude/pricing
39
+ * Google https://ai.google.dev/gemini-api/docs/pricing
40
+ *
41
+ * La versión anterior llevaba parada desde el 2026-04-18 —y el servicio sin
42
+ * redesplegar desde entonces—, así que para agosto le faltaban familias
43
+ * enteras: gpt-5.5, gpt-5.6, Opus 5, Sonnet 5, Gemini 3.5/3.6/3.7. Un modelo
44
+ * ausente ya no se cobra con la tarifa de un vecino —`findPricingKey` se niega
45
+ * a adivinar— pero sale a coste CERO y con clave `null`: revisa esta tabla
46
+ * cuando veas trazas sin precio.
47
+ *
48
+ * Se conservan modelos retirados a propósito: las trazas históricas siguen
49
+ * consultándose y tienen que seguir cuadrando.
50
+ *
51
+ * ## Lo que esta tabla NO modela (aceptable para estimar coste)
52
+ *
53
+ * - Contexto largo: `gemini-3.1-pro` y `gemini-2.5-pro` casi doblan tarifa
54
+ * por encima de 200k tokens de entrada. Se usa siempre la base, así que
55
+ * INFRAVALORA los prompts muy largos.
56
+ * - Audio: `gemini-2.5-flash` y `3.1-flash-lite` cobran más el audio que el
57
+ * texto. Aquí todo va a tarifa de texto.
58
+ * - Anthropic: `inference_geo` "us" multiplica por 1,1, y el modo rápido de
59
+ * Opus 5 / 4.8 cuesta $10/$50. No se modelan: HostWebhook no los usa.
60
+ * - Descuento del 50% de las APIs por lotes. Nadie lo usa todavía.
61
+ * - (!) Claude 4.7 en adelante usa un tokenizador nuevo que produce ~30% MÁS
62
+ * tokens para el mismo texto. El precio por token no cambia, pero la
63
+ * factura por la misma conversación sí.
64
+ *
65
+ * ⚠️ Quien ponga un tope de gasto sobre esto tiene que saber que INFRAVALORA
66
+ * en los casos de arriba. El tope real queda por encima del nominal.
67
+ */
68
+ Object.defineProperty(exports, "__esModule", { value: true });
69
+ exports.MODEL_ALTERNATIVES = exports.MODEL_PRICING = void 0;
70
+ exports.findPricingKey = findPricingKey;
71
+ exports.isModelPriced = isModelPriced;
72
+ exports.calculateCost = calculateCost;
73
+ exports.getKnownModels = getKnownModels;
74
+ exports.getSuggestedAlternative = getSuggestedAlternative;
75
+ exports.MODEL_PRICING = {
76
+ // OpenAI — GPT series
77
+ 'gpt-5.6-sol': { input: 4.00, output: 20.00 },
78
+ 'gpt-5.6-terra': { input: 2.00, output: 12.00 },
79
+ 'gpt-5.6-luna': { input: 0.20, output: 1.20 },
80
+ 'gpt-5.5-pro': { input: 30.00, output: 180.00 },
81
+ 'gpt-5.5': { input: 5.00, output: 30.00 },
82
+ 'gpt-5.4-pro': { input: 30.00, output: 180.00 },
83
+ 'gpt-5.4': { input: 2.50, output: 15.00 },
84
+ 'gpt-5.4-mini': { input: 0.75, output: 4.50 },
85
+ 'gpt-5.4-nano': { input: 0.20, output: 1.25 },
86
+ 'gpt-5.2-pro': { input: 21.00, output: 168.00 },
87
+ 'gpt-5.2': { input: 1.75, output: 14.00 },
88
+ 'gpt-5.1': { input: 1.25, output: 10.00 },
89
+ 'gpt-5-pro': { input: 15.00, output: 120.00 },
90
+ 'gpt-5': { input: 1.25, output: 10.00 },
91
+ 'gpt-5-mini': { input: 0.25, output: 2.00 },
92
+ 'gpt-5-nano': { input: 0.05, output: 0.40 },
93
+ 'gpt-4.1': { input: 2.00, output: 8.00 },
94
+ 'gpt-4.1-mini': { input: 0.40, output: 1.60 },
95
+ 'gpt-4.1-nano': { input: 0.10, output: 0.40 },
96
+ 'gpt-4o': { input: 2.50, output: 10.00 },
97
+ 'gpt-4o-mini': { input: 0.15, output: 0.60 },
98
+ 'gpt-4-turbo': { input: 10.00, output: 30.00 },
99
+ 'gpt-4': { input: 30.00, output: 60.00 },
100
+ 'gpt-3.5-turbo': { input: 0.50, output: 1.50 },
101
+ 'gpt-4o-2024-05-13': { input: 5.00, output: 15.00 },
102
+ 'chat-latest': { input: 5.00, output: 30.00 },
103
+ // OpenAI — reasoning series
104
+ 'o1-pro': { input: 150.00, output: 600.00 },
105
+ 'o1': { input: 15.00, output: 60.00 },
106
+ 'o1-mini': { input: 1.10, output: 4.40 },
107
+ 'o3-pro': { input: 20.00, output: 80.00 },
108
+ 'o3': { input: 2.00, output: 8.00 },
109
+ 'o3-mini': { input: 1.10, output: 4.40 },
110
+ 'o4-mini': { input: 1.10, output: 4.40 },
111
+ // Anthropic
112
+ 'claude-fable-5': { input: 10.00, output: 50.00 },
113
+ 'claude-mythos-5': { input: 10.00, output: 50.00 },
114
+ 'claude-opus-5': { input: 5.00, output: 25.00 },
115
+ 'claude-opus-4-8': { input: 5.00, output: 25.00 },
116
+ 'claude-opus-4-7': { input: 5.00, output: 25.00 },
117
+ 'claude-opus-4-6': { input: 5.00, output: 25.00 },
118
+ 'claude-opus-4-5': { input: 5.00, output: 25.00 },
119
+ /* (!) `claude-opus-4-20250514` ES Claude Opus 4, y Opus 4 cuesta $15/$75.
120
+ Estaba a $5/$25 —la tarifa de la generación 4.5 en adelante—, o sea
121
+ cobrando un TERCIO de lo real. Se quitó además `claude-opus-4-5-20250514`,
122
+ un id que mezclaba la versión 4.5 con la fecha de lanzamiento de la 4; si
123
+ alguna vez llega, `findPricingKey` lo resuelve solo contra
124
+ `claude-opus-4-5`. */
125
+ 'claude-opus-4-20250514': { input: 15.00, output: 75.00 },
126
+ 'claude-opus-4': { input: 15.00, output: 75.00 },
127
+ 'claude-opus-4-1': { input: 15.00, output: 75.00 },
128
+ 'claude-sonnet-5': { input: 2.00, output: 10.00 },
129
+ 'claude-sonnet-4-6': { input: 3.00, output: 15.00 },
130
+ 'claude-sonnet-4-5': { input: 3.00, output: 15.00 },
131
+ 'claude-sonnet-4-5-20250514': { input: 3.00, output: 15.00 },
132
+ 'claude-sonnet-4': { input: 3.00, output: 15.00 },
133
+ 'claude-sonnet-4-20250514': { input: 3.00, output: 15.00 },
134
+ 'claude-haiku-4-5-20251001': { input: 1.00, output: 5.00 },
135
+ 'claude-3-7-sonnet-20250219': { input: 3.00, output: 15.00 },
136
+ 'claude-3-5-sonnet-20241022': { input: 3.00, output: 15.00 },
137
+ 'claude-3-5-haiku-20241022': { input: 0.80, output: 4.00 },
138
+ 'claude-3-opus-20240229': { input: 15.00, output: 75.00 },
139
+ 'claude-haiku-3': { input: 0.25, output: 1.25 },
140
+ // Google
141
+ 'gemini-3.7-flash': { input: 0.75, output: 3.75 },
142
+ 'gemini-3.6-flash': { input: 0.75, output: 3.75 },
143
+ 'gemini-3.5-flash-lite': { input: 0.30, output: 2.50 },
144
+ 'gemini-3.5-flash': { input: 1.50, output: 9.00 },
145
+ 'gemini-3.1-pro': { input: 2.00, output: 12.00 },
146
+ 'gemini-3.1-flash-lite': { input: 0.25, output: 1.50 },
147
+ 'gemini-3-flash': { input: 0.50, output: 3.00 },
148
+ 'gemini-2.5-pro': { input: 1.25, output: 10.00 },
149
+ 'gemini-2.5-flash': { input: 0.30, output: 2.50 },
150
+ 'gemini-2.5-flash-lite': { input: 0.10, output: 0.40 },
151
+ 'gemini-2.0-flash': { input: 0.10, output: 0.40 },
152
+ 'gemini-1.5-pro': { input: 1.25, output: 5.00 },
153
+ 'gemini-1.5-flash': { input: 0.075, output: 0.30 },
154
+ // Mistral
155
+ 'mistral-large-latest': { input: 2.00, output: 6.00 },
156
+ 'mistral-small-latest': { input: 0.20, output: 0.60 },
157
+ 'codestral-latest': { input: 0.30, output: 0.90 },
158
+ // Cohere
159
+ 'command-r-plus': { input: 2.50, output: 10.00 },
160
+ 'command-r': { input: 0.15, output: 0.60 },
161
+ };
162
+ /* ── Caché ───────────────────────────────────────────────────────── */
163
+ /** Multiplicadores de lectura de caché, sobre el precio de ENTRADA. */
164
+ const CACHE_READ_MULTIPLIERS = {
165
+ anthropic: 0.1, // 10% del precio de entrada
166
+ openai: 0.5, // 50% del precio de entrada
167
+ google: 0.1, // 10% del precio de entrada ($0.125/$1.25)
168
+ };
169
+ /** Multiplicadores de creación de caché, sobre el precio de ENTRADA. */
170
+ const CACHE_CREATION_MULTIPLIERS = {
171
+ anthropic: 1.25, // 125% del precio de entrada
172
+ // OpenAI y Google: la creación no se cobra (cacheado automático)
173
+ };
174
+ function detectProvider(model) {
175
+ if (model.startsWith('gpt-') ||
176
+ model.startsWith('o1') ||
177
+ model.startsWith('o3') ||
178
+ model.startsWith('o4'))
179
+ return 'openai';
180
+ if (model.includes('claude'))
181
+ return 'anthropic';
182
+ if (model.includes('gemini'))
183
+ return 'google';
184
+ if (model.includes('mistral') || model.includes('mixtral'))
185
+ return 'mistral';
186
+ if (model.includes('command'))
187
+ return 'cohere';
188
+ return 'custom';
189
+ }
190
+ /* ── Resolución del modelo ───────────────────────────────────────── */
191
+ /**
192
+ * Qué entrada de la tabla pone precio a este modelo, o `null` cuando de verdad
193
+ * no lo sabemos.
194
+ *
195
+ * ## Por qué la regla es «la clave conocida es PREFIJO del modelo pedido»
196
+ *
197
+ * La versión anterior iba al revés: recortaba el modelo pedido y buscaba la
198
+ * primera clave que empezara por ese trozo. Dos fallos, los dos MEDIDOS
199
+ * ejecutando la función con 1M de entrada y 1M de salida:
200
+ *
201
+ * - `gpt-5.4-turbo-2026` recortaba a `gpt-5.4` y `find()` devolvía la PRIMERA
202
+ * clave declarada que empieza así —`gpt-5.4-pro`, a $30/$180—. Cobraba $210
203
+ * donde la base `gpt-5.4` cuesta $17.50. Doce veces de más, y el ORDEN DE
204
+ * DECLARACIÓN del objeto decidía la factura.
205
+ * - `claude-opus-5` recortaba a `claude-opus` y cobraba $30 con la tarifa de
206
+ * `claude-opus-4-7`. Un modelo ausente salía con un precio inventado y de
207
+ * aspecto plausible.
208
+ *
209
+ * La dirección correcta es la contraria: sólo vale una clave que sea prefijo
210
+ * DEL MODELO PEDIDO, y gana la más larga. Eso cubre el caso real —un snapshot
211
+ * con fecha, `claude-sonnet-4-6-20260101`— y se niega a adivinar cuando de
212
+ * verdad es un modelo nuevo. Mejor decir «no lo sé» que cobrar la tarifa de
213
+ * otro.
214
+ *
215
+ * La frontera importa: `gpt-4` NO puede prestar su precio a `gpt-45-turbo`,
216
+ * así que el carácter siguiente tiene que ser un separador.
217
+ *
218
+ * ⚠️ Ese `null` es INFORMACIÓN, no un hueco que rellenar. Hay código aguas
219
+ * abajo que necesita distinguir «gratis» de «no sé cuánto vale» —un tope de
220
+ * gasto, sin ir más lejos—. Quien lo «mejore» adivinando le devuelve al
221
+ * llamante un número inventado con pinta de bueno.
222
+ */
223
+ function findPricingKey(model) {
224
+ if (!model)
225
+ return null;
226
+ if (exports.MODEL_PRICING[model])
227
+ return model;
228
+ const SEPARADORES = new Set(['-', '_', '.', ':', '@', '/']);
229
+ let mejor = null;
230
+ for (const key of Object.keys(exports.MODEL_PRICING)) {
231
+ if (!model.startsWith(key))
232
+ continue;
233
+ if (!SEPARADORES.has(model.charAt(key.length)))
234
+ continue;
235
+ if (!mejor || key.length > mejor.length)
236
+ mejor = key;
237
+ }
238
+ return mejor;
239
+ }
240
+ /** True cuando la tabla sabe de verdad cuánto vale este modelo. */
241
+ function isModelPriced(model) {
242
+ return findPricingKey(model) !== null;
243
+ }
244
+ /* ── Coste ───────────────────────────────────────────────────────── */
245
+ /**
246
+ * Coste en USD, redondeado a 6 decimales (precisión de micro-dólar).
247
+ *
248
+ * ⚠️ Devuelve 0 para modelos que no sabemos poner en precio: usa
249
+ * `isModelPriced()` o `findPricingKey()` para distinguir «gratis» de «no lo
250
+ * sé» ANTES de enseñarle un 0 a una persona —o de dejar pasar un turno de chat
251
+ * porque su coste «no llega al tope»—.
252
+ *
253
+ * ⚠️ Entrada y salida se tarifan POR SEPARADO, con precios distintos y a
254
+ * menudo con un factor de 5x entre ellos. Sumar tokens y multiplicar por un
255
+ * precio único da un número que no es el de nadie.
256
+ *
257
+ * ## (!) `outputTokens` YA incluye el razonamiento
258
+ *
259
+ * `reasoningTokens` es INFORMATIVO y no se cobra aparte, porque los dos
260
+ * proveedores que lo reportan ya lo tienen dentro de la salida:
261
+ *
262
+ * - OpenAI: "reasoning tokens ... are billed as output tokens";
263
+ * `completion_tokens_details.reasoning_tokens` es un desglose de
264
+ * `completion_tokens`, no un extra.
265
+ * - Google: `thoughtsTokenCount` va aparte de `candidatesTokenCount` en el
266
+ * JSON, así que quien emite tiene que sumarlos ANTES de mandar
267
+ * `outputTokens` — es lo que hace HostWebhook desde el arreglo del conteo
268
+ * de Gemini.
269
+ *
270
+ * Antes se sumaba `reasoningTokens * precio_de_salida` ENCIMA de
271
+ * `outputTokens` para openai y google: eso cobraba dos veces el mismo
272
+ * pensamiento. El parámetro se conserva para no romper a quien ya lo pasa.
273
+ */
274
+ function calculateCost(model, inputTokens, outputTokens, cacheReadInputTokens = 0, cacheCreationInputTokens = 0, _reasoningTokens = 0) {
275
+ const key = findPricingKey(model);
276
+ if (!key)
277
+ return 0;
278
+ const pricing = exports.MODEL_PRICING[key];
279
+ const provider = detectProvider(key);
280
+ const inputCost = (inputTokens / 1000000) * pricing.input;
281
+ const outputCost = (outputTokens / 1000000) * pricing.output;
282
+ // Caché: multiplicadores por proveedor
283
+ const cacheReadMul = CACHE_READ_MULTIPLIERS[provider] ?? 0.1;
284
+ const cacheCreationMul = CACHE_CREATION_MULTIPLIERS[provider] ?? 0;
285
+ const cacheReadCost = (cacheReadInputTokens / 1000000) * pricing.input * cacheReadMul;
286
+ const cacheCreationCost = (cacheCreationInputTokens / 1000000) * pricing.input * cacheCreationMul;
287
+ return (Math.round((inputCost + outputCost + cacheReadCost + cacheCreationCost) * 1000000) / 1000000);
288
+ }
289
+ /** Los modelos que la tabla sabe poner en precio, en orden de declaración. */
290
+ function getKnownModels() {
291
+ return Object.keys(exports.MODEL_PRICING);
292
+ }
293
+ /* ── Alternativas más baratas ────────────────────────────────────── */
294
+ /**
295
+ * De un modelo caro a otro más barato de capacidad parecida.
296
+ *
297
+ * ⚠️ Es un mapa a mano y NO cubre toda la tabla: un modelo que no está aquí
298
+ * simplemente no tiene sugerencia. No es la lista de modelos válidos —esa es
299
+ * `MODEL_PRICING`—.
300
+ */
301
+ exports.MODEL_ALTERNATIVES = {
302
+ // OpenAI — GPT series
303
+ 'gpt-5.4': 'gpt-5.2',
304
+ 'gpt-5.2': 'gpt-5',
305
+ 'gpt-5.1': 'gpt-5-mini',
306
+ 'gpt-5': 'gpt-5-mini',
307
+ 'gpt-5-mini': 'gpt-5-nano',
308
+ 'gpt-4.1': 'gpt-4.1-mini',
309
+ 'gpt-4.1-mini': 'gpt-4.1-nano',
310
+ 'gpt-4o': 'gpt-4o-mini',
311
+ 'gpt-4-turbo': 'gpt-4.1',
312
+ 'gpt-4': 'gpt-4.1',
313
+ // OpenAI — reasoning series
314
+ 'o1': 'o3',
315
+ 'o3': 'o4-mini',
316
+ 'o1-mini': 'o4-mini',
317
+ 'o3-mini': 'o4-mini',
318
+ // Anthropic
319
+ 'claude-opus-4-6': 'claude-sonnet-4-6',
320
+ 'claude-opus-4-20250514': 'claude-sonnet-4-20250514',
321
+ 'claude-sonnet-4-6': 'claude-haiku-4-5-20251001',
322
+ 'claude-sonnet-4-20250514': 'claude-haiku-4-5-20251001',
323
+ 'claude-3-7-sonnet-20250219': 'claude-haiku-4-5-20251001',
324
+ 'claude-3-5-sonnet-20241022': 'claude-3-5-haiku-20241022',
325
+ 'claude-3-opus-20240229': 'claude-sonnet-4-6',
326
+ // Google
327
+ 'gemini-3.1-pro': 'gemini-3-flash',
328
+ 'gemini-3-flash': 'gemini-2.5-flash',
329
+ 'gemini-2.5-pro': 'gemini-2.5-flash',
330
+ 'gemini-2.5-flash': 'gemini-2.5-flash-lite',
331
+ 'gemini-1.5-pro': 'gemini-2.5-flash',
332
+ // Mistral
333
+ 'mistral-large-latest': 'mistral-small-latest',
334
+ // Cohere
335
+ 'command-r-plus': 'command-r',
336
+ };
337
+ /**
338
+ * La alternativa más barata sugerida para este modelo y lo que se ahorraría,
339
+ * o `null` cuando no hay sugerencia.
340
+ *
341
+ * ⚠️ Busca por el nombre EXACTO en `MODEL_ALTERNATIVES` —no usa
342
+ * `findPricingKey`—, así que un snapshot con fecha no encuentra sugerencia
343
+ * aunque su modelo base sí la tenga. Se conserva tal cual: cambiarlo alteraría
344
+ * qué se le sugiere hoy a la gente.
345
+ */
346
+ function getSuggestedAlternative(model, inputTokens, outputTokens) {
347
+ const alt = exports.MODEL_ALTERNATIVES[model];
348
+ if (!alt)
349
+ return null;
350
+ const currentCost = calculateCost(model, inputTokens, outputTokens);
351
+ const alternativeCost = calculateCost(alt, inputTokens, outputTokens);
352
+ if (currentCost <= 0)
353
+ return null;
354
+ const savingsUsd = currentCost - alternativeCost;
355
+ const savingsPercent = Math.round((savingsUsd / currentCost) * 10000) / 100;
356
+ return {
357
+ alternativeModel: alt,
358
+ currentCost: Math.round(currentCost * 1000000) / 1000000,
359
+ alternativeCost: Math.round(alternativeCost * 1000000) / 1000000,
360
+ savingsUsd: Math.round(savingsUsd * 1000000) / 1000000,
361
+ savingsPercent,
362
+ };
363
+ }
@@ -32,7 +32,15 @@
32
32
  * Antes la puerta leía `memoryEnabled` del documento para decidir; esa decisión
33
33
  * se va con el dato, que es donde puede comprobarse.
34
34
  */
35
- /** El nodo conectado, en lo que la puerta lee de él. Cuatro campos, medidos. */
35
+ /**
36
+ * El nodo conectado, en lo que la puerta lee de él. Siete campos, medidos.
37
+ *
38
+ * Los cuatro primeros son lo que la puerta necesitaba para EJECUTAR. Los tres
39
+ * últimos son lo que necesita para PONERLE PRECIO al turno antes de ejecutarlo:
40
+ * el Chat Trigger tiene topes en dólares y reserva por adelantado, y esa reserva
41
+ * no se puede calcular sin saber a qué modelo se va a llamar ni cuánto puede
42
+ * devolver. Sin ellos la estimación caía a sus valores por defecto en silencio.
43
+ */
36
44
  export interface NodoDeIaConectado {
37
45
  id: string;
38
46
  /** Para el paso del historial y para decirle al widget con quién habla. */
@@ -40,6 +48,35 @@ export interface NodoDeIaConectado {
40
48
  workspaceId: string | null;
41
49
  /** Si el widget enseña las llamadas a herramientas mientras ocurren. */
42
50
  muestraLasHerramientas: boolean;
51
+ /** El modelo configurado en el nodo. Sin el nombre no hay tarifa, y sin
52
+ * tarifa la reserva del tope EN DÓLARES no se puede calcular: los tokens
53
+ * estimados no son un precio hasta que alguien dice de qué modelo son. */
54
+ model: string;
55
+ /** El tope de salida del nodo. Es el techo de lo que puede devolver, y la
56
+ * estimación es deliberadamente PESIMISTA: reserva el techo entero. */
57
+ maxTokens: number;
58
+ /**
59
+ * La LONGITUD del system prompt del nodo, no el prompt.
60
+ *
61
+ * ⚠️ Un `number` y no la cadena, por dos motivos, y los dos importan:
62
+ *
63
+ * 1. No hace falta más. La estimación sólo hace `.length`: suma ese largo a
64
+ * los caracteres del turno y divide entre tres. El número da exactamente
65
+ * el mismo resultado que la cadena, así que mandar la cadena sería mandar
66
+ * algo que nadie va a leer.
67
+ *
68
+ * 2. Esto CRUZA UNA FRONTERA DE SERVICIO —es el motivo de existir de este
69
+ * fichero, y está en su cabecera—, y el `systemPrompt` es contenido del
70
+ * cliente. Mandarlo entero lo filtraría a un servicio que no lo necesita.
71
+ * Mismo criterio que ya aplica `ChunkingStrategy` en `formas-compartidas`:
72
+ * «sólo cruza la ESTRATEGIA, no la configuración entera».
73
+ *
74
+ * ⚠️ Y se llama `largoDelSystemPrompt` y no `largoDelPrompt` porque en el
75
+ * consumidor conviven DOS prompts: el del nodo y el `systemPromptOverride`
76
+ * del trigger, que lo pisa. El nombre dice cuál se midió — éste es el del
77
+ * nodo, y quien tenga override medirá el suyo y no usará éste.
78
+ */
79
+ largoDelSystemPrompt: number;
43
80
  }
44
81
  export interface EjecucionDeIa {
45
82
  /** El nodo conectado a ese chat, o `null` si ya no existe. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/platform-contracts",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",