payloop 0.0.4 → 0.1.1

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: 0d134effe2b909cc928b4dfc80149d3c9dcde238be7630bb37dba541fa33be0a
4
- data.tar.gz: ddf58b4672381d25ab90b349c6bf29391e4fe099f9e304ba20f5169d3ee6bf1c
3
+ metadata.gz: 959126f07dc93d39fbb949ee79fd33960cdfd1ff43f8760c408f66b9dcd3bb36
4
+ data.tar.gz: 073dc2ecdfdbdd7e8a295d55a04de87a0626a2a917a0724c81deb78dd75b95dd
5
5
  SHA512:
6
- metadata.gz: 3b943601582c65f45f66209c6c7aa37c0041f3f961130b3fce5109f09369359d05cc2c7264ca85ffea99b067c95b7c1419d165bf11d847fb994f43a29697c4df
7
- data.tar.gz: 2d4ff576bccc13c526dc80ae8ab9b2dfb72a9480f3f6c88ed261a723a217271867a3f6f88e2083f8bb5a65813362b5603e8396bd5a6c944885cbf1c4010fe963
6
+ metadata.gz: 33cd64b18a4f6f44ece8ec51a804877e9e65e009e55c6163d020b72dcf040862457b9c3820bbeed89fda7eb58aba1b09458dc775e17d784f26580dc679e4d17d
7
+ data.tar.gz: 34337dccc3c9d9765980b2ad058a292fc771086fd47b3b9aa3ea5b973969ec42c46c3b6952c82ded7448bddb7e0fc9c2814260f9bc51e532040b08102210c464
data/CHANGELOG.md CHANGED
@@ -35,3 +35,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
35
35
 
36
36
  ### Fixed
37
37
  - Correct Payloop request payload structure for OpenAI streaming
38
+
39
+ ## [0.1.0] - 2026-02-03
40
+
41
+ ### Added
42
+ - Support for Sentinel intercept.
43
+ - Support for Gemini AI client tracking.
44
+
45
+ ## [0.1.1] - 2026-02-11
46
+
47
+ ### Fixed
48
+ - Stopped sending collector calls for intercepted requests
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Payloop
4
+ module API
5
+ class Sentinel < Base
6
+ def relevance_intercept(payload)
7
+ post("/sentinel/relevance/intercept", payload)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -5,7 +5,7 @@ require "securerandom"
5
5
  module Payloop
6
6
  # Main Payloop client for tracking AI costs
7
7
  class Client
8
- attr_reader :config, :collector
8
+ attr_reader :config, :collector, :sentinel
9
9
 
10
10
  def initialize(api_key: nil, collector_url: nil, api_url: nil, timeout: nil)
11
11
  api_key ||= ENV.fetch("PAYLOOP_API_KEY", nil)
@@ -18,21 +18,32 @@ module Payloop
18
18
  timeout: timeout
19
19
  )
20
20
  @collector = Collector.new(@config)
21
+ @sentinel = Sentinel.new(@config)
21
22
  end
22
23
 
23
24
  # OpenAI provider wrapper
24
25
  def openai
25
- @openai ||= Wrappers::OpenAI.new(@config, @collector)
26
+ @openai ||= Wrappers::OpenAI.new(@config, @collector, @sentinel)
26
27
  end
27
28
 
28
29
  # Anthropic provider wrapper
29
30
  def anthropic
30
- @anthropic ||= Wrappers::Anthropic.new(@config, @collector)
31
+ @anthropic ||= Wrappers::Anthropic.new(@config, @collector, @sentinel)
31
32
  end
32
33
 
33
- # Google GenAI provider wrapper
34
+ # Google GenAI provider wrapper (Tied to google-genai Gem)
35
+ # This is a port of the official Python library, but it does not
36
+ # support system prompts which is used for sentinel. This still
37
+ # works for non-sentinel use cases.
34
38
  def google
35
- @google ||= Wrappers::Google.new(@config, @collector)
39
+ @google ||= Wrappers::Google.new(@config, @collector, @sentinel)
40
+ end
41
+
42
+ # Google GenAI provider wrapper (Tied to gemini-ai Gem version 4.3.0)
43
+ # Google doesn't have an official Ruby library, so this is seems to be
44
+ # the most popular unofficial one.
45
+ def geminiai
46
+ @geminiai ||= Wrappers::GeminiAI.new(@config, @collector, @sentinel)
36
47
  end
37
48
 
38
49
  # Set attribution for cost tracking
