El seguimiento de costos de OpenRouter devuelve $0 debido a error de tipo en extractCostBreakdown() - OpenRouter cost tracking returns $0 due to type mismatch in extractCostBreakdown()
La función extractCostBreakdown() espera usage.cost como un objeto, pero OpenRouter lo devuelve como un número plano, lo que causa fallos silenciosos en el seguimiento de costos.
🔍 Síntomas
Manifestación principal: Las solicitudes a la API de OpenRouter devuelven usage.cost como un valor numérico escalar (por ejemplo, 0.0045), pero la lógica de seguimiento de costos intenta acceder a él como una propiedad de un objeto anidado.
Salida de error:
// En la función extractCostBreakdown() en pi-embedded-*.js:
const total = toFiniteNumber(cost.total); // cost es el número 0.0045
// Resultado: total = NaN (ya que cost.total === undefined)
// Costo registrado como: $0.00
// Ejemplo de salida en consola:
[OpenClaw] Solicitud a OpenRouter Completada
Modelo: anthropic/claude-3.5-sonnet
Costo: $0.00 ← INCORRECTO (debería ser ~$0.0045)
Tokens: 2453 entrada, 892 salida
Detección técnica:
// Inspección de depuración de la respuesta de OpenRouter
console.log(response.usage.cost);
// Salida: 0.0045 (número)
// Inspección de depuración de la extracción de costos interna
console.log(typeof response.usage.cost);
// Salida: "number"
// Estructura esperada según otros proveedores
console.log(response.usage.cost);
// Salida: { total: 0.0045, input: 0.001, output: 0.0035 } (objeto)
🧠 Causa raíz
Divergencia arquitectónica: La función extractCostBreakdown() fue diseñada contra estructuras de costos compatibles con OpenAI donde usage.cost siempre es un objeto que contiene las claves total, input y output.
Secuencia de fallo:
- La API de OpenRouter devuelve
usage.cost = 0.0045(número escalar) extractCostBreakdown()ejecuta:const total = toFiniteNumber(cost.total)- Cuando
costes un número,cost.totalse evalúa comoundefined toFiniteNumber(undefined)devuelveNaN- La lógica de agregación de costos trata
NaNcomo$0para propósitos de visualización - No se lanza ningún error—el fallo es silencioso
Ruta de código afectada:
// Archivo: dist/pi-embedded-*.js
// Ubicación: función extractCostBreakdown()
function extractCostBreakdown(cost) {
const total = toFiniteNumber(cost.total); // ← FALLA cuando cost es un número
const input = toFiniteNumber(cost.input);
const output = toFiniteNumber(cost.output);
return { total, input, output };
}
Comparación de proveedores:
| Proveedor | Tipo de usage.cost | Estructura |
|---|---|---|
| OpenAI | Objeto | { total, input, output } |
| Azure OpenAI | Objeto | { total, input, output } |
| Anthropic | Objeto | { total, input, output } |
| OpenRouter | Número | 0.0045 (valor plano) |
🛠️ Solución paso a paso
Archivo objetivo: dist/pi-embedded-*.js (o equivalente en código fuente)
Cambio requerido: Actualizar la función extractCostBreakdown() para manejar tanto tipos de número escalar como de objeto para usage.cost.
Antes:
function extractCostBreakdown(cost) {
const total = toFiniteNumber(cost.total);
const input = toFiniteNumber(cost.input);
const output = toFiniteNumber(cost.output);
return { total, input, output };
}
Después:
function extractCostBreakdown(cost) {
// Manejar número plano de OpenRouter vs objeto compatible con OpenAI
const total = toFiniteNumber(typeof cost === 'number' ? cost : cost.total);
const input = toFiniteNumber(cost.input);
const output = toFiniteNumber(cost.output);
return { total, input, output };
}
Implementación alternativa robusta:
function extractCostBreakdown(cost) {
// Defensivo: normalizar cost a forma de objeto independientemente del tipo de entrada
const costObj = typeof cost === 'number'
? { total: cost, input: 0, output: 0 }
: cost;
const total = toFiniteNumber(costObj.total);
const input = toFiniteNumber(costObj.input);
const output = toFiniteNumber(costObj.output);
return { total, input, output };
}
Corrección en archivo fuente (si está disponible):
Navega al archivo fuente que contiene extractCostBreakdown():
# Asumiendo estructura estándar del proyecto
find . -name "*.ts" -o -name "*.js" | xargs grep -l "extractCostBreakdown"
# Salida: src/providers/openrouter.ts o src/utils/cost.ts
Aplica la corrección de guarda de tipo directamente en el archivo fuente antes de reconstruir el paquete de distribución.
🧪 Verificación
Caso de prueba 1: Costo como número plano de OpenRouter
// Simular respuesta de OpenRouter
const mockOpenRouterCost = 0.0045;
const result = extractCostBreakdown(mockOpenRouterCost);
console.log(result);
// Esperado: { total: 0.0045, input: NaN o 0, output: NaN o 0 }
// Verificar que total se extrae correctamente
console.log(Number.isFinite(result.total));
// Esperado: true
Caso de prueba 2: Costo como objeto compatible con OpenAI
// Simular respuesta de proveedor estándar
const mockStandardCost = { total: 0.012, input: 0.006, output: 0.006 };
const result = extractCostBreakdown(mockStandardCost);
console.log(result);
// Esperado: { total: 0.012, input: 0.006, output: 0.006 }
Verificación de integración:
# Ejecutar suite de pruebas de seguimiento de costos
npm test -- --grep "cost"
# O específicamente para OpenRouter
npm test -- --grep "OpenRouter"
# Esperado: Todas las pruebas de extracción de costos pasan
Verificación manual de extremo a extremo:
# 1. Ejecutar una solicitud simple a OpenRouter
curl -X POST https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-3.5-sonnet",
"messages": [{"role": "user", "content": "Hello"}]
}'
# 2. Verificar el costo extraído en los logs de OpenClaw
# Debería mostrar el valor real del costo, no $0.00
Salida de log esperada después de la corrección:
[OpenClaw] Solicitud a OpenRouter Completada
Modelo: anthropic/claude-3.5-sonnet
Costo: $0.0045 ← CORRECTO
Tokens: 2453 entrada, 892 salida
⚠️ Errores comunes
- Patrón de fallo silencioso: Este error no produce errores lanzados. Siempre instrumenta la extracción de costos con registro para detectar anomalías de
NaNo$0. - Otros proveedores con números planos: Cualquier proveedor compatible con OpenAI que devuelva un
usage.costescalar en el futuro provocará un fallo idéntico. Documenta esta suposición en el código base. - Desajuste de artefactos de compilación: Corregir archivos fuente requiere reconstruir
dist/pi-embedded-*.js. Verifica que el paquete de distribución refleje los cambios del código fuente.# Error común: editar archivo dist sin reconstruir # Siempre reconstruir después de cambios en el código fuente npm run build npm run dist - Casos extremos de toFiniteNumber: La función auxiliar
toFiniteNumber()puede devolver0paraundefinedonull, ocultando el problema subyacente.// Verificar comportamiento de toFiniteNumber toFiniteNumber(undefined); // Devuelve NaN o 0 dependiendo de la implementación toFiniteNumber(null); // Devuelve 0 toFiniteNumber(NaN); // Devuelve NaN - Costo por token vs costo total: Cuando el costo es un número plano, se pierde el desglose de
inputyoutput. Considera si se requiere un informe de costos granular. - Enrutamiento /auto de OpenRouter: Cuando se usa la selección de modelo
/auto, el cálculo de costos ocurre después de la respuesta. Asegúrate de que la corrección se aplique tanto a solicitudes explícitas como a las enrutadas automáticamente.
🔗 Errores relacionados
TypeError: Cannot read property 'total' of undefined
Ocurre cuandousage.costesundefineden lugar de un número. Modo de fallo diferente pero misma ubicación de función.NaNapareciendo en logs de agregación de costos
Síntoma del error actual cuando el resultado de la extracción de costos se propaga a través de operaciones aritméticas.- Issue de GitHub #142: "Seguimiento de costos roto para el proveedor X devuelve $0"
Patrón histórico donde otros proveedores con formatos de costos no estándar causaron síntomas idénticos. TypeError: cost.total is not a function(mensaje engañoso)
Se activa sicostes una representación de cadena de un número (por ejemplo,"0.0045").- Datos de costos faltantes en webhook/payload
Problema posterior relacionado donde los fallos en el seguimiento de costos causan camposcost_breakdownvacíos en informes exportados. - Discrepancia de costos de la API de OpenRouter
OpenRouter puede devolver el costo comonullpara ciertos modelos gratuitos o sin caché. Asegúrate de que el manejo de null también sea considerado.