April 21, 2026

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:

  1. La API de OpenRouter devuelve usage.cost = 0.0045 (número escalar)
  2. extractCostBreakdown() ejecuta: const total = toFiniteNumber(cost.total)
  3. Cuando cost es un número, cost.total se evalúa como undefined
  4. toFiniteNumber(undefined) devuelve NaN
  5. La lógica de agregación de costos trata NaN como $0 para propósitos de visualización
  6. 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:

ProveedorTipo de usage.costEstructura
OpenAIObjeto{ total, input, output }
Azure OpenAIObjeto{ total, input, output }
AnthropicObjeto{ total, input, output }
OpenRouterNúmero0.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 NaN o $0.
  • Otros proveedores con números planos: Cualquier proveedor compatible con OpenAI que devuelva un usage.cost escalar 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 devolver 0 para undefined o null, 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 input y output. 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 cuando usage.cost es undefined en lugar de un número. Modo de fallo diferente pero misma ubicación de función.
  • NaN apareciendo 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 si cost es 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 campos cost_breakdown vacíos en informes exportados.
  • Discrepancia de costos de la API de OpenRouter
    OpenRouter puede devolver el costo como null para ciertos modelos gratuitos o sin caché. Asegúrate de que el manejo de null también sea considerado.

Evidencia y fuentes

Esta guía de solución de problemas fue sintetizada automáticamente por la tubería de inteligencia de FixClaw a partir de las discusiones de la comunidad.