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 +4 -4
- data/CHANGELOG.md +11 -1
- data/README.md +1 -1
- data/lib/payloop/client.rb +6 -0
- data/lib/payloop/version.rb +1 -1
- data/lib/payloop/wrappers/anthropic.rb +3 -0
- data/lib/payloop/wrappers/constants.rb +1 -0
- data/lib/payloop/wrappers/geminiai.rb +6 -0
- data/lib/payloop/wrappers/google.rb +3 -0
- data/lib/payloop/wrappers/openai.rb +3 -0
- data/lib/payloop/wrappers/ruby_llm.rb +156 -0
- data/lib/payloop.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7783fa796b37bc909b7902b2477e59eeadb7b28460f4867cece51629b50e114f
|
|
4
|
+
data.tar.gz: 69871d85d4066f899cf697179ed3468b7adad4420319ee5c2c50f0a11314f8b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
37
|
+
VERSION = "0.2.0"
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
2. **Update the CHANGELOG** in [CHANGELOG.md](CHANGELOG.md):
|
data/lib/payloop/client.rb
CHANGED
|
@@ -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(
|
data/lib/payloop/version.rb
CHANGED
|
@@ -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,
|
|
@@ -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.
|
|
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-
|
|
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
|