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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4dd3be8a2eac5f3cda3b04fbc7d73d2fa6c9b4f35d640840a90f6a872e90a58
4
- data.tar.gz: e348e027345bc09b1233e976c38871747410d3caccac959fc119c49143f14df4
3
+ metadata.gz: 0d134effe2b909cc928b4dfc80149d3c9dcde238be7630bb37dba541fa33be0a
4
+ data.tar.gz: ddf58b4672381d25ab90b349c6bf29391e4fe099f9e304ba20f5169d3ee6bf1c
5
5
  SHA512:
6
- metadata.gz: 6d030e46f95833e825651eb0c11e23116eff4f542b10784e96aff2031892c408294115bfae3c5544f37a25b32d4435092d5b17940373f4b0968a2684657611c7
7
- data.tar.gz: dc86258b883762641d561b39261bbfc0b66b494f15f5d891446e00b4d786434f5684986d01d7bfb69c989a7db54991e290656fc04b59429c1ff8dad6c034c28d
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
@@ -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,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Payloop
4
- VERSION = "0.0.2"
4
+ VERSION = "0.0.4"
5
5
  end
@@ -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
- if response.respond_to?(:to_h)
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
- { raw: response.to_s }
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: 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.2
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-25 00:00:00.000000000 Z
11
+ date: 2025-10-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby