payloop 0.0.4 → 0.1.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: 0d134effe2b909cc928b4dfc80149d3c9dcde238be7630bb37dba541fa33be0a
4
- data.tar.gz: ddf58b4672381d25ab90b349c6bf29391e4fe099f9e304ba20f5169d3ee6bf1c
3
+ metadata.gz: 046f4c8d567e9eb4bf8f6797ef0a693f90a279f04de29d9d875030b264323788
4
+ data.tar.gz: 1fa3b863d95bf6568bb78b0df6819bde803449786ecad95c97c8dde57394689e
5
5
  SHA512:
6
- metadata.gz: 3b943601582c65f45f66209c6c7aa37c0041f3f961130b3fce5109f09369359d05cc2c7264ca85ffea99b067c95b7c1419d165bf11d847fb994f43a29697c4df
7
- data.tar.gz: 2d4ff576bccc13c526dc80ae8ab9b2dfb72a9480f3f6c88ed261a723a217271867a3f6f88e2083f8bb5a65813362b5603e8396bd5a6c944885cbf1c4010fe963
6
+ metadata.gz: 97cf236a7d99c0cef892e2f8c3d615d47bbba504073d5f64a67b11023859c5ed1ff6ddc67a43b3287ac85ffe37d31e633f1531078ac0ac6ff69f795c30921566
7
+ data.tar.gz: 205bb28b079d0459021cfb8cd6f1032df7ff7d1c351d9280c4bad530d1fdc73a8e07943d2fc6b36b60bfbcc23df66c1c1c607577e03f6f9fe2af7a7442225291
data/CHANGELOG.md CHANGED
@@ -35,3 +35,9 @@ 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.
@@ -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.0"
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,
@@ -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,152 @@
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 StandardError => e
80
+ payloop_submit_error_analytics(
81
+ method: :generate_content,
82
+ args: [],
83
+ kwargs: parameters,
84
+ error: e,
85
+ start_time: start_time,
86
+ end_time: Time.now,
87
+ title: GOOGLE_CLIENT_TITLE
88
+ )
89
+
90
+ raise e
91
+ end
92
+ end
93
+ end
94
+
95
+ # Handles the stream_generate_content from the client. Note, that
96
+ # the Payloop backend does not expect the payload to be merged like
97
+ # the OpenAI payload is. So, all the results are just sent as an
98
+ # array.
99
+ def wrap_stream_generate_content_method(client)
100
+ client.singleton_class.class_eval do
101
+ include Base
102
+
103
+ alias_method :original_stream_generate_content, :stream_generate_content
104
+
105
+ define_method(:stream_generate_content) do |*args, **kwargs, &block|
106
+ # Handle both positional hash and keyword arguments
107
+ parameters = if args.first.is_a?(Hash)
108
+ args.first
109
+ elsif kwargs.any?
110
+ kwargs
111
+ else
112
+ {}
113
+ end
114
+
115
+ start_time = Time.now
116
+
117
+ sentinel = instance_variable_get(:@payloop_sentinel)
118
+ sentinel&.raise_if_irrelevant!(title: GOOGLE_CLIENT_TITLE, request: parameters)
119
+
120
+ # Call original method with wrapped block
121
+ response = original_stream_generate_content(*args, **kwargs, &block)
122
+
123
+ # Submit analytics with accumulated response
124
+ payloop_submit_analytics(
125
+ method: :stream_generate_content,
126
+ args: [],
127
+ kwargs: parameters,
128
+ response: response,
129
+ start_time: start_time,
130
+ end_time: Time.now,
131
+ title: GOOGLE_CLIENT_TITLE
132
+ )
133
+
134
+ response
135
+ rescue StandardError => e
136
+ payloop_submit_error_analytics(
137
+ method: :stream_generate_content,
138
+ args: [],
139
+ kwargs: parameters,
140
+ error: e,
141
+ start_time: start_time,
142
+ end_time: Time.now,
143
+ title: GOOGLE_CLIENT_TITLE
144
+ )
145
+
146
+ raise e
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ 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,
@@ -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
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.0
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-03 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