@@ -15,6 +15,8 @@ module Payloop
15
15
  @version = Payloop::VERSION
16
16
  @attribution = Concurrent::AtomicReference.new(nil)
17
17
  @tx_uuid = Concurrent::AtomicReference.new(SecureRandom.uuid)
18
+ @raise_if_irrelevant = Concurrent::AtomicReference.new(false)
19
+ @secs_irrelevant_request_timeout = Concurrent::AtomicReference.new(5)
18
20
  end
19
21
 
20
22
  def attribution
@@ -33,6 +35,22 @@ module Payloop
33
35
  @tx_uuid.set(value)
34
36
  end
35
37
 
38
+ def raise_if_irrelevant
39
+ @raise_if_irrelevant.get
40
+ end
41
+
42
+ def raise_if_irrelevant=(value)
43
+ @raise_if_irrelevant.set(value)
44
+ end
45
+
46
+ def secs_irrelevant_request_timeout
47
+ @secs_irrelevant_request_timeout.get
48
+ end
49
+
50
+ def secs_irrelevant_request_timeout=(value)
51
+ @secs_irrelevant_request_timeout.set(value)
52
+ end
53
+
36
54
  def new_transaction
37
55
  @tx_uuid.set(SecureRandom.uuid)
38
56
  end
@@ -27,4 +27,6 @@ module Payloop
27
27
  super(message)
28
28
  end
29
29
  end
30
+
31
+ class PayloopRequestInterceptedError < Error; end
30
32
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Payloop
4
+ class Sentinel
5
+ def initialize(config)
6
+ @config = config
7
+ end
8
+
9
+ def raise_if_irrelevant(enabled: true)
10
+ raise TypeError, "enabled must be a bool" unless [true, false].include?(enabled)
11
+
12
+ @config.raise_if_irrelevant = enabled
13
+ self
14
+ end
15
+
16
+ def set_secs_irrelevant_request_timeout(timeout)
17
+ raise TypeError, "timeout must be a Numeric" unless timeout.is_a?(Numeric)
18
+ raise ArgumentError, "timeout must be greater than 0" unless timeout.positive?
19
+
20
+ @config.secs_irrelevant_request_timeout = timeout
21
+ self
22
+ end
23
+
24
+ def raise_if_irrelevant!(title:, request:, provider: nil, version: nil)
25
+ return unless @config.raise_if_irrelevant
26
+
27
+ begin
28
+ relevant, reason = make_relevance_intercept_request(
29
+ title: title,
30
+ request: request,
31
+ provider: provider,
32
+ version: version
33
+ )
34
+ rescue StandardError
35
+ return nil
36
+ end
37
+
38
+ raise PayloopRequestInterceptedError, (reason || "Irrelevant request blocked.") unless relevant
39
+ end
40
+
41
+ private
42
+
43
+ def make_relevance_intercept_request(title:, request:, provider: nil, version: nil)
44
+ payload = {
45
+ attribution: @config.attribution&.to_h,
46
+ conversation: {
47
+ client: {
48
+ provider: provider,
49
+ title: title,
50
+ version: version
51
+ },
52
+ request: normalize_request(request)
53
+ }
54
+ }
55
+
56
+ api = API::Sentinel.new(@config.api_url, @config.api_key, @config.secs_irrelevant_request_timeout)
57
+ response = api.relevance_intercept(payload) || {}
58
+
59
+ relevant = response.key?("relevant") ? response["relevant"] : true
60
+ reason = relevant ? nil : (response["reason"] || "Irrelevant request blocked.")
61
+
62
+ [relevant, reason]
63
+ end
64
+
65
+ def normalize_request(request)
66
+ # Remove Procs before JSON creation since the
67
+ # procs or methods will fail for normalization.
68
+ # We know that OpenAI sends a proc on their
69
+ # request, so this is mostly for that.
70
+ cleaned = remove_procs(request)
71
+ json_obj = JSON.generate(cleaned)
72
+ JSON.parse(json_obj)
73
+ rescue StandardError
74
+ request
75
+ end
76
+
77
+ def remove_procs(obj)
78
+ case obj
79
+ when Hash
80
+ obj.transform_values { |v| remove_procs(v) }
81
+ when Array
82
+ obj.map { |v| remove_procs(v) }
83
+ when Proc, Method
84
+ true
85
+ else
86
+ # Return true object for any callable object; otherwise return obj.
87
+ obj.respond_to?(:call) || obj
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Payloop
4
- VERSION = "0.0.4"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -6,9 +6,10 @@ module Payloop
6
6
  module Wrappers
