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.
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.1.4
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-06 00:00:00.000000000 Z
12
+ date: 2025-12-17 00:00:00.000000000 Z
13
13
  dependencies: []
14
- description: A Ruby gem to automatically monitor and log external LLM requests. It
15
- patches Net::HTTP to capture request and response data.
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
- - coolhand-ruby.gemspec
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: Intercepts and logs OpenAI API calls from a Ruby application.
73
+ summary: Monitor and log LLM API calls from OpenAI, Anthropic, and other providers
74
+ to Coolhand analytics.
70
75
  test_files: []
@@ -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