coolhand 0.1.5 → 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.5
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-09 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,119 +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 @patched
9
-
10
- @patched = true
11
- Coolhand.log "📡 Monitoring outbound requests ..."
12
-
13
- # Use prepend instead of alias_method to avoid conflicts with other gems
14
- Faraday::Connection.prepend(Module.new do
15
- def initialize(url = nil, options = nil, &block)
16
- super
17
-
18
- # Only add interceptor if it's not already present
19
- use Coolhand::Interceptor unless @builder.handlers.any? { |h| h.klass == Coolhand::Interceptor }
20
- end
21
- end)
22
-
23
- Coolhand.log "🔧 Setting up monitoring for Faraday ..."
24
- end
25
-
26
- def self.unpatch!
27
- # NOTE: With prepend, there's no clean way to unpatch
28
- # We'll mark it as unpatched so it can be re-patched
29
- @patched = false
30
- Coolhand.log "🔌 Faraday monitoring disabled ..."
31
- end
32
-
33
- def self.patched?
34
- @patched
35
- end
36
-
37
- def call(env)
38
- return super unless llm_api_request?(env)
39
-
40
- Coolhand.log "🎯 INTERCEPTING OpenAI call #{env.url}"
41
-
42
- call_data = build_call_data(env)
43
- buffer = override_on_data(env)
44
-
45
- process_complete_callback(env, buffer, call_data)
46
- end
47
-
48
- private
49
-
50
- def llm_api_request?(env)
51
- Coolhand.configuration.intercept_addresses.any? do |address|
52
- env.url.to_s.include?(address)
53
- end
54
- end
55
-
56
- def build_call_data(env)
57
- {
58
- id: SecureRandom.uuid,
59
- timestamp: DateTime.now,
60
- method: env.method,
61
- url: env.url.to_s,
62
- headers: sanitize_headers(env.request_headers),
63
- request_body: parse_json(env.request_body),
64
- response_body: nil,
65
- response_headers: nil,
66
- status_code: nil
67
- }
68
- end
69
-
70
- def override_on_data(env)
71
- buffer = +""
72
- original_on_data = env.request.on_data
73
- env.request.on_data = proc do |chunk, overall_received_bytes|
74
- buffer << chunk
75
-
76
- original_on_data&.call(chunk, overall_received_bytes)
77
- end
78
-
79
- buffer
80
- end
81
-
82
- def process_complete_callback(env, buffer, call_data)
83
- @app.call(env).on_complete do |response_env|
84
- if buffer.empty?
85
- body = response_env.body
86
- else
87
- body = buffer
88
- response_env.body = body
89
- end
90
-
91
- call_data[:response_body] = parse_json(body)
92
- call_data[:response_headers] = sanitize_headers(response_env.request_headers)
93
- call_data[:status_code] = response_env.status
94
-
95
- Thread.new { Coolhand.logger_service.log_to_api(call_data) }
96
- end
97
- end
98
-
99
- def parse_json(string)
100
- JSON.parse(string)
101
- rescue JSON::ParserError, TypeError
102
- string
103
- end
104
-
105
- def sanitize_headers(headers)
106
- sanitized = headers.transform_keys(&:to_s).dup
107
-
108
- if sanitized["Authorization"]
109
- sanitized["Authorization"] = sanitized["Authorization"].gsub(/Bearer .+/, "Bearer [REDACTED]")
110
- end
111
-
112
- %w[openai-api-key api-key].each do |key|
113
- sanitized[key] = "[REDACTED]" if sanitized[key]
114
- end
115
-
116
- sanitized
117
- end
118
- end
119
- end