7
7
  # Wrapper for Anthropic Ruby client
8
8
  class Anthropic
9
- def initialize(config, collector)
9
+ def initialize(config, collector, sentinel = nil)
10
10
  @config = config
11
11
  @collector = collector
12
+ @sentinel = sentinel
12
13
  end
13
14
 
14
15
  def register(client)
@@ -20,6 +21,7 @@ module Payloop
20
21
  # Store references in client instance
21
22
  client.instance_variable_set(:@payloop_config, @config)
22
23
  client.instance_variable_set(:@payloop_collector, @collector)
24
+ client.instance_variable_set(:@payloop_sentinel, @sentinel)
23
25
  client.instance_variable_set(:@payloop_registered, true)
24
26
 
25
27
  # Wrap the messages method
@@ -43,6 +45,7 @@ module Payloop
43
45
  # Store references on the messages resource (needed for Base module)
44
46
  messages_resource.instance_variable_set(:@payloop_config, client.instance_variable_get(:@payloop_config))
45
47
  messages_resource.instance_variable_set(:@payloop_collector, client.instance_variable_get(:@payloop_collector))
48
+ messages_resource.instance_variable_set(:@payloop_sentinel, client.instance_variable_get(:@payloop_sentinel))
46
49
 
47
50
  # Store the original create method
48
51
  original_create = messages_resource.method(:create)
@@ -54,6 +57,11 @@ module Payloop
54
57
 
55
58
  start_time = Time.now
56
59
 
60
+ # Extract parameters for sentinel and analytics
61
+ params = kwargs.any? ? kwargs : (args.first || {})
62
+ sentinel = instance_variable_get(:@payloop_sentinel)
63
+ sentinel&.raise_if_irrelevant!(title: ANTHROPIC_CLIENT_TITLE, request: params)
64
+
57
65
  # Call original method
58
66
  response = if kwargs.any?
59
67
  original_create.call(**kwargs, &block)
@@ -61,9 +69,6 @@ module Payloop
61
69
  original_create.call(*args, &block)
62
70
  end
63
71
 
64
- # Extract parameters for analytics
65
- params = kwargs.any? ? kwargs : (args.first || {})
66
-
67
72
  # Submit analytics
68
73
  payloop_submit_analytics(
69
74
  method: :create,
@@ -76,6 +81,9 @@ module Payloop
76
81
  )
77
82
 
78
83
  response
84
+ rescue PayloopRequestInterceptedError => e
85
+ # We don't want to send intercepts to collector
86
+ raise e
79
87
  rescue StandardError => e
80
88
  params = kwargs.any? ? kwargs : (args.first || {})
81
89
 
@@ -6,7 +6,7 @@ module Payloop
6
6
  module Wrappers
7
7
  # Base functionality for all provider wrappers
8
8
  module Base
9
- def payloop_wrap_method(method_name, provider_name)
9
+ def payloop_wrap_method(method_name, _provider_name)
10
10
  return if method(method_name).source_location&.first&.include?("payloop")
11
11
 
12
12
  original_method = instance_method(method_name)
@@ -43,7 +43,8 @@ module Payloop
43
43
  end
44
44
  end
45
45
 
46
- def payloop_submit_analytics(method:, args:, kwargs:, response:, start_time:, end_time:, provider: nil, title: nil)
46
+ def payloop_submit_analytics(method:, args:, kwargs:, response:, start_time:,
47
+ end_time:, provider: nil, title: nil)
47
48
  collector = instance_variable_get(:@payloop_collector)
48
49
  config = instance_variable_get(:@payloop_config)
49
50
 
@@ -63,7 +64,8 @@ module Payloop
63
64
  collector.submit_async(payload)
64
65
  end
65
66
 
66
- def payloop_submit_error_analytics(method:, args:, kwargs:, error:, start_time:, end_time:, provider: nil, title: nil)
67
+ def payloop_submit_error_analytics(method:, args:, kwargs:, error:, start_time:,
68
+ end_time:, provider: nil, title: nil)
67
69
  collector = instance_variable_get(:@payloop_collector)
68
70
  config = instance_variable_get(:@payloop_config)
69
71
 
@@ -135,9 +137,7 @@ module Payloop
135
137
 
136
138
  # Normalize stream parameter: convert Proc to boolean true
137
139
  # This ensures streaming requests are properly serialized to JSON
