coolhand 0.4.0 → 0.5.1
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/CHANGELOG.md +51 -0
- data/README.md +70 -149
- data/SECURITY.md +20 -0
- data/docs/configuration.md +59 -0
- data/docs/feedback.md +79 -0
- data/docs/openai.md +60 -0
- data/docs/vertex.md +54 -0
- data/lib/coolhand/api_service.rb +12 -1
- data/lib/coolhand/base_interceptor.rb +35 -107
- data/lib/coolhand/configuration.rb +2 -2
- data/lib/coolhand/logger_service.rb +3 -2
- data/lib/coolhand/net_http_interceptor.rb +28 -5
- data/lib/coolhand/open_ai/batch_result_processor.rb +2 -0
- data/lib/coolhand/open_ai/webhook_validator.rb +6 -2
- data/lib/coolhand/version.rb +1 -1
- data/lib/coolhand/vertex/batch_result_processor.rb +97 -40
- data/lib/coolhand/webhook_interceptor.rb +11 -0
- data/lib/coolhand.rb +1 -1
- metadata +15 -8
- data/.idea/coolhand-ruby.iml +0 -6
- data/CLAUDE.md +0 -34
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25393afdf95625b624ca2910cb3017685705529e9628a5d15cb3bdffdf122d52
|
|
4
|
+
data.tar.gz: 3e478c3b8494d5fef5bc87896fbf69cb9f93d088029b9a9745563c9a789b4473
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad635f9377f6cb81967fc24b0c2aa9f9938c41e308c6b98511ee0139b9938e01ea766baf72a085700866c69ac50207da11e29f04d30a94ad3b487d3e22243d9e
|
|
7
|
+
data.tar.gz: d6ef0baf4e37d29b36a0d7a2d6a41822f551a0a753a2dc963c0da9eed60b90bf29b5f0015caa8a628c6f340ec9cb2b3ef2c83ecb9b59fbcec909d70f396603df
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.1] - 2026-08-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`Coolhand::Vertex::BatchResultProcessor.new` accepts an optional `model:` keyword** (falls back to a `"model"` key in `batch_info` when omitted) to populate the `model` field on batch-result logs — included only when one of those is available and non-blank, and omitted from the payload entirely otherwise (#76).
|
|
14
|
+
- **`Coolhand::BaseInterceptor.send_complete_request_log` accepts optional `source_api:`/`model:` keywords**, for synthetic/batch log types (like the Vertex processor above) that aren't backed by a real intercepted HTTP request (#76).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Vertex batch results with a missing or malformed job resource name or unparseable `startTime`/`endTime` are no longer logged at all.** Previously the processor still sent a request log using whatever was in `batch_info["name"]` (even if blank), just with a broken URL. It now validates these fields and skips the log entirely — with a `Rails.logger.error` explaining why — rather than sending something unusable (#76).
|
|
18
|
+
- **A batch whose `endTime` precedes its `startTime` (e.g. clock skew) is still logged**, with `duration_ms` clamped to `0` and a warning logged, rather than dropped — unlike the missing/malformed cases above, the request/response content itself is still valid here (#76).
|
|
19
|
+
- **A Vertex batch result item that isn't a `Hash` with a `"request"` and/or `"response"` key is now rejected and logged as an error**, instead of being sent as a log with `nil` request/response bodies (#76).
|
|
20
|
+
- **The published gem no longer packages `.claude/`, `.idea/`, or `CLAUDE.md`** — these are repo/agent tooling, not part of the library.
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
- **A webhook processing error no longer leaves the request authenticated by default.** `Coolhand::WebhookInterceptor#intercept_batch_request` previously logged and swallowed *any* exception (a malformed webhook secret, a non-Hash JSON payload, a bug in `process_event`), returning normally without halting the Rails `before_action` chain — so the controller action still ran for a request whose signature was never confirmed valid. It now calls `head :unauthorized` in that rescue, matching the existing invalid-signature path, and validates the parsed webhook payload is a `Hash` before use.
|
|
24
|
+
- **A blank or whitespace-only OpenAI webhook secret is no longer usable as the HMAC key.** `Coolhand::OpenAi::WebhookValidator` previously only rejected `nil`/`false`, so `webhook_secret = ""` (e.g. an unset `ENV` var with a blank default) silently signed and verified requests with an empty key — a much weaker, easy-to-guess bypass than the already-known "secret not configured" path. It now uses the same `Coolhand.required_field?` blank-check used elsewhere in the gem.
|
|
25
|
+
- **`Cookie`/`Set-Cookie` headers are now redacted**, alongside the existing key/token/secret/signature/authorization patterns. Forwarding a Rails session cookie (a bearer credential) to Coolhand — e.g. via `LoggerService#forward_webhook(headers: request.headers)`, the pattern this gem's own docs show — was previously possible.
|
|
26
|
+
- **`BaseInterceptor.sanitize_url` now redacts a broader set of credential-shaped query parameters** (`sig`, `credential`, `password`, `auth`, in addition to the existing `key`/`token`/`secret`), matching the pattern-based approach already used for headers instead of a small exact-match list. This covers AWS SigV4 (`X-Amz-Signature`, `X-Amz-Credential`) and Google Cloud (`X-Goog-Signature`, `X-Goog-Credential`) presigned-URL parameters that were previously logged in full.
|
|
27
|
+
- **A failure capturing an intercepted request's body (e.g. an already-consumed `body_stream`) no longer prevents the real request from being attempted.** The capture happened before any error handling was in place, so an exception there escaped `NetHttpInterceptor#request` entirely — the host app's actual LLM call never happened. It's now rescued, logged, and treated as "body unavailable," and the real request still proceeds.
|
|
28
|
+
- **Streamed response bodies are no longer buffered into a thread-local for requests this gem never intercepted.** `ResponseInterceptor#read_body` previously buffered every block-form `read_body` call process-wide, regardless of whether the enclosing request matched `intercept_addresses` — an unbounded memory growth path on any thread that streams large non-LLM responses (file downloads, etc.). Buffering now only happens while a `NetHttpInterceptor#request` call is actively capturing.
|
|
29
|
+
- **A request made from inside another intercepted request's streaming callback no longer corrupts the outer request's logged response body.** The per-thread stream buffer is now saved and restored around each intercepted request instead of unconditionally cleared, so nested/reentrant LLM calls (e.g. a second provider call triggered from a chunk handler) no longer mix one request's content into another's log entry.
|
|
30
|
+
- **Coolhand's own request to its backend API is no longer re-intercepted and re-logged.** If a self-hosted `base_url` or custom `intercept_addresses` entry happens to match Coolhand's own API host, the log-shipping request itself would previously be captured and logged like any other request — which itself gets logged, recursively, without bound. It's now wrapped in `Coolhand.without_capture`.
|
|
31
|
+
- `Coolhand::OpenAi::WebhookValidator#webhook_secret` is no longer a public reader (it's only used internally) — reduces the chance of the raw secret ending up in error-tracker breadcrumbs or view-rendered controller state.
|
|
32
|
+
- `YAML.load_file` → `YAML.safe_load_file` for the gem's own bundled default-addresses/patterns YAML files — defense-in-depth against unsafe deserialization, consistent with modern Psych defaults.
|
|
33
|
+
- Added missing `require "openssl"` (`WebhookValidator`) and `require "stringio"` (`NetHttpInterceptor`) instead of relying on both being transitively loaded by something else first.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- **Vertex batch result logging** — `Coolhand::Vertex::BatchResultProcessor` now sends a fully-qualified URL (`https://aiplatform.googleapis.com/v1/<resource name>`) instead of the bare Vertex job resource name, plus explicit `source_api` and `model` values, all inside `raw_request`. The Coolhand API treats `raw_request` as free-form JSON, so this improves the data available to its classification without depending on new top-level payload fields being accepted. Also fixed: `timestamp`/`completed_at` are now sent as proper ISO 8601 strings instead of raw `Time` objects, and the URL is now run through the same `sanitize_url` helper used by every other logging path in the gem, for consistency (a Vertex batch URL can't actually carry a redactable query string today, so this is defense-in-depth rather than a fix for a live leak) (#76).
|
|
37
|
+
- **`require "coolhand"` no longer requires `faraday`.** The gem hasn't used Faraday directly since the 0.4.0 unified `NetHttpInterceptor`, but a stale top-level `require "faraday"` meant loading the gem could raise `LoadError` for anyone without faraday installed — it was never a declared dependency. If your app relied on `require "coolhand"` transitively loading Faraday, require it yourself.
|
|
38
|
+
- `Coolhand::Vertex::BatchResultProcessor`, `Coolhand::OpenAi::BatchResultProcessor`, and `Coolhand::OpenAi::WebhookValidator` can now each be required independently (e.g. `require "coolhand/vertex/batch_result_processor"`) without first requiring `"coolhand"` — previously this could raise `NameError` depending on load order.
|
|
39
|
+
- `Coolhand::BaseInterceptor.send_complete_request_log` now runs `request_headers`/`response_headers` through `sanitize_headers` itself, rather than trusting every caller to have pre-sanitized them. `NetHttpInterceptor` already sanitized before calling it (`sanitize_headers` is idempotent, so no behavior change there); this closes the gap for any future caller that forgets.
|
|
40
|
+
- `Coolhand::Vertex::BatchResultProcessor#handle_failed_batch` no longer raises `NoMethodError` when a failed batch's `error` field is missing or not a Hash — it now logs the batch-failure message either way instead of falling through to the generic "failed to process" catch-all.
|
|
41
|
+
- **README's "Request Flow" section corrected** — it previously claimed data is sent to the Coolhand API "asynchronously in a background thread" with "zero performance impact"; it's actually sent inline, after the original response is available, bounded by the 5-second timeout described in 0.5.0's Security section.
|
|
42
|
+
|
|
43
|
+
## [0.5.0] - 2026-07-30
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- **`llm_request_log_id` and `workload_id` in feedback API responses are now hashid strings, not raw integers** — the Coolhand API now returns these as hashids, matching every other external-facing identifier on the record (they previously leaked the raw integer foreign key). This gem never typed or coerced these fields (plain hashes throughout), so no code changes are required here, but if your application stores or compares `result[:llm_request_log_id]` or `result[:workload_id]` as an integer, update it to treat the value as an opaque string identifier instead. The `create_feedback`/`update_feedback` input fields (`llm_request_log_id`, `workload_hashid`) are unaffected — they still accept either a raw integer or a hashid string.
|
|
47
|
+
- **`id` in feedback API responses has actually been a hashid string for some time** — flagging here since it's the same category of field; no gem-level change needed since this was never typed.
|
|
48
|
+
- `BaseInterceptor.sanitize_headers` and `LoggerService#sanitize_headers` (used for the main request/response logging path and webhook forwarding, respectively) now share the same sensitive-header pattern instead of each maintaining its own list, so both paths redact consistently.
|
|
49
|
+
|
|
50
|
+
### Security
|
|
51
|
+
- **AWS Bedrock SigV4 session tokens (`X-Amz-Security-Token`) are now redacted before logging.** The interceptor's header sanitizer used a hardcoded list of known API-key header names (`api-key`, `x-api-key`, `x-goog-api-key`, `openai-api-key`) instead of a general pattern, so this header — present on requests signed with temporary/STS credentials, the common case for Bedrock — was forwarded to the Coolhand backend and printed in `debug_mode` unredacted. The sanitizer now redacts any header whose name matches `key`, `token`, `secret`, `signature`, or `authorization`, closing this and similar gaps for any current or future provider header.
|
|
52
|
+
- **`debug_mode`'s "skipping capture" log line no longer prints the raw URL.** When a request matched `exclude_api_patterns` while `debug_mode` was on, the log line bypassed the usual URL sanitizer, so a Gemini/Vertex `?key=...` query-param API key could be printed in full. It now goes through the same URL sanitizer as every other log line.
|
|
53
|
+
- **Outbound requests to the Coolhand backend now set a 5-second connect/read timeout.** Previously this call had no explicit timeout and ran inline on the same thread as the intercepted LLM request, so a slow or unreachable Coolhand endpoint (including a self-hosted `base_url`) could add Ruby's ~60s Net::HTTP default (up to ~120s total) of latency to real LLM calls made by the host app.
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
- Deleted unused `BaseInterceptor` methods with no callers anywhere in the gem: `extract_response_data`, `extract_usage_metadata`, `clean_request_headers`, `clean_response_headers`. The latter two duplicated `sanitize_headers` with a narrower, case-sensitive header list and were never wired into any interceptor — dead code carrying its own security debt.
|
|
57
|
+
|
|
58
|
+
### Dependencies
|
|
59
|
+
- Bumped `faraday` from 2.14.2 to 2.14.3 (#73).
|
|
60
|
+
|
|
10
61
|
## [0.4.0] - 2026-06-22
|
|
11
62
|
|
|
12
63
|
### Added
|
data/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/coolhand)
|
|
4
4
|
|
|
5
|
-
Monitor and log LLM API calls
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
> **Scope**: Coolhand intercepts outgoing HTTP requests to configured LLM API endpoints only. It does not read, scan, or transmit your source code files.
|
|
6
8
|
|
|
7
9
|
## Installation
|
|
8
10
|
|
|
@@ -37,38 +39,21 @@ end
|
|
|
37
39
|
# ✅ OpenAI SDK calls
|
|
38
40
|
# ✅ Anthropic API calls
|
|
39
41
|
# ✅ Direct HTTP requests to AI APIs
|
|
40
|
-
# ✅
|
|
42
|
+
# ✅ Any library making AI API calls via Faraday's default adapter
|
|
41
43
|
|
|
42
44
|
# NO code changes needed in your existing services!
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
**✨ Why Automatic Monitoring:**
|
|
46
48
|
- 🚫 **Zero refactoring** - No code changes to existing services
|
|
47
|
-
- 📊 **Complete coverage** - Monitors
|
|
49
|
+
- 📊 **Complete coverage** - Monitors AI libraries using Net::HTTP or Faraday's default adapter automatically
|
|
48
50
|
- 🔒 **Security built-in** - Automatic credential sanitization
|
|
49
51
|
- ⚡ **Performance optimized** - Negligible overhead via async logging
|
|
50
52
|
- 🛡️ **Future-proof** - Automatically captures new AI calls added by your team
|
|
51
53
|
|
|
52
54
|
## Self-Hosted Deployments
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```ruby
|
|
57
|
-
Coolhand.configure do |config|
|
|
58
|
-
config.api_key = ENV['COOLHAND_API_KEY']
|
|
59
|
-
config.base_url = ENV['COOLHAND_BASE_URL'] # e.g. "https://coolhand.internal.example.com/api"
|
|
60
|
-
end
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
When `base_url` is unset the SDK defaults to `https://coolhandlabs.com/api` and behaviour is unchanged.
|
|
64
|
-
|
|
65
|
-
**Accepted values:**
|
|
66
|
-
- Any `https://` URL — required for production use
|
|
67
|
-
- `http://localhost` or `http://127.0.0.1` — accepted for local development only
|
|
68
|
-
|
|
69
|
-
**Trailing slashes** are stripped automatically, so `"https://example.com/api/"` and `"https://example.com/api"` are equivalent.
|
|
70
|
-
|
|
71
|
-
The SDK raises `Coolhand::Error` at configure time if `base_url` is set to a plain `http://` URL pointing at a non-localhost host.
|
|
56
|
+
Point the SDK at your own Coolhand-compatible endpoint via `config.base_url` for compliance or data-residency requirements. See [Self-Hosted Deployments →](docs/configuration.md).
|
|
72
57
|
|
|
73
58
|
## Feedback API
|
|
74
59
|
|
|
@@ -80,10 +65,10 @@ Collect feedback on LLM responses to improve model performance.
|
|
|
80
65
|
require 'coolhand'
|
|
81
66
|
|
|
82
67
|
# Create feedback for an LLM response
|
|
83
|
-
feedback_service = Coolhand::FeedbackService.new
|
|
68
|
+
feedback_service = Coolhand::FeedbackService.new
|
|
84
69
|
|
|
85
70
|
feedback = feedback_service.create_feedback(
|
|
86
|
-
llm_request_log_id:
|
|
71
|
+
llm_request_log_id: 'abc123def456', # hashid from a prior response; a raw integer FK also still works
|
|
87
72
|
llm_provider_unique_id: 'req_xxxxxxx',
|
|
88
73
|
client_unique_id: 'workorder-chat-456',
|
|
89
74
|
creator_unique_id: 'user-789',
|
|
@@ -94,22 +79,7 @@ feedback = feedback_service.create_feedback(
|
|
|
94
79
|
)
|
|
95
80
|
```
|
|
96
81
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
### Matching Fields
|
|
100
|
-
- **`llm_request_log_id`** 🎯 *Exact Match* - ID from the Coolhand API response when the original LLM request was logged. Provides exact matching.
|
|
101
|
-
- **`llm_provider_unique_id`** 🎯 *Exact Match* - The x-request-id from the LLM API response (e.g., "req_xxxxxxx")
|
|
102
|
-
- **`original_output`** 🔍 *Fuzzy Match* - The original LLM response text. Provides fuzzy matching but isn't 100% reliable.
|
|
103
|
-
- **`client_unique_id`** 🔗 *Your Internal Matcher* - Connect to an identifier from your system for internal matching
|
|
104
|
-
|
|
105
|
-
### Quality Data
|
|
106
|
-
- **`revised_output`** ⭐ *Best Signal* - End user revision of the LLM response. The highest value data for improving quality scores.
|
|
107
|
-
- **`explanation`** 💬 *Medium Signal* - End user explanation of why the response was good or bad. Valuable qualitative data.
|
|
108
|
-
- **`sentiment`** 🎭 *Preferred* - String sentiment: `'like'`, `'dislike'`, or `'neutral'`. Takes precedence over `like` if both are provided. The gem automatically converts `like` to `sentiment` before sending.
|
|
109
|
-
- **`like`** 👍 *Low Signal (Deprecated)* - Boolean: `true` = like, `false` = dislike. Use `sentiment` instead. Conversion: `true` → `"like"`, `false` → `"dislike"`.
|
|
110
|
-
- **`workload_hashid`** 🔗 *Workload Association* - Hashid of a workload to associate this feedback with.
|
|
111
|
-
- **`creator_unique_id`** 👤 *User Tracking* - Unique ID to match feedback to the end user who created it
|
|
112
|
-
- **`creator_type`** 🧑🤝🤖 *Creator Type* - What kind of creator submitted the feedback: `'human'`, `'agent'`, or `'unknown'`.
|
|
82
|
+
For a full field reference and matching strategy, see [Feedback API →](docs/feedback.md).
|
|
113
83
|
|
|
114
84
|
## Rails Integration
|
|
115
85
|
|
|
@@ -138,7 +108,7 @@ end
|
|
|
138
108
|
```ruby
|
|
139
109
|
class ChatController < ApplicationController
|
|
140
110
|
def create_feedback
|
|
141
|
-
feedback_service = Coolhand::FeedbackService.new
|
|
111
|
+
feedback_service = Coolhand::FeedbackService.new
|
|
142
112
|
|
|
143
113
|
feedback = feedback_service.create_feedback(
|
|
144
114
|
llm_request_log_id: params[:log_id],
|
|
@@ -163,7 +133,7 @@ end
|
|
|
163
133
|
```ruby
|
|
164
134
|
class FeedbackCollectionJob < ApplicationJob
|
|
165
135
|
def perform(feedback_data)
|
|
166
|
-
feedback_service = Coolhand::FeedbackService.new
|
|
136
|
+
feedback_service = Coolhand::FeedbackService.new
|
|
167
137
|
|
|
168
138
|
feedback_service.create_feedback(
|
|
169
139
|
llm_provider_unique_id: feedback_data[:request_id],
|
|
@@ -184,6 +154,7 @@ end
|
|
|
184
154
|
|--------|------|---------|-------------|
|
|
185
155
|
| `api_key` | String | `nil` | Your Coolhand API key. If absent, intercepted requests are skipped with a warning log rather than raising at boot time |
|
|
186
156
|
| `enabled` | Boolean | `true` | Set to `false` to disable all patching and validation (e.g. `Rails.env.production?`) |
|
|
157
|
+
| `capture` | Boolean | `true` | Whether to capture and forward intercepted requests. Set to `false` to monitor without forwarding, then use [`Coolhand.with_capture`](#selective-capture) to re-enable selectively |
|
|
187
158
|
| `silent` | Boolean | `false` | Whether to suppress console output |
|
|
188
159
|
| `intercept_addresses` | Array | `["api.openai.com", "api.anthropic.com"]` | Array of API endpoint strings to monitor |
|
|
189
160
|
|
|
@@ -217,6 +188,38 @@ puts response.dig("choices", 0, "message", "content")
|
|
|
217
188
|
|
|
218
189
|
📖 **[Complete Anthropic Integration Guide →](docs/anthropic.md)** - Supports both official and community gems with automatic detection
|
|
219
190
|
|
|
191
|
+
### Selective Capture
|
|
192
|
+
|
|
193
|
+
Use `Coolhand.with_capture` and `Coolhand.without_capture` to override the global `capture` setting for a specific block of code, without changing your configuration.
|
|
194
|
+
|
|
195
|
+
**Capture a specific call when capture is globally disabled:**
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
Coolhand.configure do |config|
|
|
199
|
+
config.api_key = 'your_api_key_here'
|
|
200
|
+
config.capture = false # disabled globally
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
Coolhand.with_capture do
|
|
204
|
+
response = openai_client.chat(parameters: {
|
|
205
|
+
model: "gpt-4o-mini",
|
|
206
|
+
messages: [{ role: "user", content: "Hello!" }],
|
|
207
|
+
})
|
|
208
|
+
# This request is captured and forwarded to Coolhand
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Skip capture for a specific call when capture is globally enabled:**
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
Coolhand.without_capture do
|
|
216
|
+
response = openai_client.chat(parameters: { ... })
|
|
217
|
+
# This request is NOT forwarded to Coolhand
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Both methods are thread-safe. Blocks nest correctly — the innermost block takes precedence, and the outer setting is restored when the block exits.
|
|
222
|
+
|
|
220
223
|
## Logging Inbound Webhooks
|
|
221
224
|
|
|
222
225
|
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:
|
|
@@ -294,39 +297,45 @@ The filtering is automatic and applies to all monitored API calls and webhook lo
|
|
|
294
297
|
|
|
295
298
|
The monitor works with multiple transport layers and Ruby libraries:
|
|
296
299
|
|
|
297
|
-
**Faraday-based libraries
|
|
300
|
+
**Faraday-based libraries** (via Faraday's default `net_http` adapter — a non-Net::HTTP adapter such as `faraday-typhoeus` is not covered):
|
|
298
301
|
- OpenAI Ruby SDK
|
|
299
302
|
- ruby-anthropic gem (community Anthropic gem)
|
|
300
303
|
- ruby-openai gem
|
|
301
304
|
- LangChain.rb
|
|
302
|
-
- Direct Faraday requests
|
|
303
|
-
- Any other Faraday-based HTTP client
|
|
305
|
+
- Direct Faraday requests using the default adapter
|
|
304
306
|
|
|
305
307
|
**Native HTTP libraries:**
|
|
306
308
|
- Official Anthropic Ruby SDK (using Net::HTTP)
|
|
307
309
|
- GitHub Models SDK / any client using `models.github.ai`
|
|
308
310
|
- Any library using Net::HTTP directly
|
|
309
311
|
|
|
312
|
+
**Other providers monitored out of the box:**
|
|
313
|
+
- Google Gemini (`generativelanguage.googleapis.com`)
|
|
314
|
+
- Google Vertex AI (`aiplatform.googleapis.com`) — see the [Vertex AI batch result logging guide](docs/vertex.md) for async batch jobs
|
|
315
|
+
- AWS Bedrock (OpenAI-compatible endpoint)
|
|
316
|
+
- Cloudflare AI Gateway
|
|
317
|
+
- OpenRouter
|
|
318
|
+
|
|
310
319
|
**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.
|
|
311
320
|
|
|
312
321
|
## How It Works
|
|
313
322
|
|
|
314
|
-
Coolhand uses a unified Net::HTTP interceptor to
|
|
323
|
+
Coolhand uses a unified Net::HTTP interceptor to capture outgoing requests to configured LLM API endpoints:
|
|
315
324
|
|
|
316
325
|
### Net::HTTP Interceptor
|
|
317
|
-
-
|
|
318
|
-
-
|
|
326
|
+
- Extends Ruby's `Net::HTTP` using `Module#prepend` (a standard Ruby technique for wrapping library behavior)
|
|
327
|
+
- Covers HTTP libraries that delegate to Net::HTTP under the hood (which is most of them)
|
|
319
328
|
- Handles both standard requests and streaming responses via `read_body` interception
|
|
320
329
|
- Thread-safe design using thread-local storage for streaming buffers
|
|
321
330
|
|
|
322
331
|
### Request Flow
|
|
323
332
|
When a request matches configured LLM endpoints:
|
|
324
333
|
|
|
325
|
-
1. The original request executes normally
|
|
334
|
+
1. The original request executes normally
|
|
326
335
|
2. Request and response data (body, headers, status) are captured by the interceptor
|
|
327
336
|
3. For streaming requests, the complete accumulated response is captured (not individual chunks)
|
|
328
|
-
4. Data is sent to the Coolhand API
|
|
329
|
-
5. Your application continues
|
|
337
|
+
4. Data is sent to the Coolhand API inline, after the original response is available (bounded by a 5-second connect/read timeout so a slow or unreachable Coolhand backend can't hang your request indefinitely)
|
|
338
|
+
5. Your application continues once the log is sent (or times out) — the original request/response is never blocked on Coolhand being reachable
|
|
330
339
|
|
|
331
340
|
For non-matching endpoints, requests pass through unchanged.
|
|
332
341
|
|
|
@@ -392,106 +401,14 @@ The monitor handles errors gracefully:
|
|
|
392
401
|
- Invalid API keys will be reported but won't crash your app
|
|
393
402
|
- Network issues are handled with appropriate error messages
|
|
394
403
|
|
|
404
|
+
## Documentation
|
|
395
405
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
- Include the interceptor in your controller:
|
|
403
|
-
include Coolhand::WebhookInterceptor
|
|
404
|
-
- Add the before_action to validate and populate @validator payload:
|
|
405
|
-
before_action :intercept_batch_request, only: :openai
|
|
406
|
-
- Ensure you skip CSRF for the webhook endpoint:
|
|
407
|
-
skip_before_action :verify_authenticity_token
|
|
408
|
-
- Override the webhook_secret method to return your OpenAI webhook secret
|
|
409
|
-
|
|
410
|
-
Minimal example (only key lines shown):
|
|
411
|
-
|
|
412
|
-
```ruby
|
|
413
|
-
# app/controllers/webhooks/batch_api_requests_controller.rb
|
|
414
|
-
# ...existing code...
|
|
415
|
-
include Coolhand::WebhookInterceptor
|
|
416
|
-
|
|
417
|
-
skip_before_action :verify_authenticity_token
|
|
418
|
-
before_action :intercept_batch_request, only: :openai
|
|
419
|
-
|
|
420
|
-
def openai
|
|
421
|
-
event = JSON.parse(@validator.payload)
|
|
422
|
-
case event["type"]
|
|
423
|
-
when "batch.completed", "batch.failed", "batch.expired", "batch.cancelled"
|
|
424
|
-
batch_id = event.dig("data", "id")
|
|
425
|
-
batch_request = BatchApiRequest.find_by(provider: "openai", provider_batch_id: batch_id)
|
|
426
|
-
|
|
427
|
-
if batch_request
|
|
428
|
-
OpenAi::BatchResultProcessor.perform_async(batch_request.id)
|
|
429
|
-
Rails.logger.info("Queued batch result processing for BatchApiRequest #{batch_request.id}")
|
|
430
|
-
else
|
|
431
|
-
Rails.logger.warn("Could not find BatchApiRequest for OpenAI batch ID: #{batch_id}")
|
|
432
|
-
end
|
|
433
|
-
else
|
|
434
|
-
Rails.logger.info("Unhandled OpenAI webhook event type: #{event["type"]}")
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
head :ok
|
|
438
|
-
rescue JSON::ParserError
|
|
439
|
-
head :bad_request
|
|
440
|
-
rescue StandardError => e
|
|
441
|
-
Rails.logger.error("OpenAI webhook error: #{e.message}")
|
|
442
|
-
head :internal_server_error
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
def webhook_secret
|
|
446
|
-
Rails.application.credentials.openai_webhook_secret
|
|
447
|
-
end
|
|
448
|
-
# ...existing code...
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
## Batch webhook handler (Vertex)
|
|
452
|
-
|
|
453
|
-
Automatically handle Vertex batch event logs.
|
|
454
|
-
|
|
455
|
-
Usage:
|
|
456
|
-
- call Coolhand::Vertex::BatchResultProcessor service with batch_info and download batch results
|
|
457
|
-
|
|
458
|
-
Minimal example (only key lines shown):
|
|
459
|
-
|
|
460
|
-
```ruby
|
|
461
|
-
class Vertex::BatchCallbackProcessor < BaseService
|
|
462
|
-
option :batch_request, model: BatchApiRequest
|
|
463
|
-
option :batch_info
|
|
464
|
-
|
|
465
|
-
def call
|
|
466
|
-
case batch_info["state"]
|
|
467
|
-
when "JOB_STATE_PENDING"
|
|
468
|
-
nil
|
|
469
|
-
when "JOB_STATE_RUNNING", "JOB_STATE_QUEUED"
|
|
470
|
-
batch_request.update!(status: "processing")
|
|
471
|
-
|
|
472
|
-
Coolhand::Vertex::BatchResultProcessor.new(batch_info:).call
|
|
473
|
-
when "JOB_STATE_SUCCEEDED"
|
|
474
|
-
output_file_id = batch_info["outputInfo"]["gcsOutputDirectory"]
|
|
475
|
-
results = download_batch_results(output_file_id)
|
|
476
|
-
results.each { |batch_item| process_batch_result(batch_item) }
|
|
477
|
-
|
|
478
|
-
batch_request.update!(status: "completed", completed_at: Time.current, output_file_id:)
|
|
479
|
-
|
|
480
|
-
Coolhand::Vertex::BatchResultProcessor.new(batch_info:).call(results)
|
|
481
|
-
|
|
482
|
-
# Clean up GCS files after successful processing
|
|
483
|
-
cleanup_gcs_files(output_file_id)
|
|
484
|
-
when "JOB_STATE_FAILED"
|
|
485
|
-
handle_failed_batch(batch_info["error"]["message"])
|
|
486
|
-
end
|
|
487
|
-
end
|
|
488
|
-
end
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
## Integration Guides
|
|
492
|
-
|
|
493
|
-
- **[Anthropic Integration](docs/anthropic.md)** - Complete guide for both official and community Anthropic gems, including streaming, dual gem handling, and troubleshooting
|
|
494
|
-
- **[ElevenLabs Integration](docs/elevenlabs.md)** - Complete guide for integrating ElevenLabs Conversational AI with webhook capture and feedback submission
|
|
406
|
+
- **[Configuration](docs/configuration.md)** — Self-hosted deployments, base_url rules, debug mode, custom intercept addresses
|
|
407
|
+
- **[Feedback API](docs/feedback.md)** — Full field reference, matching strategies, sentiment values
|
|
408
|
+
- **[Anthropic Integration](docs/anthropic.md)** — Official and community Anthropic Ruby gems, streaming, dual gem handling, and troubleshooting
|
|
409
|
+
- **[ElevenLabs Integration](docs/elevenlabs.md)** — Webhook capture, feedback submission, and Rails integration
|
|
410
|
+
- **[OpenAI Batch Webhook Handler](docs/openai.md)** — Handle OpenAI batch job completion events via webhook interception
|
|
411
|
+
- **[Google Vertex AI Batch Result Logging](docs/vertex.md)** — Log completed Vertex AI batch prediction job results
|
|
495
412
|
|
|
496
413
|
## Security
|
|
497
414
|
|
|
@@ -511,6 +428,10 @@ end
|
|
|
511
428
|
- **Contribute?** [Submit a pull request](https://github.com/Coolhand-Labs/coolhand-ruby/pulls)
|
|
512
429
|
- **Support?** Visit [coolhandlabs.com](https://coolhandlabs.com)
|
|
513
430
|
|
|
431
|
+
## About Coolhand Labs
|
|
432
|
+
|
|
433
|
+
Coolhand Labs builds LLM observability and feedback tooling so teams can monitor, understand, and improve their AI applications. Learn more at [coolhandlabs.com](https://coolhandlabs.com).
|
|
434
|
+
|
|
514
435
|
## License
|
|
515
436
|
|
|
516
437
|
Apache-2.0
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Only the latest published version of `coolhand` on RubyGems is supported with security fixes.
|
|
6
|
+
Please upgrade to the latest version before reporting an issue.
|
|
7
|
+
|
|
8
|
+
## Reporting a Vulnerability
|
|
9
|
+
|
|
10
|
+
Please do **not** open a public GitHub issue for security vulnerabilities.
|
|
11
|
+
|
|
12
|
+
Instead, report vulnerabilities privately using one of the following:
|
|
13
|
+
|
|
14
|
+
- [GitHub Security Advisories](https://github.com/Coolhand-Labs/coolhand-ruby/security/advisories/new)
|
|
15
|
+
for this repository (preferred)
|
|
16
|
+
- Email team@coolhandlabs.com
|
|
17
|
+
|
|
18
|
+
Please include a description of the vulnerability, steps to reproduce, and the impact you believe
|
|
19
|
+
it has. We aim to acknowledge reports within 3 business days and to provide a fix or mitigation
|
|
20
|
+
plan within 30 days, depending on severity.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Advanced Configuration
|
|
2
|
+
|
|
3
|
+
## Self-Hosted Deployments
|
|
4
|
+
|
|
5
|
+
For compliance, data-residency, or cost reasons you can run your own Coolhand-compatible endpoint and point the SDK at it via `config.base_url`:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Coolhand.configure do |config|
|
|
9
|
+
config.api_key = ENV['COOLHAND_API_KEY']
|
|
10
|
+
config.base_url = ENV['COOLHAND_BASE_URL'] # e.g. "https://coolhand.internal.example.com/api"
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
When `base_url` is unset the SDK defaults to `https://coolhandlabs.com/api` and behaviour is unchanged.
|
|
15
|
+
|
|
16
|
+
**URL validation rules:**
|
|
17
|
+
- Any `https://` URL — required for production use
|
|
18
|
+
- `http://localhost` or `http://127.0.0.1` — accepted for local development only
|
|
19
|
+
- Non-HTTPS remote URLs are rejected: the SDK raises `Coolhand::Error` at configure time if `base_url` is set to a plain `http://` URL pointing at a non-localhost host
|
|
20
|
+
|
|
21
|
+
**Trailing slashes** are stripped automatically, so `"https://example.com/api/"` and `"https://example.com/api"` are equivalent.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Debug Mode
|
|
26
|
+
|
|
27
|
+
`config.debug_mode` is a local-development aid: it prints every prepared payload to the console instead of sending it to the Coolhand API (or your self-hosted `base_url`), so you can inspect exactly what would be logged without an API key or network access.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
Coolhand.configure do |config|
|
|
31
|
+
config.debug_mode = Rails.env.development?
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**What changes when `debug_mode` is `true`:**
|
|
36
|
+
- No HTTP request is made — `create_log`, `create_feedback`, and `send_llm_request_log` all return `nil` and print the payload via `JSON.pretty_generate` instead.
|
|
37
|
+
- Capture is forced on for every request, including inside a `Coolhand.without_capture` block and when `config.capture = false`. This is intentional — debug mode is meant to show you everything, not respect capture suppression — so don't leave it enabled in an environment where you rely on `without_capture` to keep specific calls out of the logs.
|
|
38
|
+
- Header and URL sanitization (`[REDACTED]` API keys/tokens, redacted `key=`/`token=` query params) still applies to the printed payload, exactly as it would to a real request.
|
|
39
|
+
|
|
40
|
+
Because it forces capture unconditionally and prints full request/response bodies to the console, only enable `debug_mode` in development — never in production or in an environment handling real user data.
|
|
41
|
+
|
|
42
|
+
## Custom Intercept Addresses
|
|
43
|
+
|
|
44
|
+
By default Coolhand captures requests to a built-in list of LLM API hosts (OpenAI, Anthropic, Google Gemini, ElevenLabs, GitHub Models, and more). To capture a custom endpoint — an internal proxy, a self-hosted model server, or a third-party gateway — override `intercept_addresses`:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
Coolhand.configure do |config|
|
|
48
|
+
config.api_key = ENV['COOLHAND_API_KEY']
|
|
49
|
+
config.intercept_addresses = [
|
|
50
|
+
'my-llm-proxy.internal',
|
|
51
|
+
'api.openai.com', # include the defaults you still want
|
|
52
|
+
'api.anthropic.com',
|
|
53
|
+
]
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Setting `intercept_addresses` **replaces** the default list entirely, so include any default hosts you still need.
|
|
58
|
+
|
|
59
|
+
The default list can be found in `Coolhand::Configuration::DEFAULT_INTERCEPT_ADDRESSES`.
|
data/docs/feedback.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Feedback API
|
|
2
|
+
|
|
3
|
+
Collect user feedback on LLM responses to improve your AI outputs. The Feedback API lets you capture sentiment ratings, explanations, and human-corrected outputs.
|
|
4
|
+
|
|
5
|
+
> **Frontend widget:** For browser-based feedback collection, see [coolhand-js](https://github.com/Coolhand-Labs/coolhand-js) — a lightweight JavaScript widget that captures actionable user feedback on any AI output.
|
|
6
|
+
|
|
7
|
+
## Basic Usage
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require 'coolhand'
|
|
11
|
+
|
|
12
|
+
feedback_service = Coolhand::FeedbackService.new
|
|
13
|
+
|
|
14
|
+
# Positive feedback linked by log ID (most reliable)
|
|
15
|
+
feedback_service.create_feedback(
|
|
16
|
+
llm_request_log_id: 'abc123def456', # hashid from a prior response; a raw integer FK also still works
|
|
17
|
+
sentiment: 'like',
|
|
18
|
+
explanation: 'Clear and accurate answer.',
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Negative feedback with a human correction
|
|
22
|
+
feedback_service.create_feedback(
|
|
23
|
+
original_output: 'The capital of France is London.',
|
|
24
|
+
sentiment: 'dislike',
|
|
25
|
+
revised_output: 'The capital of France is Paris.',
|
|
26
|
+
explanation: 'Factually wrong.',
|
|
27
|
+
)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Field Reference
|
|
33
|
+
|
|
34
|
+
All fields are optional. Use at least one **Matching Field** to link feedback to its originating LLM request.
|
|
35
|
+
|
|
36
|
+
### Matching Fields
|
|
37
|
+
|
|
38
|
+
These fields identify which LLM request the feedback refers to. Use the most specific one available.
|
|
39
|
+
|
|
40
|
+
| Field | Match type | Description |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `llm_request_log_id` | Exact | Hashid returned when the original request was logged (a raw integer FK is also still accepted for backward compatibility). Most reliable. |
|
|
43
|
+
| `llm_provider_unique_id` | Exact | The provider's own request ID (e.g. `x-request-id` from Anthropic or OpenAI). |
|
|
44
|
+
| `client_unique_id` | Exact | Your own internal identifier for the request (e.g. a database row ID). |
|
|
45
|
+
| `original_output` | Fuzzy | The raw text the LLM produced. Used for fuzzy matching when no ID is available — less reliable. |
|
|
46
|
+
|
|
47
|
+
### Quality Signals
|
|
48
|
+
|
|
49
|
+
| Field | Signal strength | Description |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `revised_output` | ⭐ Best | The human-corrected version of the LLM output. Highest-value signal for quality improvement. |
|
|
52
|
+
| `explanation` | Medium | Free-text reason the response was good or bad. |
|
|
53
|
+
| `sentiment` | Low–Medium | `"like"`, `"dislike"`, or `"neutral"`. **Preferred** over the deprecated `like` boolean. |
|
|
54
|
+
| `like` | Low (deprecated) | Boolean: `true` = like, `false` = dislike. Auto-converted to `sentiment` before submission. Use `sentiment` instead. |
|
|
55
|
+
|
|
56
|
+
### Attribution Fields
|
|
57
|
+
|
|
58
|
+
| Field | Description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `creator_unique_id` | ID of the user providing feedback (for per-user quality tracking). |
|
|
61
|
+
| `creator_type` | Who submitted the feedback: `"human"`, `"agent"`, or `"unknown"`. |
|
|
62
|
+
| `workload_hashid` | Associate feedback with a specific workload in the Coolhand dashboard. |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Sentiment Values
|
|
67
|
+
|
|
68
|
+
| Value | Meaning |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `"like"` | The response was helpful / correct |
|
|
71
|
+
| `"dislike"` | The response was unhelpful / wrong |
|
|
72
|
+
| `"neutral"` | Neither good nor bad (e.g. factual but irrelevant) |
|
|
73
|
+
|
|
74
|
+
The deprecated `like: bool` field is still accepted and automatically converted:
|
|
75
|
+
|
|
76
|
+
| `like` (bool) | Equivalent `sentiment` |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `true` | `"like"` |
|
|
79
|
+
| `false` | `"dislike"` |
|
data/docs/openai.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# OpenAI Batch Webhook Handler
|
|
2
|
+
|
|
3
|
+
Automatically handle OpenAI batch event logs (`batch.completed`, `batch.failed`, `batch.expired`, `batch.cancelled`) by intercepting webhook requests and logging completed batch results to Coolhand.
|
|
4
|
+
|
|
5
|
+
For monitoring regular (non-batch) OpenAI API calls, see the [OpenAI client example](../README.md#with-openai-ruby-client) in the main README — no extra setup is required beyond `Coolhand.configure`.
|
|
6
|
+
|
|
7
|
+
Requires Rails — `Coolhand::WebhookInterceptor` and `Coolhand::OpenAi::BatchResultProcessor` log via `Rails.logger` internally. `config.capture = false` and `Coolhand.without_capture` do not suppress these logs: unlike the passive Net::HTTP interceptor, `intercept_batch_request` dispatching to `Coolhand::OpenAi::BatchResultProcessor` is an explicit, deliberate act, so it always sends.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
- Add the `openai` gem to your Gemfile — `Coolhand::OpenAi::BatchResultProcessor` needs it to download batch result files.
|
|
12
|
+
- Include the interceptor in your controller: `include Coolhand::WebhookInterceptor`
|
|
13
|
+
- Add the `before_action` to validate and populate the `@validator` payload: `before_action :intercept_batch_request, only: :openai`
|
|
14
|
+
- Ensure you skip CSRF for the webhook endpoint: `skip_before_action :verify_authenticity_token`
|
|
15
|
+
- Override the `webhook_secret` method to return your OpenAI webhook secret
|
|
16
|
+
|
|
17
|
+
`intercept_batch_request` already validates the webhook and, for `batch.completed`/`batch.failed`/`batch.expired`/`batch.cancelled` events, calls `Coolhand::OpenAi::BatchResultProcessor` **synchronously**, inline in the `before_action` — that part requires no code in your controller action. It downloads two JSONL files from OpenAI and sends one request log per batch item, so a large batch can take long enough to trip your webhook endpoint's timeout (and OpenAI's retry behavior); consider that when sizing batches. The example below shows a controller action doing additional, app-specific work on top of that (e.g. updating your own `BatchApiRequest` record and enqueuing your own background job), which is optional.
|
|
18
|
+
|
|
19
|
+
## Minimal example
|
|
20
|
+
|
|
21
|
+
Only the key lines are shown — wire this into your own controller and background job setup.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# app/controllers/webhooks/batch_api_requests_controller.rb
|
|
25
|
+
# ...existing code...
|
|
26
|
+
include Coolhand::WebhookInterceptor
|
|
27
|
+
|
|
28
|
+
skip_before_action :verify_authenticity_token
|
|
29
|
+
before_action :intercept_batch_request, only: :openai
|
|
30
|
+
|
|
31
|
+
def openai
|
|
32
|
+
event = JSON.parse(@validator.payload)
|
|
33
|
+
case event["type"]
|
|
34
|
+
when "batch.completed", "batch.failed", "batch.expired", "batch.cancelled"
|
|
35
|
+
batch_id = event.dig("data", "id")
|
|
36
|
+
batch_request = BatchApiRequest.find_by(provider: "openai", provider_batch_id: batch_id)
|
|
37
|
+
|
|
38
|
+
if batch_request
|
|
39
|
+
MyApp::BatchResultJob.perform_async(batch_request.id)
|
|
40
|
+
Rails.logger.info("Queued batch result processing for BatchApiRequest #{batch_request.id}")
|
|
41
|
+
else
|
|
42
|
+
Rails.logger.warn("Could not find BatchApiRequest for OpenAI batch ID: #{batch_id}")
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
Rails.logger.info("Unhandled OpenAI webhook event type: #{event["type"]}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
head :ok
|
|
49
|
+
rescue JSON::ParserError
|
|
50
|
+
head :bad_request
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
Rails.logger.error("OpenAI webhook error: #{e.message}")
|
|
53
|
+
head :internal_server_error
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def webhook_secret
|
|
57
|
+
Rails.application.credentials.openai_webhook_secret
|
|
58
|
+
end
|
|
59
|
+
# ...existing code...
|
|
60
|
+
```
|