payloop 0.1.0 → 0.2.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: 046f4c8d567e9eb4bf8f6797ef0a693f90a279f04de29d9d875030b264323788
4
- data.tar.gz: 1fa3b863d95bf6568bb78b0df6819bde803449786ecad95c97c8dde57394689e
3
+ metadata.gz: 7783fa796b37bc909b7902b2477e59eeadb7b28460f4867cece51629b50e114f
4
+ data.tar.gz: 69871d85d4066f899cf697179ed3468b7adad4420319ee5c2c50f0a11314f8b3
5
5
  SHA512:
6
- metadata.gz: 97cf236a7d99c0cef892e2f8c3d615d47bbba504073d5f64a67b11023859c5ed1ff6ddc67a43b3287ac85ffe37d31e633f1531078ac0ac6ff69f795c30921566
7
- data.tar.gz: 205bb28b079d0459021cfb8cd6f1032df7ff7d1c351d9280c4bad530d1fdc73a8e07943d2fc6b36b60bfbcc23df66c1c1c607577e03f6f9fe2af7a7442225291
6
+ metadata.gz: afc394253b0c42ea0e4bf41dc9c7cfee11dad20dccffbd6689e9c5e3f19ccde79d2a1b1b807b8b07dab83562f8242286047efe6716de476882134be384d1610d
7
+ data.tar.gz: c98627c98fedec6e77e0b152614dcc1bce1adebe149344c344e10f99e8292607aa35e600da32cb29f9c74d77541de1474b79b5cfdfb519e129b0fdc6b8bd7aeb
data/CHANGELOG.md CHANGED
@@ -40,4 +40,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
40
40
 
41
41
  ### Added
42
42
  - Support for Sentinel intercept.
43
- - Support for Gemini AI client tracking.
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
49
+
50
+ ## [0.2.0] - 2026-04-08
51
+
52
+ ### Added
53
+ - Support for the RubyLLM library
data/README.md CHANGED
@@ -34,7 +34,7 @@ To release a new version of the Payloop Ruby gem (e.g., version 0.0.2):
34
34
 
35
35
  1. **Update the version number** in [lib/payloop/version.rb](lib/payloop/version.rb):
36
36
  ```ruby
37
- VERSION = "0.0.2"
37
+ VERSION = "0.2.0"
38
38
  ```
39
39
 
40
40
  2. **Update the CHANGELOG** in [CHANGELOG.md](CHANGELOG.md):
@@ -46,6 +46,12 @@ module Payloop
46
46
  @geminiai ||= Wrappers::GeminiAI.new(@config, @collector, @sentinel)
47
47
  end
48
48
 
49
+ # RubyLLM provider wrapper (ruby_llm gem)
50
+ # Supports any provider available via RubyLLM (Anthropic, OpenAI, Google, etc.)
51
+ def ruby_llm
52
+ @ruby_llm ||= Wrappers::RubyLLM.new(@config, @collector, @sentinel)
53
+ end
54
+
49
55
  # Set attribution for cost tracking
50
56
  def attribution(parent_id:, parent_name: nil, subsidiary_id: nil, subsidiary_name: nil)
51
57
  attr = Attribution.new(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Payloop
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -81,6 +81,9 @@ module Payloop
81
81
  )
82
82
 
83
83
  response
84
+ rescue PayloopRequestInterceptedError => e
85
+ # We don't want to send intercepts to collector
86
+ raise e
84
87
  rescue StandardError => e
85
88
  params = kwargs.any? ? kwargs : (args.first || {})
86
89
 
@@ -6,5 +6,6 @@ module Payloop
6
6
  OPENAI_CLIENT_TITLE = "openai"
7
7
  ANTHROPIC_CLIENT_TITLE = "anthropic"
8
8
  GOOGLE_CLIENT_TITLE = "google"
9
+ RUBY_LLM_PROVIDER = "ruby_llm"
9
10
  end
10
11
  end
@@ -76,6 +76,9 @@ module Payloop
76
76
  )
77
77
 
78
78
  response
