coolhand 0.1.4 → 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/.rubocop.yml +2 -1
- data/CHANGELOG.md +53 -0
- data/README.md +32 -33
- data/docs/anthropic.md +518 -0
- data/lib/coolhand/ruby/anthropic_interceptor.rb +300 -0
- data/lib/coolhand/ruby/api_service.rb +18 -3
- data/lib/coolhand/ruby/base_interceptor.rb +148 -0
- data/lib/coolhand/ruby/faraday_interceptor.rb +129 -0
- data/lib/coolhand/ruby/version.rb +1 -1
- data/lib/coolhand/ruby.rb +38 -7
- metadata +12 -7
- data/coolhand-ruby.gemspec +0 -42
- data/lib/coolhand/ruby/interceptor.rb +0 -118
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: coolhand
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Carroll
|
|
@@ -9,10 +9,12 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2025-12-
|
|
12
|
+
date: 2025-12-17 00:00:00.000000000 Z
|
|
13
13
|
dependencies: []
|
|
14
|
-
description:
|
|
15
|
-
|
|
14
|
+
description: Automatically intercept and log LLM requests from Ruby applications.
|
|
15
|
+
Supports OpenAI, official Anthropic gem, ruby-anthropic gem, and other Faraday-based
|
|
16
|
+
libraries. Features dual interceptor architecture, streaming support, thread-safe
|
|
17
|
+
operation, and automatic duplicate request prevention.
|
|
16
18
|
email:
|
|
17
19
|
- mc@coolhandlabs.com
|
|
18
20
|
executables: []
|
|
@@ -27,15 +29,17 @@ files:
|
|
|
27
29
|
- LICENSE
|
|
28
30
|
- README.md
|
|
29
31
|
- Rakefile
|
|
30
|
-
-
|
|
32
|
+
- docs/anthropic.md
|
|
31
33
|
- docs/elevenlabs.md
|
|
32
34
|
- lib/coolhand.rb
|
|
33
35
|
- lib/coolhand/ruby.rb
|
|
36
|
+
- lib/coolhand/ruby/anthropic_interceptor.rb
|
|
34
37
|
- lib/coolhand/ruby/api_service.rb
|
|
38
|
+
- lib/coolhand/ruby/base_interceptor.rb
|
|
35
39
|
- lib/coolhand/ruby/collector.rb
|
|
36
40
|
- lib/coolhand/ruby/configuration.rb
|
|
41
|
+
- lib/coolhand/ruby/faraday_interceptor.rb
|
|
37
42
|
- lib/coolhand/ruby/feedback_service.rb
|
|
38
|
-
- lib/coolhand/ruby/interceptor.rb
|
|
39
43
|
- lib/coolhand/ruby/logger_service.rb
|
|
40
44
|
- lib/coolhand/ruby/version.rb
|
|
41
45
|
- sig/coolhand/ruby.rbs
|
|
@@ -66,5 +70,6 @@ requirements: []
|
|
|
66
70
|
rubygems_version: 3.3.26
|
|
67
71
|
signing_key:
|
|
68
72
|
specification_version: 4
|
|
69
|
-
summary:
|
|
73
|
+
summary: Monitor and log LLM API calls from OpenAI, Anthropic, and other providers
|
|
74
|
+
to Coolhand analytics.
|
|
70
75
|
test_files: []
|
data/coolhand-ruby.gemspec
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "lib/coolhand/ruby/version"
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = "coolhand"
|
|
7
|
-
spec.version = Coolhand::Ruby::VERSION
|
|
8
|
-
spec.authors = ["Michael Carroll", "Yaroslav Malyk"]
|
|
9
|
-
spec.email = ["mc@coolhandlabs.com"]
|
|
10
|
-
|
|
11
|
-
spec.summary = "Intercepts and logs OpenAI API calls from a Ruby application."
|
|
12
|
-
spec.description = "A Ruby gem to automatically monitor and log external LLM requests. It patches Net::HTTP " \
|
|
13
|
-
"to capture request and response data."
|
|
14
|
-
spec.homepage = "https://coolhandlabs.com/"
|
|
15
|
-
spec.license = "Apache-2.0"
|
|
16
|
-
spec.required_ruby_version = ">= 3.0.0"
|
|
17
|
-
|
|
18
|
-
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
19
|
-
|
|
20
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
|
21
|
-
spec.metadata["source_code_uri"] = "https://github.com/Coolhand-Labs/coolhand-ruby"
|
|
22
|
-
spec.metadata["changelog_uri"] = "https://github.com/Coolhand-Labs/coolhand-ruby"
|
|
23
|
-
|
|
24
|
-
# Specify which files should be added to the gem when it is released.
|
|
25
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
26
|
-
spec.files = Dir.chdir(__dir__) do
|
|
27
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
|
28
|
-
(File.expand_path(f) == __FILE__) ||
|
|
29
|
-
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
spec.bindir = "exe"
|
|
33
|
-
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
34
|
-
spec.require_paths = ["lib"]
|
|
35
|
-
|
|
36
|
-
# Uncomment to register a new dependency of your gem
|
|
37
|
-
# spec.add_dependency "example-gem", "~> 1.0"
|
|
38
|
-
|
|
39
|
-
# For more information and examples about making a new gem, check out our
|
|
40
|
-
# guide at: https://bundler.io/guides/creating_gem.html
|
|
41
|
-
spec.metadata["rubygems_mfa_required"] = "true"
|
|
42
|
-
end
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Coolhand
|
|
4
|
-
class Interceptor < Faraday::Middleware
|
|
5
|
-
ORIGINAL_METHOD_ALIAS = :coolhand_original_initialize
|
|
6
|
-
|
|
7
|
-
def self.patch!
|
|
8
|
-
return if Faraday::Connection.private_method_defined?(ORIGINAL_METHOD_ALIAS)
|
|
9
|
-
|
|
10
|
-
Coolhand.log "📡 Monitoring outbound requests ..."
|
|
11
|
-
|
|
12
|
-
Faraday::Connection.class_eval do
|
|
13
|
-
alias_method ORIGINAL_METHOD_ALIAS, :initialize
|
|
14
|
-
|
|
15
|
-
def initialize(url = nil, options = nil, &block)
|
|
16
|
-
send(ORIGINAL_METHOD_ALIAS, url, options, &block)
|
|
17
|
-
|
|
18
|
-
use Interceptor
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
Coolhand.log "🔧 Setting up monitoring for Faraday ..."
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.unpatch!
|
|
26
|
-
return unless Faraday::Connection.private_method_defined?(ORIGINAL_METHOD_ALIAS)
|
|
27
|
-
|
|
28
|
-
Faraday::Connection.class_eval do
|
|
29
|
-
alias_method :initialize, ORIGINAL_METHOD_ALIAS
|
|
30
|
-
remove_method ORIGINAL_METHOD_ALIAS
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
Coolhand.log "🔌 Faraday unpatched ..."
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def call(env)
|
|
37
|
-
return super unless llm_api_request?(env)
|
|
38
|
-
|
|
39
|
-
Coolhand.log "🎯 INTERCEPTING OpenAI call #{env.url}"
|
|
40
|
-
|
|
41
|
-
call_data = build_call_data(env)
|
|
42
|
-
buffer = override_on_data(env)
|
|
43
|
-
|
|
44
|
-
process_complete_callback(env, buffer, call_data)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def llm_api_request?(env)
|
|
50
|
-
Coolhand.configuration.intercept_addresses.any? do |address|
|
|
51
|
-
env.url.to_s.include?(address)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def build_call_data(env)
|
|
56
|
-
{
|
|
57
|
-
id: SecureRandom.uuid,
|
|
58
|
-
timestamp: DateTime.now,
|
|
59
|
-
method: env.method,
|
|
60
|
-
url: env.url.to_s,
|
|
61
|
-
headers: sanitize_headers(env.request_headers),
|
|
62
|
-
request_body: parse_json(env.request_body),
|
|
63
|
-
response_body: nil,
|
|
64
|
-
response_headers: nil,
|
|
65
|
-
status_code: nil
|
|
66
|
-
}
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def override_on_data(env)
|
|
70
|
-
buffer = +""
|
|
71
|
-
original_on_data = env.request.on_data
|
|
72
|
-
env.request.on_data = proc do |chunk, overall_received_bytes|
|
|
73
|
-
buffer << chunk
|
|
74
|
-
|
|
75
|
-
original_on_data&.call(chunk, overall_received_bytes)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
buffer
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def process_complete_callback(env, buffer, call_data)
|
|
82
|
-
@app.call(env).on_complete do |response_env|
|
|
83
|
-
if buffer.empty?
|
|
84
|
-
body = response_env.body
|
|
85
|
-
else
|
|
86
|
-
body = buffer
|
|
87
|
-
response_env.body = body
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
call_data[:response_body] = parse_json(body)
|
|
91
|
-
call_data[:response_headers] = sanitize_headers(response_env.request_headers)
|
|
92
|
-
call_data[:status_code] = response_env.status
|
|
93
|
-
|
|
94
|
-
Thread.new { Coolhand.logger_service.log_to_api(call_data) }
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def parse_json(string)
|
|
99
|
-
JSON.parse(string)
|
|
100
|
-
rescue JSON::ParserError, TypeError
|
|
101
|
-
string
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def sanitize_headers(headers)
|
|
105
|
-
sanitized = headers.transform_keys(&:to_s).dup
|
|
106
|
-
|
|
107
|
-
if sanitized["Authorization"]
|
|
108
|
-
sanitized["Authorization"] = sanitized["Authorization"].gsub(/Bearer .+/, "Bearer [REDACTED]")
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
%w[openai-api-key api-key].each do |key|
|
|
112
|
-
sanitized[key] = "[REDACTED]" if sanitized[key]
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
sanitized
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|