mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 12:04:08 +00:00
Track failed LLM API calls in llm_usages table (#360)
* Track failed LLM API calls in llm_usages table This commit adds comprehensive error tracking for failed LLM API calls: - Updated LlmUsage model with helper methods to identify failed calls and retrieve error details (failed?, http_status_code, error_message) - Modified Provider::Openai to record failed API calls with error metadata including HTTP status codes and error messages in both native and generic chat response methods - Enhanced UsageRecorder concern with record_usage_error method to support error tracking for auto-categorization and auto-merchant detection - Updated LLM usage UI to display failed calls with: - Red background highlighting for failed rows - Error indicator icon with "Failed" label - Interactive tooltip on hover showing error message and HTTP status code Failed calls are now tracked with zero tokens and null cost, storing error details in the metadata JSONB column for visibility and debugging. * Dark mode fixes --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,21 @@ class LlmUsage < ApplicationRecord
|
||||
estimated_cost.nil? ? "N/A" : "$#{estimated_cost.round(4)}"
|
||||
end
|
||||
|
||||
# Check if this usage record represents a failed API call
|
||||
def failed?
|
||||
metadata.present? && metadata["error"].present?
|
||||
end
|
||||
|
||||
# Get the HTTP status code from metadata
|
||||
def http_status_code
|
||||
metadata&.dig("http_status_code")
|
||||
end
|
||||
|
||||
# Get the error message from metadata
|
||||
def error_message
|
||||
metadata&.dig("error")
|
||||
end
|
||||
|
||||
# Estimate cost for auto-categorizing a batch of transactions
|
||||
# Based on typical token usage patterns:
|
||||
# - ~100 tokens per transaction in the prompt
|
||||
|
||||
@@ -241,6 +241,7 @@ class Provider::Openai < Provider
|
||||
session_id: session_id,
|
||||
user_identifier: user_identifier
|
||||
)
|
||||
record_llm_usage(family: family, model: model, operation: "chat", error: e)
|
||||
raise
|
||||
end
|
||||
end
|
||||
@@ -314,6 +315,7 @@ class Provider::Openai < Provider
|
||||
session_id: session_id,
|
||||
user_identifier: user_identifier
|
||||
)
|
||||
record_llm_usage(family: family, model: model, operation: "chat", error: e)
|
||||
raise
|
||||
end
|
||||
end
|
||||
@@ -446,8 +448,36 @@ class Provider::Openai < Provider
|
||||
Rails.logger.warn("Langfuse logging failed: #{e.message}")
|
||||
end
|
||||
|
||||
def record_llm_usage(family:, model:, operation:, usage:)
|
||||
return unless family && usage
|
||||
def record_llm_usage(family:, model:, operation:, usage: nil, error: nil)
|
||||
return unless family
|
||||
|
||||
# For error cases, record with zero tokens
|
||||
if error.present?
|
||||
Rails.logger.info("Recording failed LLM usage - Error: #{error.message}")
|
||||
|
||||
# Extract HTTP status code if available from the error
|
||||
http_status_code = extract_http_status_code(error)
|
||||
|
||||
inferred_provider = LlmUsage.infer_provider(model)
|
||||
family.llm_usages.create!(
|
||||
provider: inferred_provider,
|
||||
model: model,
|
||||
operation: operation,
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
estimated_cost: nil,
|
||||
metadata: {
|
||||
error: error.message,
|
||||
http_status_code: http_status_code
|
||||
}
|
||||
)
|
||||
|
||||
Rails.logger.info("Failed LLM usage recorded successfully - Status: #{http_status_code}")
|
||||
return
|
||||
end
|
||||
|
||||
return unless usage
|
||||
|
||||
Rails.logger.info("Recording LLM usage - Raw usage data: #{usage.inspect}")
|
||||
|
||||
@@ -487,4 +517,23 @@ class Provider::Openai < Provider
|
||||
rescue => e
|
||||
Rails.logger.error("Failed to record LLM usage: #{e.message}")
|
||||
end
|
||||
|
||||
def extract_http_status_code(error)
|
||||
# Try to extract HTTP status code from various error types
|
||||
# OpenAI gem errors may have status codes in different formats
|
||||
if error.respond_to?(:code)
|
||||
error.code
|
||||
elsif error.respond_to?(:http_status)
|
||||
error.http_status
|
||||
elsif error.respond_to?(:status_code)
|
||||
error.status_code
|
||||
elsif error.respond_to?(:response) && error.response.respond_to?(:code)
|
||||
error.response.code.to_i
|
||||
elsif error.message =~ /(\d{3})/
|
||||
# Extract 3-digit HTTP status code from error message
|
||||
$1.to_i
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,4 +44,53 @@ module Provider::Openai::Concerns::UsageRecorder
|
||||
rescue => e
|
||||
Rails.logger.error("Failed to record LLM usage: #{e.message}")
|
||||
end
|
||||
|
||||
# Records failed LLM usage for a family with error details
|
||||
def record_usage_error(model_name, operation:, error:, metadata: {})
|
||||
return unless family
|
||||
|
||||
Rails.logger.info("Recording failed LLM usage - Operation: #{operation}, Error: #{error.message}")
|
||||
|
||||
# Extract HTTP status code if available from the error
|
||||
http_status_code = extract_http_status_code(error)
|
||||
|
||||
error_metadata = metadata.merge(
|
||||
error: error.message,
|
||||
http_status_code: http_status_code
|
||||
)
|
||||
|
||||
inferred_provider = LlmUsage.infer_provider(model_name)
|
||||
family.llm_usages.create!(
|
||||
provider: inferred_provider,
|
||||
model: model_name,
|
||||
operation: operation,
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
estimated_cost: nil,
|
||||
metadata: error_metadata
|
||||
)
|
||||
|
||||
Rails.logger.info("Failed LLM usage recorded - Operation: #{operation}, Status: #{http_status_code}")
|
||||
rescue => e
|
||||
Rails.logger.error("Failed to record LLM usage error: #{e.message}")
|
||||
end
|
||||
|
||||
def extract_http_status_code(error)
|
||||
# Try to extract HTTP status code from various error types
|
||||
if error.respond_to?(:code)
|
||||
error.code
|
||||
elsif error.respond_to?(:http_status)
|
||||
error.http_status
|
||||
elsif error.respond_to?(:status_code)
|
||||
error.status_code
|
||||
elsif error.respond_to?(:response) && error.response.respond_to?(:code)
|
||||
error.response.code.to_i
|
||||
elsif error.message =~ /(\d{3})/
|
||||
# Extract 3-digit HTTP status code from error message
|
||||
$1.to_i
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<% @llm_usages.each do |usage| %>
|
||||
<tr>
|
||||
<tr class="<%= 'bg-red-50 theme-dark:bg-red-950/30' if usage.failed? %>">
|
||||
<td class="px-4 py-3 text-sm text-primary whitespace-nowrap">
|
||||
<%= usage.created_at.strftime("%b %d, %Y %I:%M %p") %>
|
||||
</td>
|
||||
@@ -128,10 +128,27 @@
|
||||
<%= usage.model %>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-primary text-right whitespace-nowrap">
|
||||
<%= number_with_delimiter(usage.total_tokens) %>
|
||||
<span class="text-xs text-secondary">
|
||||
(<%= number_with_delimiter(usage.prompt_tokens) %>/<%= number_with_delimiter(usage.completion_tokens) %>)
|
||||
</span>
|
||||
<% if usage.failed? %>
|
||||
<div data-controller="tooltip" class="inline-flex justify-end">
|
||||
<div class="inline-flex items-center gap-1 cursor-help">
|
||||
<%= icon "alert-circle", class: "w-4 h-4 text-red-600 theme-dark:text-red-400" %>
|
||||
<span class="text-red-600 theme-dark:text-red-400 font-medium">Failed</span>
|
||||
</div>
|
||||
<div role="tooltip" data-tooltip-target="tooltip" class="tooltip bg-gray-800 text-sm p-3 rounded w-72 text-left break-words whitespace-normal shadow-lg hidden">
|
||||
<div class="text-white">
|
||||
<% if usage.http_status_code.present? %>
|
||||
<p class="text-xs mt-1 text-gray-300">HTTP Status: <%= usage.http_status_code %></p>
|
||||
<% end %>
|
||||
<p class="text-xs"><%= usage.error_message %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= number_with_delimiter(usage.total_tokens) %>
|
||||
<span class="text-xs text-secondary">
|
||||
(<%= number_with_delimiter(usage.prompt_tokens) %>/<%= number_with_delimiter(usage.completion_tokens) %>)
|
||||
</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm font-medium text-primary text-right whitespace-nowrap">
|
||||
<%= usage.formatted_cost %>
|
||||
|
||||
Reference in New Issue
Block a user