coolhand 0.2.0 → 0.3.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/.rubocop.yml +12 -0
- data/CHANGELOG.md +53 -1
- data/CLAUDE.md +34 -0
- data/README.md +147 -27
- data/coolhand-ruby.gemspec +46 -0
- data/docs/anthropic.md +11 -11
- data/docs/elevenlabs.md +6 -4
- data/lib/coolhand/api_service.rb +264 -0
- data/lib/coolhand/base_interceptor.rb +213 -0
- data/lib/coolhand/collector.rb +19 -0
- data/lib/coolhand/configuration.rb +84 -0
- data/lib/coolhand/default_exclude_api_patterns.yml +9 -0
- data/lib/coolhand/default_intercept_addresses.yml +15 -0
- data/lib/coolhand/feedback_service.rb +15 -0
- data/lib/coolhand/logger_service.rb +112 -0
- data/lib/coolhand/net_http_interceptor.rb +163 -0
- data/lib/coolhand/open_ai/batch_result_processor.rb +139 -0
- data/lib/coolhand/open_ai/webhook_validator.rb +127 -0
- data/lib/coolhand/{ruby/version.rb → version.rb} +1 -3
- data/lib/coolhand/vertex/batch_result_processor.rb +84 -0
- data/lib/coolhand/webhook_interceptor.rb +39 -0
- data/lib/coolhand.rb +109 -2
- metadata +35 -18
- data/lib/coolhand/ruby/anthropic_interceptor.rb +0 -300
- data/lib/coolhand/ruby/api_service.rb +0 -226
- data/lib/coolhand/ruby/base_interceptor.rb +0 -148
- data/lib/coolhand/ruby/collector.rb +0 -21
- data/lib/coolhand/ruby/configuration.rb +0 -38
- data/lib/coolhand/ruby/faraday_interceptor.rb +0 -129
- data/lib/coolhand/ruby/feedback_service.rb +0 -17
- data/lib/coolhand/ruby/logger_service.rb +0 -114
- data/lib/coolhand/ruby.rb +0 -121
data/lib/coolhand.rb
CHANGED
|
@@ -1,4 +1,111 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
require "uri"
|
|
4
|
+
require "faraday"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "json"
|
|
7
|
+
require "base64"
|
|
8
|
+
|
|
9
|
+
require_relative "coolhand/version"
|
|
10
|
+
require_relative "coolhand/configuration"
|
|
11
|
+
require_relative "coolhand/collector"
|
|
12
|
+
require_relative "coolhand/base_interceptor"
|
|
13
|
+
require_relative "coolhand/net_http_interceptor"
|
|
14
|
+
require_relative "coolhand/api_service"
|
|
15
|
+
require_relative "coolhand/logger_service"
|
|
16
|
+
require_relative "coolhand/feedback_service"
|
|
17
|
+
require_relative "coolhand/webhook_interceptor"
|
|
18
|
+
|
|
19
|
+
# The main module for the Coolhand gem.
|
|
20
|
+
# It provides the configuration interface and initializes the patching.
|
|
21
|
+
module Coolhand
|
|
22
|
+
class Error < StandardError; end
|
|
23
|
+
|
|
24
|
+
# Class-level instance variables to hold the configuration
|
|
25
|
+
@configuration = Configuration.new
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
attr_reader :configuration
|
|
29
|
+
|
|
30
|
+
# Reset configuration to defaults (mainly for testing)
|
|
31
|
+
def reset_configuration!
|
|
32
|
+
@configuration = Configuration.new
|
|
33
|
+
Thread.current[:coolhand_capture_override] = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Provides a block to configure the gem.
|
|
37
|
+
#
|
|
38
|
+
# Example:
|
|
39
|
+
# Coolhand.configure do |config|
|
|
40
|
+
# config.environment = 'development'
|
|
41
|
+
# config.silent = false
|
|
42
|
+
# config.api_key = "xxx-yyy-zzz"
|
|
43
|
+
# config.intercept_addresses = ["openai.com", "api.anthropic.com"]
|
|
44
|
+
# end
|
|
45
|
+
def configure
|
|
46
|
+
yield(configuration)
|
|
47
|
+
|
|
48
|
+
configuration.validate!
|
|
49
|
+
|
|
50
|
+
NetHttpInterceptor.patch!
|
|
51
|
+
|
|
52
|
+
log "✅ Coolhand ready - will log inference calls on monitored URIs"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def capture
|
|
56
|
+
unless block_given?
|
|
57
|
+
log "❌ Coolhand Error: Method .capture requires block."
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
patched = NetHttpInterceptor.patched?
|
|
62
|
+
|
|
63
|
+
NetHttpInterceptor.patch!
|
|
64
|
+
|
|
65
|
+
yield
|
|
66
|
+
ensure
|
|
67
|
+
NetHttpInterceptor.unpatch! unless patched
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def without_capture
|
|
71
|
+
previous = Thread.current[:coolhand_capture_override]
|
|
72
|
+
Thread.current[:coolhand_capture_override] = false
|
|
73
|
+
yield
|
|
74
|
+
ensure
|
|
75
|
+
Thread.current[:coolhand_capture_override] = previous
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def with_capture
|
|
79
|
+
previous = Thread.current[:coolhand_capture_override]
|
|
80
|
+
Thread.current[:coolhand_capture_override] = true
|
|
81
|
+
yield
|
|
82
|
+
ensure
|
|
83
|
+
Thread.current[:coolhand_capture_override] = previous
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A simple logger that respects the 'silent' configuration option.
|
|
87
|
+
def log(message)
|
|
88
|
+
return if configuration.silent
|
|
89
|
+
|
|
90
|
+
puts "COOLHAND: #{message}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Creates a new FeedbackService instance
|
|
94
|
+
def feedback_service
|
|
95
|
+
FeedbackService.new
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Creates a new LoggerService instance
|
|
99
|
+
def logger_service
|
|
100
|
+
LoggerService.new
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def required_field?(value)
|
|
104
|
+
return false if value.nil?
|
|
105
|
+
return false if value.respond_to?(:empty?) && value.empty?
|
|
106
|
+
return false if value.to_s.strip.empty?
|
|
107
|
+
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
metadata
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: coolhand
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Carroll
|
|
8
8
|
- Yaroslav Malyk
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: exe
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
13
|
-
dependencies:
|
|
11
|
+
date: 2026-05-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.2'
|
|
14
27
|
description: Automatically intercept and log LLM requests from Ruby applications.
|
|
15
28
|
Supports OpenAI, official Anthropic gem, ruby-anthropic gem, and other Faraday-based
|
|
16
29
|
libraries. Features dual interceptor architecture, streaming support, thread-safe
|
|
@@ -26,22 +39,28 @@ files:
|
|
|
26
39
|
- ".rubocop.yml"
|
|
27
40
|
- ".simplecov"
|
|
28
41
|
- CHANGELOG.md
|
|
42
|
+
- CLAUDE.md
|
|
29
43
|
- LICENSE
|
|
30
44
|
- README.md
|
|
31
45
|
- Rakefile
|
|
46
|
+
- coolhand-ruby.gemspec
|
|
32
47
|
- docs/anthropic.md
|
|
33
48
|
- docs/elevenlabs.md
|
|
34
49
|
- lib/coolhand.rb
|
|
35
|
-
- lib/coolhand/
|
|
36
|
-
- lib/coolhand/
|
|
37
|
-
- lib/coolhand/
|
|
38
|
-
- lib/coolhand/
|
|
39
|
-
- lib/coolhand/
|
|
40
|
-
- lib/coolhand/
|
|
41
|
-
- lib/coolhand/
|
|
42
|
-
- lib/coolhand/
|
|
43
|
-
- lib/coolhand/
|
|
44
|
-
- lib/coolhand/
|
|
50
|
+
- lib/coolhand/api_service.rb
|
|
51
|
+
- lib/coolhand/base_interceptor.rb
|
|
52
|
+
- lib/coolhand/collector.rb
|
|
53
|
+
- lib/coolhand/configuration.rb
|
|
54
|
+
- lib/coolhand/default_exclude_api_patterns.yml
|
|
55
|
+
- lib/coolhand/default_intercept_addresses.yml
|
|
56
|
+
- lib/coolhand/feedback_service.rb
|
|
57
|
+
- lib/coolhand/logger_service.rb
|
|
58
|
+
- lib/coolhand/net_http_interceptor.rb
|
|
59
|
+
- lib/coolhand/open_ai/batch_result_processor.rb
|
|
60
|
+
- lib/coolhand/open_ai/webhook_validator.rb
|
|
61
|
+
- lib/coolhand/version.rb
|
|
62
|
+
- lib/coolhand/vertex/batch_result_processor.rb
|
|
63
|
+
- lib/coolhand/webhook_interceptor.rb
|
|
45
64
|
- sig/coolhand/ruby.rbs
|
|
46
65
|
homepage: https://coolhandlabs.com/
|
|
47
66
|
licenses:
|
|
@@ -50,9 +69,8 @@ metadata:
|
|
|
50
69
|
allowed_push_host: https://rubygems.org
|
|
51
70
|
homepage_uri: https://coolhandlabs.com/
|
|
52
71
|
source_code_uri: https://github.com/Coolhand-Labs/coolhand-ruby
|
|
53
|
-
changelog_uri: https://github.com/Coolhand-Labs/coolhand-ruby
|
|
72
|
+
changelog_uri: https://github.com/Coolhand-Labs/coolhand-ruby/blob/main/CHANGELOG.md
|
|
54
73
|
rubygems_mfa_required: 'true'
|
|
55
|
-
post_install_message:
|
|
56
74
|
rdoc_options: []
|
|
57
75
|
require_paths:
|
|
58
76
|
- lib
|
|
@@ -67,8 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
67
85
|
- !ruby/object:Gem::Version
|
|
68
86
|
version: '0'
|
|
69
87
|
requirements: []
|
|
70
|
-
rubygems_version: 3.
|
|
71
|
-
signing_key:
|
|
88
|
+
rubygems_version: 3.6.2
|
|
72
89
|
specification_version: 4
|
|
73
90
|
summary: Monitor and log LLM API calls from OpenAI, Anthropic, and other providers
|
|
74
91
|
to Coolhand analytics.
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
begin
|
|
4
|
-
require "anthropic"
|
|
5
|
-
rescue LoadError
|
|
6
|
-
# Anthropic gem not available - interceptor will be a no-op
|
|
7
|
-
end
|
|
8
|
-
require "securerandom"
|
|
9
|
-
require "json"
|
|
10
|
-
require "ostruct"
|
|
11
|
-
require_relative "base_interceptor"
|
|
12
|
-
|
|
13
|
-
module Coolhand
|
|
14
|
-
module Ruby
|
|
15
|
-
module AnthropicInterceptor
|
|
16
|
-
module_function
|
|
17
|
-
|
|
18
|
-
def patch!
|
|
19
|
-
return if @patched
|
|
20
|
-
return unless defined?(Anthropic)
|
|
21
|
-
|
|
22
|
-
# Check if both anthropic gems are installed
|
|
23
|
-
if both_gems_installed?
|
|
24
|
-
# Always show this warning, regardless of silent mode
|
|
25
|
-
warn_message = "⚠️ Warning: Both 'anthropic' and 'ruby-anthropic' gems are installed. " \
|
|
26
|
-
"Coolhand will only monitor ruby-anthropic (Faraday-based) requests. " \
|
|
27
|
-
"Official anthropic gem monitoring has been disabled."
|
|
28
|
-
puts "COOLHAND: #{warn_message}"
|
|
29
|
-
|
|
30
|
-
# Mark as patched since ruby-anthropic will be handled by FaradayInterceptor
|
|
31
|
-
@patched = true
|
|
32
|
-
return
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Check if we're using the official anthropic gem
|
|
36
|
-
# The official gem has Anthropic::Internal, ruby-anthropic doesn't
|
|
37
|
-
if defined?(Anthropic::Internal)
|
|
38
|
-
# Patch the BaseClient request method for official anthropic gem
|
|
39
|
-
require "anthropic/internal/transport/base_client"
|
|
40
|
-
::Anthropic::Internal::Transport::BaseClient.prepend(RequestInterceptor)
|
|
41
|
-
else
|
|
42
|
-
# ruby-anthropic uses Faraday, so the FaradayInterceptor already handles it
|
|
43
|
-
Coolhand.log "✅ ruby-anthropic detected, using Faraday interceptor"
|
|
44
|
-
@patched = true
|
|
45
|
-
return
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Patch MessageStream to capture completion data
|
|
49
|
-
patch_message_stream!
|
|
50
|
-
|
|
51
|
-
@patched = true
|
|
52
|
-
Coolhand.log "✅ Anthropic interceptor patched"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def unpatch!
|
|
56
|
-
# NOTE: Ruby doesn't have a clean way to unpatch prepended modules
|
|
57
|
-
# This is mainly for testing - in production, patching is permanent
|
|
58
|
-
@patched = false
|
|
59
|
-
Coolhand.log "⚠️ Anthropic interceptor unpatch requested (not fully implemented)"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def patched?
|
|
63
|
-
@patched ||= false
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def both_gems_installed?
|
|
67
|
-
# Check if both gems are installed by looking at loaded specs
|
|
68
|
-
anthropic_gem = Gem.loaded_specs["anthropic"]
|
|
69
|
-
ruby_anthropic_gem = Gem.loaded_specs["ruby-anthropic"]
|
|
70
|
-
|
|
71
|
-
anthropic_gem && ruby_anthropic_gem
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def patch_message_stream!
|
|
75
|
-
# Try to load MessageStream class if available
|
|
76
|
-
begin
|
|
77
|
-
require "anthropic/helpers/streaming/message_stream"
|
|
78
|
-
rescue LoadError
|
|
79
|
-
# MessageStream not available in this version of anthropic gem
|
|
80
|
-
return
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Only proceed if the constant is now defined
|
|
84
|
-
return unless defined?(Anthropic::Streaming::MessageStream)
|
|
85
|
-
|
|
86
|
-
# Prepend our patch module
|
|
87
|
-
::Anthropic::Streaming::MessageStream.prepend(MessageStreamInterceptor)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
module RequestInterceptor
|
|
91
|
-
def request(method:, path:, body: nil, headers: {}, **options)
|
|
92
|
-
# Generate request ID for correlation
|
|
93
|
-
request_id = SecureRandom.hex(16)
|
|
94
|
-
start_time = Time.now
|
|
95
|
-
|
|
96
|
-
# Store request ID in thread-local storage for application access
|
|
97
|
-
Thread.current[:coolhand_current_request_id] = request_id
|
|
98
|
-
|
|
99
|
-
# Temporarily disable Faraday interception for this thread to prevent double logging
|
|
100
|
-
Thread.current[:coolhand_disable_faraday] = true
|
|
101
|
-
|
|
102
|
-
# Extract request metadata
|
|
103
|
-
full_url = "#{@base_url}#{path}"
|
|
104
|
-
|
|
105
|
-
# Capture all request headers including those added by the client
|
|
106
|
-
request_headers = BaseInterceptor.clean_request_headers(headers.dup)
|
|
107
|
-
request_body = body
|
|
108
|
-
|
|
109
|
-
# Detect if this is a streaming request
|
|
110
|
-
is_streaming = streaming_request?(body, headers)
|
|
111
|
-
|
|
112
|
-
# Call the original request method
|
|
113
|
-
begin
|
|
114
|
-
response = super
|
|
115
|
-
end_time = Time.now
|
|
116
|
-
duration_ms = ((end_time - start_time) * 1000).round(2)
|
|
117
|
-
|
|
118
|
-
# For streaming responses, store request metadata for later logging
|
|
119
|
-
if is_streaming
|
|
120
|
-
Thread.current[:coolhand_streaming_request] = {
|
|
121
|
-
request_id: request_id,
|
|
122
|
-
method: method,
|
|
123
|
-
url: full_url,
|
|
124
|
-
request_headers: request_headers,
|
|
125
|
-
request_body: request_body,
|
|
126
|
-
start_time: start_time,
|
|
127
|
-
end_time: end_time,
|
|
128
|
-
duration_ms: duration_ms,
|
|
129
|
-
is_streaming: is_streaming
|
|
130
|
-
}
|
|
131
|
-
else
|
|
132
|
-
# Extract response data
|
|
133
|
-
response_data = BaseInterceptor.extract_response_data(response)
|
|
134
|
-
|
|
135
|
-
# Send complete request/response data in single API call
|
|
136
|
-
BaseInterceptor.send_complete_request_log(
|
|
137
|
-
request_id: request_id,
|
|
138
|
-
method: method,
|
|
139
|
-
url: full_url,
|
|
140
|
-
request_headers: request_headers,
|
|
141
|
-
request_body: request_body,
|
|
142
|
-
response_headers: extract_response_headers(response),
|
|
143
|
-
response_body: response_data,
|
|
144
|
-
status_code: nil,
|
|
145
|
-
start_time: start_time,
|
|
146
|
-
end_time: end_time,
|
|
147
|
-
duration_ms: duration_ms,
|
|
148
|
-
is_streaming: is_streaming
|
|
149
|
-
)
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
response
|
|
153
|
-
rescue StandardError => e
|
|
154
|
-
end_time = Time.now
|
|
155
|
-
duration_ms = ((end_time - start_time) * 1000).round(2)
|
|
156
|
-
|
|
157
|
-
# Send error response in single API call
|
|
158
|
-
BaseInterceptor.send_complete_request_log(
|
|
159
|
-
request_id: request_id,
|
|
160
|
-
method: method,
|
|
161
|
-
url: full_url,
|
|
162
|
-
request_headers: request_headers,
|
|
163
|
-
request_body: request_body,
|
|
164
|
-
response_headers: {},
|
|
165
|
-
response_body: {
|
|
166
|
-
error: {
|
|
167
|
-
message: e.message,
|
|
168
|
-
class: e.class.name
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
status_code: nil,
|
|
172
|
-
start_time: start_time,
|
|
173
|
-
end_time: end_time,
|
|
174
|
-
duration_ms: duration_ms,
|
|
175
|
-
is_streaming: is_streaming
|
|
176
|
-
)
|
|
177
|
-
raise
|
|
178
|
-
ensure
|
|
179
|
-
# Always re-enable Faraday interception for this thread
|
|
180
|
-
Thread.current[:coolhand_disable_faraday] = false
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Public method for applications to log final streaming response
|
|
185
|
-
def self.log_streaming_completion(final_response_body)
|
|
186
|
-
streaming_request = Thread.current[:coolhand_streaming_request]
|
|
187
|
-
return unless streaming_request
|
|
188
|
-
|
|
189
|
-
begin
|
|
190
|
-
send_complete_request_log(
|
|
191
|
-
request_id: streaming_request[:request_id],
|
|
192
|
-
method: streaming_request[:method],
|
|
193
|
-
url: streaming_request[:url],
|
|
194
|
-
request_headers: streaming_request[:request_headers],
|
|
195
|
-
request_body: streaming_request[:request_body],
|
|
196
|
-
response_headers: {},
|
|
197
|
-
response_body: final_response_body,
|
|
198
|
-
start_time: streaming_request[:start_time],
|
|
199
|
-
end_time: streaming_request[:end_time],
|
|
200
|
-
duration_ms: streaming_request[:duration_ms],
|
|
201
|
-
is_streaming: streaming_request[:is_streaming]
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
# Clear the thread-local data
|
|
205
|
-
Thread.current[:coolhand_streaming_request] = nil
|
|
206
|
-
rescue StandardError => e
|
|
207
|
-
Coolhand.log "❌ Error logging streaming completion: #{e.message}"
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def self.send_complete_request_log(request_id:, method:, url:, request_headers:, request_body:,
|
|
212
|
-
response_headers:, response_body:, start_time:, end_time:, duration_ms:, is_streaming:)
|
|
213
|
-
BaseInterceptor.send_complete_request_log(
|
|
214
|
-
request_id: request_id,
|
|
215
|
-
method: method,
|
|
216
|
-
url: url,
|
|
217
|
-
request_headers: request_headers,
|
|
218
|
-
request_body: request_body,
|
|
219
|
-
response_headers: response_headers,
|
|
220
|
-
response_body: response_body,
|
|
221
|
-
status_code: nil,
|
|
222
|
-
start_time: start_time,
|
|
223
|
-
end_time: end_time,
|
|
224
|
-
duration_ms: duration_ms,
|
|
225
|
-
is_streaming: is_streaming
|
|
226
|
-
)
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
private
|
|
230
|
-
|
|
231
|
-
def streaming_request?(body, headers)
|
|
232
|
-
# Check if stream parameter is set in request body
|
|
233
|
-
return true if body.is_a?(Hash) && body[:stream] == true
|
|
234
|
-
|
|
235
|
-
# Check Accept header for Server-Sent Events
|
|
236
|
-
accept_header = headers["Accept"] || headers["accept"]
|
|
237
|
-
return true if accept_header&.include?("text/event-stream")
|
|
238
|
-
|
|
239
|
-
false
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def extract_response_headers(response)
|
|
243
|
-
# Try to extract headers if the response object exposes them
|
|
244
|
-
if response.respond_to?(:headers)
|
|
245
|
-
BaseInterceptor.clean_response_headers(response.headers)
|
|
246
|
-
elsif response.respond_to?(:response_headers)
|
|
247
|
-
BaseInterceptor.clean_response_headers(response.response_headers)
|
|
248
|
-
else
|
|
249
|
-
# Anthropic gem doesn't expose response headers directly
|
|
250
|
-
# Return empty hash to indicate no headers are available
|
|
251
|
-
{}
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
module MessageStreamInterceptor
|
|
257
|
-
def accumulated_message
|
|
258
|
-
# Call the original method to get the accumulated message
|
|
259
|
-
message = super
|
|
260
|
-
|
|
261
|
-
# Log the completion data if we have streaming request metadata
|
|
262
|
-
streaming_request = Thread.current[:coolhand_streaming_request]
|
|
263
|
-
log_streaming_completion(message, streaming_request) if streaming_request
|
|
264
|
-
|
|
265
|
-
message
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
private
|
|
269
|
-
|
|
270
|
-
def log_streaming_completion(message, streaming_request)
|
|
271
|
-
# Convert message to hash for logging (preserving natural format)
|
|
272
|
-
response_body = extract_response_data(message)
|
|
273
|
-
|
|
274
|
-
# Send the completion log
|
|
275
|
-
BaseInterceptor.send_complete_request_log(
|
|
276
|
-
request_id: streaming_request[:request_id],
|
|
277
|
-
method: streaming_request[:method],
|
|
278
|
-
url: streaming_request[:url],
|
|
279
|
-
request_headers: streaming_request[:request_headers],
|
|
280
|
-
request_body: streaming_request[:request_body],
|
|
281
|
-
response_headers: {},
|
|
282
|
-
response_body: response_body,
|
|
283
|
-
status_code: nil,
|
|
284
|
-
start_time: streaming_request[:start_time],
|
|
285
|
-
end_time: streaming_request[:end_time],
|
|
286
|
-
duration_ms: streaming_request[:duration_ms],
|
|
287
|
-
is_streaming: streaming_request[:is_streaming]
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
# Clear the thread-local data
|
|
291
|
-
Thread.current[:coolhand_streaming_request] = nil
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def extract_response_data(message)
|
|
295
|
-
BaseInterceptor.extract_response_data(message)
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
end
|