neuron-explorer
Steering-first neuron explorer for Qwen3.5-4B: push every MLP neuron, keep every rollout, browse the results
Coolhand watches your production AI agents, proposes fixes as pull requests, and helps you collect the human feedback that makes them better — so your team builds what's next instead of babysitting wh
Developer toolsRuby · ShellGitHub @Coolhand-LabsOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Zero-config LLM cost & quality monitoring for Ruby apps - automatically log AI API calls and collect user feedback for analysis using Coolhand.
Taken from the repository README
Loading the full README.
Read the full README ↗Monitor and log LLM API calls — OpenAI, Anthropic, Google Gemini, Cohere, and any Faraday-based or Net::HTTP client — to the Coolhand analytics platform. Supports Ruby LLM monitoring, request logging, and feedback collection.
Scope: Coolhand intercepts outgoing HTTP requests to configured LLM API endpoints only. It does not read, scan, or transmit your source code files.
gem 'coolhand'
gem install coolhand🔥 Set it and forget it! Monitor ALL AI API calls across your entire application with minimal configuration.
# Add this configuration at the start of your application
require 'coolhand'
Coolhand.configure do |config|
config.api_key = 'your_api_key_here'
config.silent = true # Set to false for debug output
end
# That's it! ALL AI API calls are now automatically monitored:
# ✅ OpenAI SDK calls
# ✅ Anthropic API calls
# ✅ Direct HTTP requests to AI APIs
# ✅ Any library making AI API calls via Faraday's default adapter
# NO code changes needed in your existing services!
✨ Why Automatic Monitoring:
Point the SDK at your own Coolhand-compatible endpoint via config.base_url for compliance or data-residency requirements. See Self-Hosted Deployments →.
Collect feedback on LLM responses to improve model performance.
Frontend Feedback Widget: For browser-based feedback collection, see coolhand-js - an accessible, lightweight JavaScript widget that leverages best UX practices to capture actionable user feedback on any AI output.
require 'coolhand'
# Create feedback for an LLM response
feedback_service = Coolhand::FeedbackService.new
feedback = feedback_service.create_feedback(
llm_request_log_id: 'abc123def456', # hashid from a prior response; a raw integer FK also still works
llm_provider_unique_id: 'req_xxxxxxx',
client_unique_id: 'workorder-chat-456',
creator_unique_id: 'user-789',
original_output: 'Here is the original LLM response!',
revised_output: 'Here is the human edit of the original LLM response.',
explanation: 'Tone of the original response read like AI-generated open source README docs',
sentiment: 'dislike'
)
For a full field reference and matching strategy, see Feedback API →.
Read back the LLM request templates your logs are matched against. Unlike the logging and feedback methods, these require your private API key — the public key is write-only on this API and is rejected exactly like an invalid one.
require 'coolhand'
templates = Coolhand.template_service
result = templates.search_templates(search: 'summar', status: 'published')
result.templates.each { |template| puts "#{template[:name]} (#{template[:log_count]} logs)" }
result.pagination.total_count
# Prompt patterns come from a single-template fetch only
detail = templates.get_template(result.templates.first[:id])
detail[:user_prompt_pattern]
Search is a parameter on the list endpoint, not a route of its own, and the Unmatched / Ignored API Calls system buckets are hidden unless you pass include_system: true. These are read methods, so they raise Coolhand::HttpError where the write methods log and return nil.
For the full filter reference, pagination, and error handling, see Reading Templates →.
Pass include_metrics: true to search_templates (or search_workloads) for the dashboard's cost and performance numbers, over a rolling days_back window or an explicit since: / until: one. search_logs carries each log's cost and can sort and filter by it.
window = { since: Time.utc(2026, 9, 1), until: Time.utc(2026, 10, 1) }
Coolhand.workload_service.search_workloads(include_metrics: true, **window).workloads
.map { |w| [w[:name], w[:metrics][:total_cost]] }
Coolhand.log_service.search_logs(order: 'cost_desc', min_cost: 1.0, per: 10, **window).logs
See Reading Workloads → and Reading Logs →.
Coolhand.help_center_service reads the Coolhand help center (the same pages as coolhandlabs.com/help and /updates) with no API key required:
require 'coolhand'
help_center = Coolhand.help_center_service
result = help_center.search_help_articles(query: 'match rate', per_page: 5)
article = help_center.get_help_article(result.articles.first[:slug]) # full markdown :body
categories = help_center.list_help_categories
Anonymous callers are limited to 30 requests per minute per IP. If config.api_key is set it is sent and lifts the limit; a key the server rejects raises a 401 Coolhand::HttpError. These are read methods, so they raise where the write methods log and return nil.
See Reading the Help Center → for parameters, the category filter and errors.
Attach feedback to an optimization as evidence, one at a time or in bulk. These require your private API key and raise Coolhand::Error on failure (Coolhand::HttpError for non-2xx responses).
links = Coolhand.optimization_feedback_link_service
link = links.link_feedback('optimizationHashid', 'feedbackHashid', note: 'why')
links.bulk_link_feedback('optimizationHashid', %w[fb1 fb2 fb3]) # => { linked:, already_linked:, errored:, not_found: }
links.unlink_feedback('optimizationHashid', link[:id])
For batching, error semantics, and details, see Linking Feedback →.
Tell Coolhand which workload and template an LLM call belongs to, instead of waiting for it to be matched. Calls captured inside the block carry the declared identity:
Coolhand.with_context(reporter: 'my-app', workload: { key: 'Parser#call' }, template: { key: 'Parser#call/summarize' }) do
client.chat(...) # captured as usual
end
Coolhand.workload_service.upsert(key: 'Parser#call', name: 'Parser') # private key; create or rename
Or declare it once on the class and let every capture inside it pick it up, subclasses included:
class ApplicationService
include Coolhand::Declarable
coolhand_declare_all # each public method declares "ClassName#method"
end
For nesting rules, class-level declarations, Rails jobs and controllers, upserts, and public-key limits, see Declared Workloads →.
Create an initializer file at config/initializers/coolhand.rb:
# config/initializers/coolhand.rb
Coolhand.configure do |config|
# Your Coolhand API Key (Required)
# Best practice: Use Rails credentials or environment-specific configuration
config.api_key = Rails.application.credentials.coolhand_api_key
# Set to true to suppress console output
config.silent = Rails.env.production?
# Specify which LLM endpoints to intercept (array of strings)
# Optional - defaults to every provider listed under "Supported Libraries" below
# config.intercept_addresses = ["api.openai.com", "api.anthropic.com", "api.cohere.com/v2/chat"]
end
class ChatController < ApplicationController
def create_feedback
feedback_service = Coolhand::FeedbackService.new
feedback = feedback_service.create_feedback(
llm_request_log_id: params[:log_id],
creator_unique_id: current_user.id,
original_output: params[:original_response],
revised_output: params[:edited_response],
explanation: params[:feedback_text],
sentiment: params[:sentiment]
)
if feedback
render json: { success: true, message: 'Feedback recorded' }
else
render json: { success: false, message: 'Failed to record feedback' }, status: 422
end
end
end
class FeedbackCollectionJob < ApplicationJob
def perform(feedback_data)
feedback_service = Coolhand::FeedbackService.new
feedback_service.create_feedback(
llm_provider_unique_id: feedback_data[:request_id],
creator_unique_id: feedback_data[:user_id],
original_output: feedback_data[:original],
explanation: feedback_data[:reason],
sentiment: feedback_data[:sentiment]
)
end
end
| Option | Type | Default | Description |
|---|---|---|---|
api_key | String | nil | Your Coolhand API key. If absent, intercepted requests are skipped with a warning log rather than raising at boot time |
enabled | Boolean | true | Set to false to disable all patching and validation (e.g. Rails.env.production?) |
capture | Boolean | true | Whether to capture and forward intercepted requests. Set to false to monitor without forwarding, then use Coolhand.with_capture to re-enable selectively |
silent | Boolean | false | Whether to suppress console output |
intercept_addresses | Array | All supported providers (DEFAULT_INTERCEPT_ADDRESSES) | Array of API hosts to monitor (matched by host, optionally pinned to a port and/or path prefix). This is a required allow-list — [] is ignored (a warning is logged) rather than disabling capture; use enabled or capture for that. See Configuration |
intercept_path_patterns | Array | [":generateContent", ":streamGenerateContent"] | Path patterns to additionally monitor on Google API hosts — see Configuration |
exclude_api_patterns | Array | ["/batchPredictionJobs/"] | Deny-list checked after intercept_addresses; matching paths are skipped. Unlike intercept_addresses, genuinely disables exclusion. See |
require 'openai'
require 'coolhand'
# Configure Coolhand
Coolhand.configure do |config|
config.api_key = 'your_api_key_here'
end
# Use OpenAI normally - requests are automatically logged
client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
response = client.chat(
parameters: {
model: "gpt-4o-mini",
messages: [{ role: "user", content: "These pretzels are making me thirsty!"}],
temperature: 0.7
}
)
puts response.dig("choices", 0, "message", "content")
# The request and response have been automatically logged to Coolhand!
📖 Complete Anthropic Integration Guide → - Supports both official and community gems with automatic detection
Use Coolhand.with_capture and Coolhand.without_capture to override the global capture setting for a specific block of code, without changing your configuration.
Capture a specific call when capture is globally disabled:
Coolhand.configure do |config|
config.api_key = 'your_api_key_here'
config.capture = false # disabled globally
end
Coolhand.with_capture do
response = openai_client.chat(parameters: {
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Hello!" }],
})
# This request is captured and forwarded to Coolhand
end
Skip capture for a specific call when capture is globally enabled:
Coolhand.without_capture do
response = openai_client.chat(parameters: { ... })
# This request is NOT forwarded to Coolhand
end
Both methods are thread-safe. Blocks nest correctly — the innermost block takes precedence, and the outer setting is restored when the block exits. The override is fiber-local, so it does not follow into new Threads, Fibers or Async tasks started inside the block; see Threads and background logging.
For inbound webhooks (like audio transcripts or tool calls), the automatic interceptor won't capture them since they're incoming requests TO your application. In these cases, use the simple forward_webhook helper method:
class WebhooksController < ApplicationController
def elevenlabs
raw_body = request.body.read
webhook_data = JSON.parse(raw_body)
# Forward to Coolhand with automatic field generation and binary filtering
Thread.new do
Coolhand.logger_service.forward_webhook(
webhook_body: webhook_data, # Required: webhook payload
source: "elevenlabs", # Required: service name
event_type: webhook_data["type"], # Optional: e.g., post_call_transcription
headers: request.headers, # Optional & recommended
)
end
render json: { status: "success" }
end
end
Required Parameters:
webhook_body - The webhook payload (Hash or parsed JSON)source - Service name (String, e.g., "elevenlabs", "stripe", "twilio")Optional Parameters:
event_type - Event type to append to URL (e.g., "post_call_transcription" → webhook://elevenlabs/post_call_transcription)headers - Request headers (automatically sanitized). Passing Rails request.headers is safe: when the input looks like a Rack env (it has a REQUEST_METHOD key or a lowercase dotted key such as rack.input, or every key is UPPER_CASE; in a plain hash use hyphenated or mixed-case names), only HTTP_* headers, CONTENT_TYPE and CONTENT_LENGTH are forwarded, so entries such as rack.*, REQUEST_METHOD, QUERY_STRING, or process env vars (DATABASE_URL, ...) are droppedurl - Optional URL override; credentials in userinfo and sensitive query params are redacted, and any #fragment is droppedconversation_id, agent_id, metadata - Custom fields for your tracking needsError Handling:
ArgumentError if required parameters are missingfalse if required parameters are missingThat's it! The forward_webhook method automatically:
The monitor captures:
Headers containing API keys are automatically sanitized for security.
Coolhand does not track or store binary data. The gem automatically filters out:
audio, audio_data, full_audio, raw_audio)image_data, image_content)file_content, binary_content)audio_base64, base64_data)This ensures:
The filtering is automatic and applies to all monitored API calls and webhook logging.
The monitor works with multiple transport layers and Ruby libraries:
Faraday-based libraries (via Faraday's default net_http adapter — a non-Net::HTTP adapter such as faraday-typhoeus is not covered):
Native HTTP libraries:
models.github.aiOther providers monitored out of the box:
generativelanguage.googleapis.com)aiplatform.googleapis.com) — see the Vertex AI batch result logging guide for async batch jobsbedrock-runtime.<region>.amazonaws.com, plus the .amazonaws.com.cn, .api.aws dual-stack and bedrock-runtime-fips.<region> variants — model invoke, Converse and the OpenAI-compatible endpoint)opencode.ai)api.deepseek.com)api.mistral.ai)api.perplexity.ai)api.x.ai)api.cohere.com and api.cohere.ai)api.typesafe.ai/v1/systemone)Universal Coverage: Since most Ruby HTTP libraries use Net::HTTP under the hood, Coolhand's single interceptor provides comprehensive monitoring without needing library-specific integrations.
Coolhand uses a unified Net::HTTP interceptor to capture outgoing requests to configured LLM API endpoints:
Net::HTTP using Module#prepend (a standard Ruby technique for wrapping library behavior)read_body interceptionWhen a request matches configured LLM endpoints:
For non-matching endpoints, requests pass through unchanged.
Enable verbose logging to see what's being intercepted:
Coolhand.configure do |config|
config.api_key = 'your_api_key_here'
config.silent = false # Enable console output
end
In test environments, you may want to configure differently:
# config/initializers/coolhand.rb
if Rails.env.test?
Coolhand.configure do |config|
config.api_key = 'test_key'
config.silent = true
end
end
For standard Ruby scripts or non-Rails applications:
#!/usr/bin/env ruby
require 'coolhand'
Coolhand.configure do |config|
config.api_key = 'your_api_key_here' # Store securely, don't commit to git
config.environment = 'production'
config.silent = false
end
# Your application code here...
🆓 Sign up for free at coolhandlabs.com to get your API key and start monitoring your LLM usage.
What you get:
The monitor handles errors gracefully:
search_templates, get_template, search_workloads, search_logs, get_log, search_help_articles, get_help_article, list_help_categories), the feedback-link methods and the upsert methods are the exception — they raise Coolhand::Error (Coolhand::HttpError for non-2xx responses); see Reading Templates →, Reading Workloads →, Reading Logs →, Linking Feedback → and Declared Workloads →Coolhand.with_context, and create or rename them with upsertx-ratelimit-remaining-tokens are kept)authorization/authorization_token, Realtime ephemeral client secrets, api_key, and similar) are redacted; see Body redactionCoolhand Labs builds LLM observability and feedback tooling so teams can monitor, understand, and improve their AI applications. Learn more at coolhandlabs.com.
Apache-2.0
exclude_api_patterns = []max_captured_body_bytes | Integer | 1_000_000 | Maximum size of a captured JSON request body or response body — oversized bodies are replaced with a placeholder (nil disables the cap). Non-JSON bodies (e.g. file/audio uploads) are always skipped regardless of size — see Advanced Configuration |
async_logging | Boolean | true | Send logs to Coolhand from a background thread so a slow Coolhand API never delays your LLM calls. Set to false to send inline — see Advanced Configuration |
auto_declare | Array | [] | Rails only: :active_job and/or :controller to declare a workload and template for LLM calls made from jobs and controller actions — see Declared Workloads |