nitro_intelligence 2.4.0 → 2.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9041d12c07ae072e5a7a5a2c5fccbec275d260dfd2c99cba7a5bc92337eab4d
4
- data.tar.gz: 496aed9a03587308b2af556fa846686edfee3e3a1aaf4a7a3a686cf4f08b322b
3
+ metadata.gz: 0bc1642f0475d8292a1e9611003d79e31983fb9afe2fd5e35d5ddf821a191e53
4
+ data.tar.gz: f61c77b0a3cd8231ba52bd7df317ca715423dc08eb72639e028febf508d52472
5
5
  SHA512:
6
- metadata.gz: ffa988435a0128d981e79a94f28530d1525b82c231f44e6a3cd87f15d459bcdd27ad2ef19b57499153a0a583256bb7bec2c711f51bd3cf519d9c9827cb080937
7
- data.tar.gz: 18e594810e1c4815f759375f4692cb726dbc5415c00495aa03b3943d16c77d0079aa115f5aeb9ed368d715429aaafff6967f986c4b1496984160c7cb95c8aeea
6
+ metadata.gz: 7603cb32f91bda923f1728c5318db1a1b0e7fb59c1a46bca3a4ac3c01567de9d850b578626dd76f05d17b3348e1cdb5495fa6e98f28a6e9c871323147f02db24
7
+ data.tar.gz: 251976a344210f499e8b7b0c7c05bec38461fd4d941cd852fe1d5b52bc4d69cc052f992b22fe83a0241845b8d8212773f77e2e5a0ea5addcc7c459d4d65bde2f
@@ -16,12 +16,73 @@ module NitroIntelligence
16
16
  # supplied, so cap it rather than turning a large hash into a failed request.
17
17
  MAX_SPEND_LOGS_METADATA_BYTES = 4096
18
18
 
19
+ # Cost the inference gateway calculated for a response.
20
+ # See https://docs.litellm.ai/docs/proxy/response_headers
21
+ #
22
+ # The gateway is the only component that knows which deployment actually
23
+ # served a request, and the same model group can be served by internal
24
+ # capacity or by any of several third-party providers at different rates.
25
+ # Recomputing cost downstream from token counts therefore means maintaining
26
+ # a second price table that silently drifts from the one doing the billing.
27
+ RESPONSE_COST_HEADER = "x-litellm-response-cost".freeze
28
+ RESPONSE_COST_INPUT_HEADER = "x-litellm-response-cost-input".freeze
29
+ RESPONSE_COST_OUTPUT_HEADER = "x-litellm-response-cost-output".freeze
30
+
19
31
  def initialize(client:)
20
32
  @client = client
21
33
  end
22
34
 
35
+ # Cost breakdown for an observation, or nil when the gateway did not price
36
+ # the request.
37
+ #
38
+ # A deployment the gateway has no price for sends no cost header at all
39
+ # rather than a zero one, so absence has to mean "unknown" here. Recording
40
+ # nil leaves the generation without a cost; recording 0.0 would assert the
41
+ # request was free and quietly understate spend for every model still
42
+ # awaiting a price.
43
+ #
44
+ # Only the total is guaranteed. Where the cost comes from the upstream
45
+ # provider rather than the gateway's own calculation - OpenRouter reports a
46
+ # real per-request cost of its own, which the gateway passes through - there
47
+ # is no component breakdown, so input and output appear only when sent.
48
+ def cost_details(response)
49
+ headers = response_headers(response)
50
+ return nil if headers.nil?
51
+
52
+ total = header_amount(headers, RESPONSE_COST_HEADER)
53
+ return nil if total.nil?
54
+
55
+ {
56
+ total:,
57
+ input: header_amount(headers, RESPONSE_COST_INPUT_HEADER),
58
+ output: header_amount(headers, RESPONSE_COST_OUTPUT_HEADER),
59
+ }.compact
60
+ end
61
+
23
62
  private
24
63
 
64
+ # `last_response` carries the HTTP metadata of the response a typed model was
65
+ # built from. The client leaves it unset on nested and locally constructed
66
+ # models, and on endpoints returning raw or binary payloads, so both the
67
+ # method and its value are optional.
68
+ def response_headers(response)
69
+ return nil unless response.respond_to?(:last_response)
70
+
71
+ response.last_response&.headers
72
+ end
73
+
74
+ # Header values arrive as strings. A malformed one is worth ignoring rather
75
+ # than raising: a trace missing its cost is a far smaller problem than an
76
+ # inference call failing because the gateway sent something unexpected.
77
+ def header_amount(headers, name)
78
+ value = headers[name]
79
+ return nil if value.blank?
80
+
81
+ Float(value)
82
+ rescue ArgumentError, TypeError
83
+ nil
84
+ end
85
+
25
86
  def add_request_headers(parameters, headers)
26
87
  request_options = (parameters[:request_options] ||= {})
27
88
  (request_options[:extra_headers] ||= {}).merge!(headers.compact)
@@ -79,6 +79,7 @@ module NitroIntelligence
79
79
  output_tokens: audio_transcription.usage.output_tokens,