138
- if query.is_a?(Hash) && query[:stream].is_a?(Proc)
139
- query[:stream] = true
140
- end
140
+ query[:stream] = true if query.is_a?(Hash) && query[:stream].is_a?(Proc)
141
141
 
142
142
  query
143
143
  rescue StandardError
@@ -146,7 +146,7 @@ module Payloop
146
146
 
147
147
  def extract_response(response)
148
148
  case response
149
- when Hash
149
+ when Hash, Array
150
150
  deep_copy(response)
151
151
  when String
152
152
  { text: response }
@@ -206,7 +206,16 @@ module Payloop
206
206
  end
207
207
  end
208
208
 
209
- def build_payload(query:, response:, start_time:, end_time:, config:, status:, provider: nil, title: nil, exception: nil)
209
+ # Build the payload that is sent to the collector API which then sends the payload
210
+ # to the backend.
211
+ # - conversion.client - Routes the payload to the correct service
212
+ # in the backend. So, the title and provider values are known values. (e.g google,
213
+ # openai, and anthropic) are common title values.
214
+ # - conversion.query - Holds the request information.
215
+ # - conversion.response - Holds the response from the LLM. This is different for each LLM,
216
+ # and the different services handle them on the backend.
217
+ def build_payload(query:, response:, start_time:, end_time:, config:, status:,
218
+ provider: nil, title: nil, exception: nil)
210
219
  {
211
220
  attribution: config.attribution&.to_h,
212
221
  conversation: {
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+
5
+ module Payloop
6
+ module Wrappers
7
+ # Wrapper for Google GenerativeAI Ruby client (gemini-ai gem version 4.3.0)
8
+ class GeminiAI
9
+ def initialize(config, collector, sentinel = nil)
10
+ @config = config
11
+ @collector = collector
12
+ @sentinel = sentinel
13
+ end
14
+
15
+ def register(client)
16
+ validate_client!(client)
17
+
18
+ # Prevent double registration
19
+ return client if client.instance_variable_defined?(:@payloop_registered)
20
+
21
+ # Store references in client instance
22
+ client.instance_variable_set(:@payloop_config, @config)
23
+ client.instance_variable_set(:@payloop_collector, @collector)
24
+ client.instance_variable_set(:@payloop_sentinel, @sentinel)
25
+ client.instance_variable_set(:@payloop_registered, true)
26
+
27
+ # Wrap the generate_content and stream_generate_content methods
28
+ wrap_generate_content_method(client)
29
+ wrap_stream_generate_content_method(client)
30
+
31
+ client
32
+ end
33
+
34
+ private
35
+
36
+ def validate_client!(client)
37
+ return if client.respond_to?(:generate_content) && client.respond_to?(:stream_generate_content)
38
+
39
+ raise RegistrationError,
40
+ "Client does not appear to be a valid Gemini AI client (missing generate_content or stream_generate_content method)" # rubocop:disable Layout/LineLength
41
+ end
42
+
43
+ def wrap_generate_content_method(client)
44
+ client.singleton_class.class_eval do
45
+ include Base
46
+
47
+ alias_method :original_generate_content, :generate_content
48
+
49
+ define_method(:generate_content) do |*args, **kwargs, &block|
50
+ # Handle both positional hash and keyword arguments
51
+ parameters = if args.first.is_a?(Hash)
52
+ args.first
53
+ elsif kwargs.any?
54
+ kwargs
55
+ else
56
+ {}
57
+ end
58
+
59
+ start_time = Time.now
60
+
61
+ sentinel = instance_variable_get(:@payloop_sentinel)
62
+ sentinel&.raise_if_irrelevant!(title: GOOGLE_CLIENT_TITLE, request: parameters)
63
+
64
+ # Call original method
65
+ response = original_generate_content(*args, **kwargs, &block)
66
+
67
+ # Submit analytics
68
+ payloop_submit_analytics(
69
+ method: :generate_content,
70
+ args: [],
71
+ kwargs: parameters,
72
+ response: response,
73
+ start_time: start_time,
74
+ end_time: Time.now,
75
+ title: GOOGLE_CLIENT_TITLE
76
+ )
77
+
78
+ response
79
+ rescue PayloopRequestInterceptedError => e
80
+ # We don't want to send intercepts to collector
81
+ raise e
82
+ rescue StandardError => e
83
+ payloop_submit_error_analytics(
84
+ method: :generate_content,
85
+ args: [],
86
+ kwargs: parameters,
87
+ error: e,
88
+ start_time: start_time,
89
+ end_time: Time.now,
90
+ title: GOOGLE_CLIENT_TITLE
91
+ )
92
+
93
+ raise e
94
+ end
95
+ end
96
+ end
97
+
98
+ # Handles the stream_generate_content from the client. Note, that
99
+ # the Payloop backend does not expect the payload to be merged like
100
+ # the OpenAI payload is. So, all the results are just sent as an
101
+ # array.
102
+ def wrap_stream_generate_content_method(client)
103
+ client.singleton_class.class_eval do
104
+ include Base
105
+
106
+ alias_method :original_stream_generate_content, :stream_generate_content
107
+
108
+ define_method(:stream_generate_content) do |*args, **kwargs, &block|
109
+ # Handle both positional hash and keyword arguments
110
+ parameters = if args.first.is_a?(Hash)
111
+ args.first
112
+ elsif kwargs.any?
113
+ kwargs
114
+ else
115
+ {}
116
+ end
117
+
118
+ start_time = Time.now
119
+
120
+ sentinel = instance_variable_get(:@payloop_sentinel)
121
+ sentinel&.raise_if_irrelevant!(title: GOOGLE_CLIENT_TITLE, request: parameters)
122
+
123
+ # Call original method with wrapped block
124
+ response = original_stream_generate_content(*args, **kwargs, &block)
125
+
126
+ # Submit analytics with accumulated response
127
+ payloop_submit_analytics(
128
+ method: :stream_generate_content,
129
+ args: [],
130
+ kwargs: parameters,
131
+ response: response,
132
+ start_time: start_time,
133
+ end_time: Time.now,
134
+ title: GOOGLE_CLIENT_TITLE
135
+ )
136
+
137
+ response
138
+ rescue PayloopRequestInterceptedError => e
139
+ # We don't want to send intercepts to collector
140
+ raise e
141
+ rescue StandardError => e
142
+ payloop_submit_error_analytics(
143
+ method: :stream_generate_content,
144
+ args: [],
145
+ kwargs: parameters,
146
+ error: e,
147
+ start_time: start_time,
148
+ end_time: Time.now,
149
+ title: GOOGLE_CLIENT_TITLE
150
+ )
151
+
152
+ raise e
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -4,11 +4,12 @@ require_relative "constants"
4
4
 
5
5
  module Payloop
6
6
  module Wrappers
7
- # Wrapper for Google GenerativeAI Ruby client (google-genai gem)
7
+ # Wrapper for Google GenerativeAI Ruby client (google-genai library version 0.1)
8
8
  class Google
9
- def initialize(config, collector)
9
+ def initialize(config, collector, sentinel = nil)
10
10
  @config = config
11
11
  @collector = collector
12
+ @sentinel = sentinel
12
13
  end
13
14
 
14
15
  def register(client)
@@ -23,6 +24,7 @@ module Payloop
23
24
  # Store references in client instance
24
25
  client.instance_variable_set(:@payloop_config, @config)
25
26
  client.instance_variable_set(:@payloop_collector, @collector)
27
+ client.instance_variable_set(:@payloop_sentinel, @sentinel)
26
28
  client.instance_variable_set(:@payloop_registered, true)
27
29
 
28
30
  # Wrap the generate_content method
@@ -61,6 +63,7 @@ module Payloop
61
63
  # Store references on the models resource (needed for Base module)
62
64
  models_resource.instance_variable_set(:@payloop_config, client.instance_variable_get(:@payloop_config))
63
65
  models_resource.instance_variable_set(:@payloop_collector, client.instance_variable_get(:@payloop_collector))
66
+ models_resource.instance_variable_set(:@payloop_sentinel, client.instance_variable_get(:@payloop_sentinel))
64
67
 
65
68
  # Store the original generate_content method
66
69
  original_generate_content = models_resource.method(:generate_content)
@@ -72,6 +75,12 @@ module Payloop
72
75
 
73
76
  start_time = Time.now
74
77
 
78
+ # Extract parameters for analytics
79
+ params = kwargs.any? ? kwargs : (args.first || {})
80
+
81
+ sentinel = instance_variable_get(:@payloop_sentinel)
82
+ sentinel&.raise_if_irrelevant!(title: GOOGLE_CLIENT_TITLE, request: params)
83
+
75
84
  # Call original method
76
85
  response = if kwargs.any?
77
86
  original_generate_content.call(**kwargs, &block)
@@ -79,9 +88,6 @@ module Payloop
79
88
  original_generate_content.call(*args, &block)
80
89
  end
81
90
 
82
- # Extract parameters for analytics
83
- params = kwargs.any? ? kwargs : (args.first || {})
84
-
85
91
  # Submit analytics
86
92
  payloop_submit_analytics(
87
93
  method: :generate_content,
@@ -94,6 +100,9 @@ module Payloop
94
100
  )
95
101
 
96
102
  response
103
+ rescue PayloopRequestInterceptedError => e
104
+ # We don't want to send intercepts to collector
105
+ raise e
97
106
  rescue StandardError => e
98
107
  params = kwargs.any? ? kwargs : (args.first || {})
99
108
 
@@ -6,9 +6,10 @@ module Payloop
6
6
  module Wrappers
7
7
  # Wrapper for OpenAI Ruby client (ruby-openai gem)
8
8
  class OpenAI
9
- def initialize(config, collector)
9
+ def initialize(config, collector, sentinel = nil)
10
10
  @config = config
11
11
  @collector = collector
12
+ @sentinel = sentinel
12
13
  end
13
14
 
14
15
  def register(client)
@@ -20,6 +21,7 @@ module Payloop
20
21
  # Store references in client instance
21
22
  client.instance_variable_set(:@payloop_config, @config)
22
23
  client.instance_variable_set(:@payloop_collector, @collector)
24
+ client.instance_variable_set(:@payloop_sentinel, @sentinel)
23
25
  client.instance_variable_set(:@payloop_registered, true)
24
26
 
25
27
  # Wrap the chat method
@@ -45,6 +47,9 @@ module Payloop
45
47
  define_method(:chat) do |parameters: {}|
46
48
  start_time = Time.now
47
49
 
50
+ sentinel = instance_variable_get(:@payloop_sentinel)
51
+ sentinel&.raise_if_irrelevant!(title: OPENAI_CLIENT_TITLE, request: parameters)
52
+
48
53
  # Detect streaming and wrap callback to accumulate chunks
49
54
  streaming = parameters[:stream].is_a?(Proc)
50
55
  accumulated_response = {} if streaming
@@ -83,6 +88,9 @@ module Payloop
83
88
  )
84
89
 
85
90
  response
91
+ rescue PayloopRequestInterceptedError => e
92
+ # We don't want to send intercepts to collector
93
+ raise e
86
94
  rescue StandardError => e
87
95
  payloop_submit_error_analytics(
88
96
  method: :chat,
data/lib/payloop.rb CHANGED
@@ -3,13 +3,16 @@
3
3
  require_relative "payloop/version"
4
4
  require_relative "payloop/errors"
5
5
  require_relative "payloop/config"
6
+ require_relative "payloop/sentinel"
6
7
  require_relative "payloop/attribution"
7
8
  require_relative "payloop/collector"
8
9
  require_relative "payloop/wrappers/base"
9
10
  require_relative "payloop/wrappers/openai"
10
11
  require_relative "payloop/wrappers/anthropic"
11
12
  require_relative "payloop/wrappers/google"
13
+ require_relative "payloop/wrappers/geminiai"
12
14
  require_relative "payloop/api/base"
15
+ require_relative "payloop/api/sentinel"
13
16
  require_relative "payloop/api/workflows"
14
17
  require_relative "payloop/api/invocation"
15
18
  require_relative "payloop/api/workflow"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: payloop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Payloop
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-29 00:00:00.000000000 Z
11
+ date: 2026-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -37,6 +37,7 @@ files:
37
37
  - lib/payloop.rb
38
38
  - lib/payloop/api/base.rb
39
39
  - lib/payloop/api/invocation.rb
40
+ - lib/payloop/api/sentinel.rb
40
41
  - lib/payloop/api/workflow.rb
41
42
  - lib/payloop/api/workflows.rb
42
43
  - lib/payloop/attribution.rb
@@ -44,10 +45,12 @@ files:
44
45
  - lib/payloop/collector.rb
45
46
  - lib/payloop/config.rb
46
47
  - lib/payloop/errors.rb
48
+ - lib/payloop/sentinel.rb
47
49
  - lib/payloop/version.rb
48
50
  - lib/payloop/wrappers/anthropic.rb
49
51
  - lib/payloop/wrappers/base.rb
50
52
  - lib/payloop/wrappers/constants.rb
53
+ - lib/payloop/wrappers/geminiai.rb
51
54
  - lib/payloop/wrappers/google.rb
52
55
  - lib/payloop/wrappers/openai.rb
53
56
  homepage: https://trypayloop.com