Module: LlmCostTracker::Reconciliation::Sources::OpenaiUsage
- Defined in:
- lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb
Constant Summary collapse
- FINGERPRINT_KEYS =
%i[start_time end_time line_item model project_id api_key_id organization_id].freeze
- ROW_TYPE_COST =
"cost"- AUTHORITY_COST_API =
"cost_api"- DEFAULT_METER =
"tokens"
Class Method Summary collapse
- .coerce_hash(response) ⇒ Object
- .end_inclusive_date(value) ⇒ Object
- .epoch_to_date(value) ⇒ Object
- .fingerprint_for(result, start_time:, end_time:) ⇒ Object
- .match_basis_for(result) ⇒ Object
- .metadata_for(result, authority:, row_type:) ⇒ Object
- .meter_for(result) ⇒ Object
- .normalized_epoch(value) ⇒ Object
- .parse(response, authority: AUTHORITY_COST_API, row_type: ROW_TYPE_COST) ⇒ Object
- .row_for_result(raw, period_start:, period_end:, start_time:, end_time:, authority:, row_type:) ⇒ Object
- .rows_for_bucket(bucket, authority:, row_type:) ⇒ Object
- .symbolize(hash) ⇒ Object
Class Method Details
.coerce_hash(response) ⇒ Object
124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 124 def coerce_hash(response) return {} if response.nil? return symbolize(response) if response.is_a?(Hash) parsed = JSON.parse(response.to_s) raise ArgumentError, "OpenAI Costs payload must be a JSON object" unless parsed.is_a?(Hash) symbolize(parsed) rescue JSON::ParserError => e raise ArgumentError, "Unable to parse OpenAI Costs payload: #{e.}" end |
.end_inclusive_date(value) ⇒ Object
115 116 117 118 119 120 121 122 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 115 def end_inclusive_date(value) time = if value.is_a?(Numeric) || value.to_s.match?(/\A\d+\z/) Time.at(Integer(value)).utc else Time.parse(value.to_s).utc end (time - 1).utc.to_date end |
.epoch_to_date(value) ⇒ Object
109 110 111 112 113 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 109 def epoch_to_date(value) return Time.at(Integer(value)).utc.to_date if value.is_a?(Numeric) || value.to_s.match?(/\A\d+\z/) Time.parse(value.to_s).utc.to_date end |
.fingerprint_for(result, start_time:, end_time:) ⇒ Object
95 96 97 98 99 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 95 def fingerprint_for(result, start_time:, end_time:) attributes = result.merge(start_time: normalized_epoch(start_time), end_time: normalized_epoch(end_time)) Fingerprint.compute(FINGERPRINT_KEYS, attributes) end |
.match_basis_for(result) ⇒ Object
87 88 89 90 91 92 93 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 87 def match_basis_for(result) return "project" if result[:project_id] return "api_key" if result[:api_key_id] return "model" if result[:model] "period_only" end |
.metadata_for(result, authority:, row_type:) ⇒ Object
63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 63 def (result, authority:, row_type:) { "row_type" => row_type, "meter" => meter_for(result), "authority" => , "match_basis" => match_basis_for(result), "line_item" => result[:line_item], "model" => result[:model], "provider_project_id" => result[:project_id], "provider_api_key_id" => result[:api_key_id], "provider_workspace_id" => result[:organization_id] }.compact end |
.meter_for(result) ⇒ Object
77 78 79 80 81 82 83 84 85 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 77 def meter_for(result) line_item = result[:line_item].to_s.downcase case line_item when /web search/, /search content/ then "web_search" when /file search/ then "file_search_storage" when /code interpreter/, /container/ then "container_session" else DEFAULT_METER end end |
.normalized_epoch(value) ⇒ Object
101 102 103 104 105 106 107 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 101 def normalized_epoch(value) return value.to_i if value.is_a?(Numeric) Time.parse(value.to_s).utc.to_i rescue ArgumentError value.to_s end |
.parse(response, authority: AUTHORITY_COST_API, row_type: ROW_TYPE_COST) ⇒ Object
19 20 21 22 23 24 25 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 19 def parse(response, authority: AUTHORITY_COST_API, row_type: ROW_TYPE_COST) payload = coerce_hash(response) buckets = Array(payload[:data]) buckets.flat_map do |bucket| rows_for_bucket(bucket, authority: , row_type: row_type) end.compact end |
.row_for_result(raw, period_start:, period_end:, start_time:, end_time:, authority:, row_type:) ⇒ Object
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 46 def row_for_result(raw, period_start:, period_end:, start_time:, end_time:, authority:, row_type:) result = symbolize(raw) amount = symbolize(result[:amount] || {}) billed_amount = amount[:value] return nil if billed_amount.nil? fingerprint = fingerprint_for(result, start_time: start_time, end_time: end_time) { external_id: "cost-#{fingerprint}", period_start: period_start, period_end: period_end, billed_amount: billed_amount, currency: (amount[:currency] || "USD").to_s.upcase, metadata: (result, authority: , row_type: row_type) } end |
.rows_for_bucket(bucket, authority:, row_type:) ⇒ Object
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 27 def rows_for_bucket(bucket, authority:, row_type:) bucket = symbolize(bucket) start_time = bucket[:start_time] end_time = bucket[:end_time] return [] unless start_time && end_time period_start = epoch_to_date(start_time) period_end = end_inclusive_date(end_time) Array(bucket[:results]).filter_map do |raw| row_for_result(raw, period_start: period_start, period_end: period_end, start_time: start_time, end_time: end_time, authority: , row_type: row_type) end rescue ArgumentError [] end |
.symbolize(hash) ⇒ Object
136 137 138 |
# File 'lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb', line 136 def symbolize(hash) hash.to_h.transform_keys { |key| key.to_s.to_sym } end |