payloop 0.0.2 → 0.0.4
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 +10 -0
- data/lib/payloop/client.rb +1 -6
- data/lib/payloop/version.rb +1 -1
- data/lib/payloop/wrappers/base.rb +89 -5
- data/lib/payloop/wrappers/google.rb +18 -1
- data/lib/payloop/wrappers/openai.rb +24 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d134effe2b909cc928b4dfc80149d3c9dcde238be7630bb37dba541fa33be0a
|
|
4
|
+
data.tar.gz: ddf58b4672381d25ab90b349c6bf29391e4fe099f9e304ba20f5169d3ee6bf1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b943601582c65f45f66209c6c7aa37c0041f3f961130b3fce5109f09369359d05cc2c7264ca85ffea99b067c95b7c1419d165bf11d847fb994f43a29697c4df
|
|
7
|
+
data.tar.gz: 2d4ff576bccc13c526dc80ae8ab9b2dfb72a9480f3f6c88ed261a723a217271867a3f6f88e2083f8bb5a65813362b5603e8396bd5a6c944885cbf1c4010fe963
|
data/CHANGELOG.md
CHANGED
|
@@ -25,3 +25,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
25
25
|
|
|
26
26
|
### Fixed
|
|
27
27
|
- Corrected Payloop request payload structure as needed
|
|
28
|
+
|
|
29
|
+
## [0.0.3] - 2025-10-27
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Correct Payloop request payload structure for Google GenAI
|
|
33
|
+
|
|
34
|
+
## [0.0.4] - 2025-10-28
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- Correct Payloop request payload structure for OpenAI streaming
|
data/lib/payloop/client.rb
CHANGED
|
@@ -36,12 +36,7 @@ module Payloop
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Set attribution for cost tracking
|
|
39
|
-
def attribution(parent_id:, parent_name: nil, subsidiary_id: nil, subsidiary_name: nil
|
|
40
|
-
parent_uuid: nil, subsidiary_uuid: nil)
|
|
41
|
-
# Support deprecated parameters
|
|
42
|
-
parent_id ||= parent_uuid
|
|
43
|
-
subsidiary_id ||= subsidiary_uuid
|
|
44
|
-
|
|
39
|
+
def attribution(parent_id:, parent_name: nil, subsidiary_id: nil, subsidiary_name: nil)
|
|
45
40
|
attr = Attribution.new(
|
|
46
41
|
parent_id: parent_id,
|
|
47
42
|
parent_name: parent_name,
|
data/lib/payloop/version.rb
CHANGED
|
@@ -84,11 +84,62 @@ module Payloop
|
|
|
84
84
|
collector.submit_async(payload)
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
def payloop_merge_streaming_chunk(accumulated, chunk)
|
|
88
|
+
chunk.each do |key, value|
|
|
89
|
+
if accumulated.key?(key)
|
|
90
|
+
case accumulated[key]
|
|
91
|
+
when Hash
|
|
92
|
+
payloop_merge_streaming_chunk(accumulated[key], value) if value.is_a?(Hash)
|
|
93
|
+
when Array
|
|
94
|
+
# Concatenate arrays (matches Python SDK behavior: data[key].extend(chunk_value))
|
|
95
|
+
accumulated[key].concat(value) if value.is_a?(Array)
|
|
96
|
+
else
|
|
97
|
+
accumulated[key] = value
|
|
98
|
+
end
|
|
99
|
+
else
|
|
100
|
+
accumulated[key] = value
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
accumulated
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def payloop_normalize_openai_chunk(chunk)
|
|
107
|
+
# Normalize OpenAI streaming chunks to match Python SDK format
|
|
108
|
+
# Ensure delta objects have all keys present with null values
|
|
109
|
+
return chunk unless chunk.is_a?(Hash)
|
|
110
|
+
|
|
111
|
+
if chunk["choices"].is_a?(Array)
|
|
112
|
+
chunk["choices"].each do |choice|
|
|
113
|
+
next unless choice.is_a?(Hash) && choice.key?("delta")
|
|
114
|
+
|
|
115
|
+
delta = choice["delta"]
|
|
116
|
+
if delta.is_a?(Hash)
|
|
117
|
+
# Add missing keys with nil values to match Python SDK
|
|
118
|
+
delta["role"] ||= nil unless delta.key?("role")
|
|
119
|
+
delta["content"] ||= nil unless delta.key?("content")
|
|
120
|
+
delta["refusal"] ||= nil unless delta.key?("refusal")
|
|
121
|
+
delta["tool_calls"] ||= nil unless delta.key?("tool_calls")
|
|
122
|
+
delta["function_call"] ||= nil unless delta.key?("function_call")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
chunk
|
|
128
|
+
end
|
|
129
|
+
|
|
87
130
|
private
|
|
88
131
|
|
|
89
132
|
def extract_query(_method, _args, kwargs)
|
|
90
133
|
# Deep copy to avoid mutation issues
|
|
91
|
-
deep_copy(kwargs)
|
|
134
|
+
query = deep_copy(kwargs)
|
|
135
|
+
|
|
136
|
+
# Normalize stream parameter: convert Proc to boolean true
|
|
137
|
+
# 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
|
|
141
|
+
|
|
142
|
+
query
|
|
92
143
|
rescue StandardError
|
|
93
144
|
{}
|
|
94
145
|
end
|
|
@@ -100,16 +151,49 @@ module Payloop
|
|
|
100
151
|
when String
|
|
101
152
|
{ text: response }
|
|
102
153
|
else
|
|
103
|
-
|
|
154
|
+
# Try various serialization methods
|
|
155
|
+
if response.respond_to?(:as_json)
|
|
156
|
+
deep_copy(response.as_json)
|
|
157
|
+
elsif response.respond_to?(:to_h)
|
|
104
158
|
deep_copy(response.to_h)
|
|
105
159
|
elsif response.respond_to?(:to_hash)
|
|
106
160
|
deep_copy(response.to_hash)
|
|
107
161
|
else
|
|
108
|
-
|
|
162
|
+
# Extract instance variables for objects without serialization methods
|
|
163
|
+
extract_instance_variables(response)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
# If extraction fails, try to provide useful debug info
|
|
168
|
+
{ raw: response.to_s, error: e.message }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def extract_instance_variables(obj)
|
|
172
|
+
result = {}
|
|
173
|
+
obj.instance_variables.each do |var|
|
|
174
|
+
key = var.to_s.delete("@").to_sym
|
|
175
|
+
value = obj.instance_variable_get(var)
|
|
176
|
+
result[key] = serialize_value(value)
|
|
177
|
+
end
|
|
178
|
+
result
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def serialize_value(value)
|
|
182
|
+
case value
|
|
183
|
+
when Hash
|
|
184
|
+
value.transform_values { |v| serialize_value(v) }
|
|
185
|
+
when Array
|
|
186
|
+
value.map { |v| serialize_value(v) }
|
|
187
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
|
188
|
+
value
|
|
189
|
+
else
|
|
190
|
+
# Recursively extract instance variables for nested objects
|
|
191
|
+
if value.respond_to?(:instance_variables)
|
|
192
|
+
extract_instance_variables(value)
|
|
193
|
+
else
|
|
194
|
+
value.to_s
|
|
109
195
|
end
|
|
110
196
|
end
|
|
111
|
-
rescue StandardError
|
|
112
|
-
{ raw: response.to_s }
|
|
113
197
|
end
|
|
114
198
|
|
|
115
199
|
def deep_copy(obj)
|
|
@@ -4,7 +4,7 @@ require_relative "constants"
|
|
|
4
4
|
|
|
5
5
|
module Payloop
|
|
6
6
|
module Wrappers
|
|
7
|
-
# Wrapper for Google GenerativeAI Ruby client
|
|
7
|
+
# Wrapper for Google GenerativeAI Ruby client (google-genai gem)
|
|
8
8
|
class Google
|
|
9
9
|
def initialize(config, collector)
|
|
10
10
|
@config = config
|
|
@@ -17,6 +17,9 @@ module Payloop
|
|
|
17
17
|
# Prevent double registration
|
|
18
18
|
return client if client.instance_variable_defined?(:@payloop_registered)
|
|
19
19
|
|
|
20
|
+
# Patch response class after client is validated (ensures gem is loaded)
|
|
21
|
+
patch_response_class!
|
|
22
|
+
|
|
20
23
|
# Store references in client instance
|
|
21
24
|
client.instance_variable_set(:@payloop_config, @config)
|
|
22
25
|
client.instance_variable_set(:@payloop_collector, @collector)
|
|
@@ -37,6 +40,20 @@ module Payloop
|
|
|
37
40
|
"Client does not appear to be a valid Google GenAI client (missing models method)"
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
# Monkey-patch the GenerateContentResponse class to include missing fields
|
|
44
|
+
def patch_response_class!
|
|
45
|
+
# Check if the constant exists and hasn't been patched yet
|
|
46
|
+
return unless defined?(::Google::Genai::Types::GenerateContentResponse)
|
|
47
|
+
return if ::Google::Genai::Types::GenerateContentResponse.instance_variable_defined?(:@_payloop_patched)
|
|
48
|
+
|
|
49
|
+
::Google::Genai::Types::GenerateContentResponse.class_eval do
|
|
50
|
+
# Use camelCase to match Google's API response keys
|
|
51
|
+
attr_accessor :modelVersion, :usageMetadata, :responseId, :createTime
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
::Google::Genai::Types::GenerateContentResponse.instance_variable_set(:@_payloop_patched, true)
|
|
55
|
+
end
|
|
56
|
+
|
|
40
57
|
def wrap_generate_content_method(client)
|
|
41
58
|
# Get the models resource
|
|
42
59
|
models_resource = client.models
|
|
@@ -45,15 +45,38 @@ module Payloop
|
|
|
45
45
|
define_method(:chat) do |parameters: {}|
|
|
46
46
|
start_time = Time.now
|
|
47
47
|
|
|
48
|
+
# Detect streaming and wrap callback to accumulate chunks
|
|
49
|
+
streaming = parameters[:stream].is_a?(Proc)
|
|
50
|
+
accumulated_response = {} if streaming
|
|
51
|
+
|
|
52
|
+
if streaming
|
|
53
|
+
# Configure streaming to include usage (matches Python SDK behavior)
|
|
54
|
+
parameters[:stream_options] ||= {}
|
|
55
|
+
parameters[:stream_options][:include_usage] = true
|
|
56
|
+
|
|
57
|
+
user_callback = parameters[:stream]
|
|
58
|
+
parameters[:stream] = proc do |chunk, bytesize|
|
|
59
|
+
# Normalize chunk to match Python SDK format (add missing delta keys with nil)
|
|
60
|
+
normalized_chunk = payloop_normalize_openai_chunk(chunk)
|
|
61
|
+
# Accumulate chunk (merge into accumulated_response)
|
|
62
|
+
payloop_merge_streaming_chunk(accumulated_response, normalized_chunk)
|
|
63
|
+
# Call user's original callback with original chunk (don't modify user's data)
|
|
64
|
+
user_callback.call(chunk, bytesize)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
48
68
|
# Call original method
|
|
49
69
|
response = original_chat(parameters: parameters)
|
|
50
70
|
|
|
71
|
+
# Use accumulated response for streaming, otherwise use returned response
|
|
72
|
+
final_response = streaming ? accumulated_response : response
|
|
73
|
+
|
|
51
74
|
# Submit analytics
|
|
52
75
|
payloop_submit_analytics(
|
|
53
76
|
method: :chat,
|
|
54
77
|
args: [],
|
|
55
78
|
kwargs: parameters,
|
|
56
|
-
response:
|
|
79
|
+
response: final_response,
|
|
57
80
|
start_time: start_time,
|
|
58
81
|
end_time: Time.now,
|
|
59
82
|
title: OPENAI_CLIENT_TITLE
|
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
|
+
version: 0.0.4
|
|
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-
|
|
11
|
+
date: 2025-10-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|