79
+ rescue PayloopRequestInterceptedError => e
80
+ # We don't want to send intercepts to collector
81
+ raise e
79
82
  rescue StandardError => e
80
83
  payloop_submit_error_analytics(
81
84
  method: :generate_content,
@@ -132,6 +135,9 @@ module Payloop
132
135
  )
133
136
 
134
137
  response
138
+ rescue PayloopRequestInterceptedError => e
139
+ # We don't want to send intercepts to collector
140
+ raise e
135
141
  rescue StandardError => e
136
142
  payloop_submit_error_analytics(
137
143
  method: :stream_generate_content,
@@ -100,6 +100,9 @@ module Payloop
100
100
  )
101
101
 
102
102
  response
103
+ rescue PayloopRequestInterceptedError => e
104
+ # We don't want to send intercepts to collector
105
+ raise e
103
106
  rescue StandardError => e
104
107
  params = kwargs.any? ? kwargs : (args.first || {})
105
108
 
@@ -88,6 +88,9 @@ module Payloop
88
88
  )
89
89
 
90
90
  response
91
+ rescue PayloopRequestInterceptedError => e
92
+ # We don't want to send intercepts to collector
93
+ raise e
91
94
  rescue StandardError => e
92
95
  payloop_submit_error_analytics(
93
96
  method: :chat,
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+
5
+ module Payloop
6
+ module Wrappers
7
+ # Wrapper for RubyLLM (ruby_llm gem)
8
+ # Supports any provider available via RubyLLM (Anthropic, OpenAI, Google, etc.)
9
+ # Returns raw response and raw formatted query to backend so that the existing
10
+ # extractors can be used.
11
+ class RubyLLM
12
+ def initialize(config, collector, sentinel = nil)
13
+ @config = config
14
+ @collector = collector
15
+ @sentinel = sentinel
16
+ end
17
+
18
+ def register(client)
19
+ validate_client!(client)
20
+
21
+ # Prevent double registration
22
+ return client if client.instance_variable_defined?(:@payloop_registered)
23
+
24
+ # Store references in the client instance
25
+ client.instance_variable_set(:@payloop_config, @config)
26
+ client.instance_variable_set(:@payloop_collector, @collector)
27
+ client.instance_variable_set(:@payloop_sentinel, @sentinel)
28
+ client.instance_variable_set(:@payloop_registered, true)
29
+
30
+ # Ask method handles both streaming and non-streaming cases unlike some others.
31
+ wrap_ask_method(client)
32
+
33
+ client
34
+ end
35
+
36
+ private
37
+
38
+ def validate_client!(client)
39
+ return if client.respond_to?(:ask)
40
+
41
+ raise RegistrationError,
42
+ "Client does not appear to be a valid RubyLLM client (missing ask method)"
43
+ end
44
+
45
+ def wrap_ask_method(client)
46
+ # Capture the wrapper instance in a local variable so it's accessible as a closure
47
+ # inside the block, where self will no longer refer to it.
48
+ wrapper = self
49
+
50
+ client.singleton_class.class_eval do
51
+ include Base
52
+
53
+ alias_method :original_ask, :ask
54
+
55
+ define_method(:ask) do |message, with: nil, &block|
56
+ start_time = Time.now
57
+ query = {} # Fallback for error analytics if an exception occurs before build_query completes
58
+
59
+ sentinel = instance_variable_get(:@payloop_sentinel)
60
+ provider = model.provider
61
+ title = provider == "gemini" ? GOOGLE_CLIENT_TITLE : provider
62
+
63
+ # Capture existing messages (e.g. system instruction) before the call
64
+ # adds the new user message to history.
65
+ existing_msgs = messages.map { |m| { role: m.role.to_s, content: m.content.to_s } }
66
+ all_msgs = existing_msgs + [{ role: "user", content: message.to_s }]
67
+
68
+ # Build a query in the provider's native format so the backend extractor can parse it.
69
+ query = wrapper.send(:build_query, provider, model.id, all_msgs, tools)
70
+
71
+ # Pass the actual provider title (e.g. "anthropic", "openai", "google") so the
72
+ # sentinel backend can route to the correct classifier for this request format.
73
+ sentinel&.raise_if_irrelevant!(title: title, request: query)
74
+
75
+ response = original_ask(message, with: with, &block)
76
+
77
+ payloop_submit_analytics(
78
+ method: :ask,
79
+ args: [],
80
+ kwargs: query,
81
+ response: response.raw&.body,
82
+ start_time: start_time,
83
+ end_time: Time.now,
84
+ title: title,
85
+ provider: RUBY_LLM_PROVIDER
86
+ )
87
+
88
+ response
89
+ rescue PayloopRequestInterceptedError => e
90
+ raise e
91
+ rescue StandardError => e
92
+ payloop_submit_error_analytics(
93
+ method: :ask,
94
+ args: [],
95
+ kwargs: query,
96
+ error: e,
97
+ start_time: start_time,
98
+ end_time: Time.now,
99
+ title: title,
100
+ provider: RUBY_LLM_PROVIDER
101
+ )
102
+ raise e
103
+ end
104
+ end
105
+ end
106
+
107
+ # Dispatches to the appropriate provider-native query builder.
108
+ def build_query(provider, model_id, messages, tools)
109
+ if provider == "gemini"
110
+ build_google_query(model_id, messages, tools)
111
+ else
112
+ # Anthropic and OpenAI have the same query format
113
+ build_messages_query(model_id, messages, tools)
114
+ end
115
+ end
116
+
117
+ # Builds an OpenAI-compatible query (used for Anthropic and OpenAI providers).
118
+ def build_messages_query(model_id, messages, tools)
119
+ query = { model: model_id, messages: messages }
120
+ unless tools.empty?
121
+ query[:tools] = tools.values.map do |t|
122
+ { name: t.name, description: t.description, parameters: t.params_schema }
123
+ end
124
+ end
125
+ query
126
+ end
127
+
128
+ # Builds a Google-native query with contents/systemInstruction structure.
129
+ def build_google_query(model_id, messages, tools)
130
+ system_msgs = messages.select { |m| m[:role] == "system" }
131
+ content_msgs = messages.reject { |m| m[:role] == "system" }
132
+
133
+ query = {
134
+ model: model_id,
135
+ contents: content_msgs.map do |m|
136
+ google_role = m[:role] == "assistant" ? "model" : m[:role]
137
+ { role: google_role, parts: [{ text: m[:content] }] }
138
+ end
139
+ }
140
+
141
+ if system_msgs.any?
142
+ system_text = system_msgs.map { |m| m[:content] }.join("\n")
143
+ query[:systemInstruction] = { parts: [{ text: system_text }] }
144
+ end
145
+
146
+ unless tools.empty?
147
+ query[:tools] = tools.values.map do |t|
148
+ { name: t.name, description: t.description, parameters: t.params_schema }
149
+ end
150
+ end
151
+
152
+ query
153
+ end
154
+ end
155
+ end
156
+ end
data/lib/payloop.rb CHANGED
@@ -11,6 +11,7 @@ require_relative "payloop/wrappers/openai"
11
11
  require_relative "payloop/wrappers/anthropic"
12
12
  require_relative "payloop/wrappers/google"
13
13
  require_relative "payloop/wrappers/geminiai"
14
+ require_relative "payloop/wrappers/ruby_llm"
14
15
  require_relative "payloop/api/base"
15
16
  require_relative "payloop/api/sentinel"
16
17
  require_relative "payloop/api/workflows"
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Payloop
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-03 00:00:00.000000000 Z
11
+ date: 2026-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -53,6 +53,7 @@ files:
53
53
  - lib/payloop/wrappers/geminiai.rb
54
54
  - lib/payloop/wrappers/google.rb
55
55
  - lib/payloop/wrappers/openai.rb
56
+ - lib/payloop/wrappers/ruby_llm.rb
56
57
  homepage: https://trypayloop.com
57
58
  licenses:
58
59
  - MIT