80
80
  total_tokens: audio_transcription.usage.total_tokens,
81
81
  },
82
+ cost_details: @base_handler.cost_details(audio_transcription),
82
83
  }
83
84
 
84
85
  [audio_transcription, trace_attributes]
@@ -59,6 +59,7 @@ module NitroIntelligence
59
59
  completion_tokens: chat_completion.usage.completion_tokens,
60
60
  total_tokens: chat_completion.usage.total_tokens,
61
61
  },
62
+ cost_details: @base_handler.cost_details(chat_completion),
62
63
  }
63
64
 
64
65
  [chat_completion, trace_attributes]
@@ -94,6 +94,7 @@ module NitroIntelligence
94
94
  completion_tokens: chat_completion.usage.completion_tokens,
95
95
  total_tokens: chat_completion.usage.total_tokens,
96
96
  },
97
+ cost_details: @base_handler.cost_details(chat_completion),
97
98
  }
98
99
 
99
100
  [chat_completion, trace_attributes]
@@ -42,17 +42,7 @@ module NitroIntelligence
42
42
  record_input(generation, input)
43
43
 
44
44
  result, trace_attributes = observe_failures(generation) { yield(generation) }
45
-
46
- if trace_attributes
47
- handle_truncation(trace_attributes[:input], trace_attributes[:output], trace_attributes[:model])
48
-
49
- generation.model = trace_attributes[:model] if trace_attributes[:model]
50
- generation.usage_details = trace_attributes[:usage_details] if trace_attributes[:usage_details]
51
- generation.input = trace_attributes[:input] if trace_attributes[:input]
52
- generation.output = trace_attributes[:output] if trace_attributes[:output]
53
-
54
- generation.update_trace(input: trace_attributes[:input], output: trace_attributes[:output])
55
- end
45
+ record_result(generation, trace_attributes)
56
46
 
57
47
  result
58
48
  end
@@ -61,6 +51,27 @@ module NitroIntelligence
61
51
 
62
52
  private
63
53
 
54
+ # Applied once the response is in hand, so the observation reflects what came
55
+ # back rather than what was asked for. Each attribute is set only when the
56
+ # handler supplied it: an observation that records nothing is better than one
57
+ # asserting a value the response never carried. Cost is the clearest case -
58
+ # the gateway omits the header entirely for a deployment it has no price for,
59
+ # and writing a zero there would read as a free request rather than an
60
+ # unpriced one.
61
+ def record_result(generation, trace_attributes)
62
+ return unless trace_attributes
63
+
64
+ handle_truncation(trace_attributes[:input], trace_attributes[:output], trace_attributes[:model])
65
+
66
+ generation.model = trace_attributes[:model] if trace_attributes[:model]
67
+ generation.usage_details = trace_attributes[:usage_details] if trace_attributes[:usage_details]
68
+ generation.cost_details = trace_attributes[:cost_details] if trace_attributes[:cost_details]
69
+ generation.input = trace_attributes[:input] if trace_attributes[:input]
70
+ generation.output = trace_attributes[:output] if trace_attributes[:output]
71
+
72
+ generation.update_trace(input: trace_attributes[:input], output: trace_attributes[:output])
73
+ end
74
+
64
75
  # Recorded before the request is made so that a request which raises still
65
76
  # shows what was sent. Handlers whose input is not safe to record twice
66
77
  # (image generation sends base64 payloads that are replaced with media
@@ -4,6 +4,8 @@ require "uri"
4
4
 
5
5
  module NitroIntelligence
6
6
  class Reporter
7
+ class DatasetItemError < StandardError; end
8
+
7
9
  def initialize(observability_project_slug:)
8
10
  @observability_project_slug = observability_project_slug
9
11
  @project_client = fetch_project_client
@@ -20,7 +22,17 @@ module NitroIntelligence
20
22
  request["Authorization"] = "Basic #{@project_client.project.auth_token}"
21
23
  request.body = attributes.to_json
22
24
 
23
- http.request(request)
25
+ response = http.request(request)
26
+
27
+ # Every other request in this gem raises on an unsuccessful response. Without this a
28
+ # rejected write - bad credentials, a malformed item, a dataset that does not exist -
29
+ # is indistinguishable from a successful one, and a caller building a dataset ends up
30
+ # with a run against items that were never stored.
31
+ unless response.is_a?(Net::HTTPSuccess)
32
+ raise DatasetItemError, "#{response.code} creating dataset item: #{response.body}"
33
+ end
34
+
35
+ response
24
36
  end
25
37
 
26
38
  def score(trace_id:, name:, value:, id: "#{trace_id}-#{name}")
@@ -1,3 +1,3 @@
1
1
  module NitroIntelligence
2
- VERSION = "2.4.0".freeze
2
+ VERSION = "2.5.0".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nitro_intelligence
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Artemenko
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '0.58'
60
+ version: '0.79'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '0.58'
67
+ version: '0.79'
68
68
  description: The Ruby client for Nitro Intelligence
69
69
  email:
70
70
  - igor.artemenko@powerhrg.com