payloop 0.0.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +21 -0
- data/README.md +29 -0
- data/lib/payloop/api/base.rb +104 -0
- data/lib/payloop/api/invocation.rb +61 -0
- data/lib/payloop/api/workflow.rb +34 -0
- data/lib/payloop/api/workflows.rb +39 -0
- data/lib/payloop/attribution.rb +64 -0
- data/lib/payloop/client.rb +76 -0
- data/lib/payloop/collector.rb +88 -0
- data/lib/payloop/config.rb +40 -0
- data/lib/payloop/errors.rb +30 -0
- data/lib/payloop/version.rb +5 -0
- data/lib/payloop/wrappers/anthropic.rb +95 -0
- data/lib/payloop/wrappers/base.rb +161 -0
- data/lib/payloop/wrappers/google.rb +96 -0
- data/lib/payloop/wrappers/openai.rb +78 -0
- data/lib/payloop.rb +47 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8afd4c12089fbda44abae722ada80ee75413b18d043aa7c8b5ab23b4c77e15c8
|
4
|
+
data.tar.gz: 7b9b9bc476434ef3c4b83dde77cde3ec0a506627324b1275decbdce16ba5ee42
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a07580586b5a649a60c6eb1dd7823de6f33af0243379b11517225db7546b8b87cd8ad3bea690ce626628d3117f78aecc2faf46dbf3e3a51378234d2045409d28
|
7
|
+
data.tar.gz: ca34c48b13d65ac23d9b65a0217bc66d0b0e7b548c6395c7f3f2e9da805d4d242c3f9590b2f0650639d2cacf74144368f951e74bd1af98a4eb35f3146a0da165
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [0.0.1] - 2025-10-13
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Initial release of Payloop Ruby SDK
|
12
|
+
- Support for OpenAI client tracking
|
13
|
+
- Support for Anthropic client tracking
|
14
|
+
- Support for Google GenAI client tracking
|
15
|
+
- Attribution tracking (parent/subsidiary hierarchy)
|
16
|
+
- Transaction management
|
17
|
+
- Asynchronous analytics collection
|
18
|
+
- Workflow API client
|
19
|
+
- Invocation API client
|
20
|
+
- Streaming response support
|
21
|
+
- Thread-safe configuration
|
22
|
+
- Comprehensive error handling
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 PayloopAI
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Welcome to Payloop!
|
2
|
+
|
3
|
+
### Cost Visibility for AI Agents
|
4
|
+
|
5
|
+
Payloop is a lightweight infrastructure layer that gives AI teams real-time visibility into the true costs of deploying agents - across tasks, workflows, and customers. Most teams today can’t see what it actually costs to deploy their agents, making it nearly impossible to manage gross margins or price with confidence.
|
6
|
+
|
7
|
+
With just a single line of code, Payloop delivers:
|
8
|
+
- Cost tracking across OpenAI, Anthropic, Gemini, and more in one place
|
9
|
+
- Breakdowns by task, agent, and customer in real time
|
10
|
+
- Confidence to deploy the right pricing model (cost-plus, token-based, outcome-based, etc.) while preserving gross margins
|
11
|
+
|
12
|
+
Watch a short demo video: [Payloop Walkthrough Video](https://www.youtube.com/watch?v=Z-GkSl_7imY)
|
13
|
+
|
14
|
+
By surfacing exactly what’s driving cost and value, Payloop becomes the source of truth for agent economics - helping founders and operators scale their agents with confidence.
|
15
|
+
|
16
|
+
Sign up here: [trypayloop.com](https://trypayloop.com/)
|
17
|
+
|
18
|
+
# Installation
|
19
|
+
|
20
|
+
gem install payloop
|
21
|
+
|
22
|
+
# Documentation
|
23
|
+
|
24
|
+
Our SDK reference provides documentation for:
|
25
|
+
- Understanding how Payloop works
|
26
|
+
- Demonstrating how to integrate Payloop into your product
|
27
|
+
- Code samples to illustrate our SDK and API
|
28
|
+
|
29
|
+
Check it out here: [developers.trypayloop.com](https://developers.trypayloop.com)
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
module Payloop
|
8
|
+
module API
|
9
|
+
# Base HTTP client for Payloop API
|
10
|
+
class Base
|
11
|
+
def initialize(api_url, api_key, timeout)
|
12
|
+
@original_api_url = api_url
|
13
|
+
@api_url = "#{api_url}/v1/-"
|
14
|
+
@api_key = api_key
|
15
|
+
@timeout = timeout
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def get(path)
|
21
|
+
request(:get, path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def post(path, body = nil)
|
25
|
+
request(:post, path, body)
|
26
|
+
end
|
27
|
+
|
28
|
+
def put(path, body = nil)
|
29
|
+
request(:put, path, body)
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete(path)
|
33
|
+
request(:delete, path)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def request(method, path, body = nil)
|
39
|
+
# Construct full URL - path should start with /
|
40
|
+
full_url = "#{@api_url}#{path}"
|
41
|
+
uri = URI.parse(full_url)
|
42
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
43
|
+
http.use_ssl = uri.scheme == "https"
|
44
|
+
|
45
|
+
# Configure SSL to use system certificates
|
46
|
+
if http.use_ssl?
|
47
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
48
|
+
http.cert_store = OpenSSL::X509::Store.new
|
49
|
+
http.cert_store.set_default_paths
|
50
|
+
end
|
51
|
+
|
52
|
+
http.open_timeout = @timeout
|
53
|
+
http.read_timeout = @timeout
|
54
|
+
|
55
|
+
request = build_request(method, uri, body)
|
56
|
+
response = http.request(request)
|
57
|
+
|
58
|
+
handle_response(response)
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_request(method, uri, body)
|
62
|
+
request_class = case method
|
63
|
+
when :get then Net::HTTP::Get
|
64
|
+
when :post then Net::HTTP::Post
|
65
|
+
when :put then Net::HTTP::Put
|
66
|
+
when :delete then Net::HTTP::Delete
|
67
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
68
|
+
end
|
69
|
+
|
70
|
+
request = request_class.new(uri.path.empty? ? "/" : uri.path)
|
71
|
+
request["Authorization"] = "Bearer #{@api_key}"
|
72
|
+
request["Content-Type"] = "application/json"
|
73
|
+
request["User-Agent"] = "payloop-ruby/#{Payloop::VERSION}"
|
74
|
+
|
75
|
+
request.body = JSON.generate(body) if body && %i[post put].include?(method)
|
76
|
+
|
77
|
+
request
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_response(response)
|
81
|
+
case response
|
82
|
+
when Net::HTTPSuccess
|
83
|
+
parse_json_response(response.body)
|
84
|
+
when Net::HTTPNoContent
|
85
|
+
{}
|
86
|
+
else
|
87
|
+
raise APIError.new(
|
88
|
+
"API request failed: #{response.code} #{response.message}",
|
89
|
+
status_code: response.code.to_i,
|
90
|
+
response_body: response.body
|
91
|
+
)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse_json_response(body)
|
96
|
+
return {} if body.nil? || body.empty?
|
97
|
+
|
98
|
+
JSON.parse(body)
|
99
|
+
rescue JSON::ParserError => e
|
100
|
+
raise APIError, "Failed to parse JSON response: #{e.message}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
|
5
|
+
module Payloop
|
6
|
+
module API
|
7
|
+
# API client for workflow invocation operations
|
8
|
+
class Invocation < Base
|
9
|
+
def initialize(api_url, api_key, timeout)
|
10
|
+
super(api_url, api_key, timeout)
|
11
|
+
@attribution = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Set attribution filter for summary queries
|
15
|
+
# Returns self for method chaining
|
16
|
+
def attribution(parent_id:, parent_name: nil, subsidiary_id: nil, subsidiary_name: nil)
|
17
|
+
@attribution = {
|
18
|
+
parent: { id: parent_id }
|
19
|
+
}
|
20
|
+
@attribution[:parent][:name] = parent_name if parent_name
|
21
|
+
|
22
|
+
if subsidiary_id
|
23
|
+
@attribution[:subsidiary] = { id: subsidiary_id }
|
24
|
+
@attribution[:subsidiary][:name] = subsidiary_name if subsidiary_name
|
25
|
+
end
|
26
|
+
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Get invocation summary for a workflow
|
31
|
+
def summary(workflow_uuid, date_start:, date_end: nil)
|
32
|
+
body = {
|
33
|
+
date: {
|
34
|
+
start: format_date(date_start)
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
if date_end
|
39
|
+
body[:date][:end] = format_date(date_end)
|
40
|
+
end
|
41
|
+
|
42
|
+
body[:attribution] = @attribution if @attribution
|
43
|
+
|
44
|
+
post("/workflow/#{workflow_uuid}/invocation/summary", body)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def format_date(date)
|
50
|
+
return date if date.is_a?(String)
|
51
|
+
|
52
|
+
# Convert to midnight (beginning of day) in ISO8601 format
|
53
|
+
if date.respond_to?(:to_date)
|
54
|
+
date.to_date.to_time.iso8601
|
55
|
+
else
|
56
|
+
date.iso8601
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Payloop
|
4
|
+
module API
|
5
|
+
# API client for managing a specific workflow
|
6
|
+
class Workflow < Base
|
7
|
+
def initialize(uuid, api_url, api_key, timeout)
|
8
|
+
super(api_url, api_key, timeout)
|
9
|
+
@uuid = uuid
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get workflow details
|
13
|
+
def details
|
14
|
+
get("/workflow/#{@uuid}")
|
15
|
+
end
|
16
|
+
|
17
|
+
# Update workflow label
|
18
|
+
def update(label:)
|
19
|
+
body = { label: label }
|
20
|
+
put("/workflow/#{@uuid}", body)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Delete workflow
|
24
|
+
def destroy
|
25
|
+
delete("/workflow/#{@uuid}")
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get invocation client for this workflow
|
29
|
+
def invocation
|
30
|
+
Invocation.new(@uuid, @original_api_url, @api_key, @timeout)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Payloop
|
4
|
+
module API
|
5
|
+
# API client for managing workflows
|
6
|
+
class Workflows < Base
|
7
|
+
# List all workflows
|
8
|
+
def list
|
9
|
+
get("/workflows")
|
10
|
+
end
|
11
|
+
|
12
|
+
# Create a new workflow
|
13
|
+
def create(name:, description: nil)
|
14
|
+
post("/workflows", { name: name, description: description }.compact)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get workflow details
|
18
|
+
def details(uuid)
|
19
|
+
get("/workflow/#{uuid}")
|
20
|
+
end
|
21
|
+
|
22
|
+
# Update workflow label
|
23
|
+
def update(uuid, label:)
|
24
|
+
body = { label: label }
|
25
|
+
put("/workflow/#{uuid}", body)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Delete workflow
|
29
|
+
def destroy(uuid)
|
30
|
+
delete("/workflow/#{uuid}")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get invocation manager for workflow operations
|
34
|
+
def invocation
|
35
|
+
@invocation ||= Invocation.new(@original_api_url, @api_key, @timeout)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Payloop
|
4
|
+
# Attribution tracks cost hierarchy for API calls
|
5
|
+
class Attribution
|
6
|
+
attr_reader :parent_id, :parent_name, :subsidiary_id, :subsidiary_name
|
7
|
+
|
8
|
+
def initialize(parent_id:, parent_name: nil, subsidiary_id: nil, subsidiary_name: nil)
|
9
|
+
@parent_id = validate_parent_id!(parent_id)
|
10
|
+
@parent_name = validate_string_length!(parent_name, "parent_name") if parent_name
|
11
|
+
@subsidiary_id = validate_string_length!(subsidiary_id, "subsidiary_id") if subsidiary_id
|
12
|
+
@subsidiary_name = validate_string_length!(subsidiary_name, "subsidiary_name") if subsidiary_name
|
13
|
+
|
14
|
+
validate_subsidiary_requirements!
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_h
|
18
|
+
result = {
|
19
|
+
parent: {
|
20
|
+
id: parent_id
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
result[:parent][:name] = parent_name if parent_name
|
25
|
+
|
26
|
+
if subsidiary_id
|
27
|
+
result[:subsidiary] = { id: subsidiary_id }
|
28
|
+
result[:subsidiary][:name] = subsidiary_name if subsidiary_name
|
29
|
+
end
|
30
|
+
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def validate_parent_id!(value)
|
37
|
+
raise ValidationError, "parent_id is required" if value.nil? || value.to_s.empty?
|
38
|
+
|
39
|
+
value_str = value.to_s
|
40
|
+
if value_str.length > 100
|
41
|
+
raise ValidationError, "parent_id cannot exceed 100 characters (got #{value_str.length})"
|
42
|
+
end
|
43
|
+
|
44
|
+
value_str
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_string_length!(value, field_name)
|
48
|
+
return nil if value.nil?
|
49
|
+
|
50
|
+
value_str = value.to_s
|
51
|
+
if value_str.length > 100
|
52
|
+
raise ValidationError, "#{field_name} cannot exceed 100 characters (got #{value_str.length})"
|
53
|
+
end
|
54
|
+
|
55
|
+
value_str
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate_subsidiary_requirements!
|
59
|
+
return unless subsidiary_name && !subsidiary_id
|
60
|
+
|
61
|
+
raise ValidationError, "subsidiary_id is required when subsidiary_name is provided"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module Payloop
|
6
|
+
# Main Payloop client for tracking AI costs
|
7
|
+
class Client
|
8
|
+
attr_reader :config, :collector
|
9
|
+
|
10
|
+
def initialize(api_key: nil, collector_url: nil, api_url: nil, timeout: nil)
|
11
|
+
api_key ||= ENV.fetch("PAYLOOP_API_KEY", nil)
|
12
|
+
raise MissingAPIKeyError if api_key.nil? || api_key.empty?
|
13
|
+
|
14
|
+
@config = Config.new(
|
15
|
+
api_key: api_key,
|
16
|
+
collector_url: collector_url,
|
17
|
+
api_url: api_url,
|
18
|
+
timeout: timeout
|
19
|
+
)
|
20
|
+
@collector = Collector.new(@config)
|
21
|
+
end
|
22
|
+
|
23
|
+
# OpenAI provider wrapper
|
24
|
+
def openai
|
25
|
+
@openai ||= Wrappers::OpenAI.new(@config, @collector)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Anthropic provider wrapper
|
29
|
+
def anthropic
|
30
|
+
@anthropic ||= Wrappers::Anthropic.new(@config, @collector)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Google GenAI provider wrapper
|
34
|
+
def google
|
35
|
+
@google ||= Wrappers::Google.new(@config, @collector)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set attribution for cost tracking
|
39
|
+
def attribution(parent_id:, parent_name: nil, subsidiary_id: nil, subsidiary_name: nil,
|
40
|
+
parent_uuid: nil, subsidiary_uuid: nil)
|
41
|
+
# Support deprecated parameters
|
42
|
+
parent_id ||= parent_uuid
|
43
|
+
subsidiary_id ||= subsidiary_uuid
|
44
|
+
|
45
|
+
attr = Attribution.new(
|
46
|
+
parent_id: parent_id,
|
47
|
+
parent_name: parent_name,
|
48
|
+
subsidiary_id: subsidiary_id,
|
49
|
+
subsidiary_name: subsidiary_name
|
50
|
+
)
|
51
|
+
|
52
|
+
@config.attribution = attr
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# Start a new transaction
|
57
|
+
def new_transaction
|
58
|
+
@config.new_transaction
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get workflows API client
|
63
|
+
def workflows
|
64
|
+
@workflows ||= API::Workflows.new(
|
65
|
+
@config.api_url,
|
66
|
+
@config.api_key,
|
67
|
+
@config.timeout
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Gracefully shutdown the client
|
72
|
+
def shutdown
|
73
|
+
@collector.shutdown
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
require "uri"
|
6
|
+
require "concurrent"
|
7
|
+
|
8
|
+
module Payloop
|
9
|
+
# Handles asynchronous submission of analytics to Payloop collector
|
10
|
+
class Collector
|
11
|
+
DEFAULT_MAX_RETRIES = 3
|
12
|
+
RETRY_DELAY = 1 # seconds
|
13
|
+
|
14
|
+
def initialize(config)
|
15
|
+
@config = config
|
16
|
+
@executor = Concurrent::ThreadPoolExecutor.new(
|
17
|
+
min_threads: 1,
|
18
|
+
max_threads: 5,
|
19
|
+
max_queue: 100,
|
20
|
+
fallback_policy: :discard
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Submit analytics payload asynchronously
|
25
|
+
def submit_async(payload)
|
26
|
+
@executor.post do
|
27
|
+
submit_with_retry(payload)
|
28
|
+
rescue StandardError => e
|
29
|
+
warn "Payloop: Failed to submit analytics: #{e.message}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Submit analytics payload synchronously (for testing)
|
34
|
+
def submit(payload)
|
35
|
+
submit_with_retry(payload)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Gracefully shutdown the collector
|
39
|
+
def shutdown
|
40
|
+
@executor.shutdown
|
41
|
+
@executor.wait_for_termination(5)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def submit_with_retry(payload, attempt = 1)
|
47
|
+
response = post_to_collector(payload)
|
48
|
+
|
49
|
+
unless response.is_a?(Net::HTTPSuccess)
|
50
|
+
raise APIError.new(
|
51
|
+
"Collector request failed: #{response.code} #{response.message}",
|
52
|
+
status_code: response.code.to_i,
|
53
|
+
response_body: response.body
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
response
|
58
|
+
rescue StandardError => e
|
59
|
+
raise e unless attempt < DEFAULT_MAX_RETRIES
|
60
|
+
|
61
|
+
sleep(RETRY_DELAY * attempt)
|
62
|
+
submit_with_retry(payload, attempt + 1)
|
63
|
+
end
|
64
|
+
|
65
|
+
def post_to_collector(payload)
|
66
|
+
uri = URI.parse(@config.collector_url)
|
67
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
68
|
+
http.use_ssl = uri.scheme == "https"
|
69
|
+
|
70
|
+
# Configure SSL to use system certificates
|
71
|
+
if http.use_ssl?
|
72
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
73
|
+
http.cert_store = OpenSSL::X509::Store.new
|
74
|
+
http.cert_store.set_default_paths
|
75
|
+
end
|
76
|
+
|
77
|
+
http.open_timeout = @config.timeout
|
78
|
+
http.read_timeout = @config.timeout
|
79
|
+
|
80
|
+
request = Net::HTTP::Post.new("/rec")
|
81
|
+
request["Content-Type"] = "application/json"
|
82
|
+
request["User-Agent"] = "payloop-ruby/#{@config.version}"
|
83
|
+
request.body = JSON.generate(payload)
|
84
|
+
|
85
|
+
http.request(request)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
|
5
|
+
module Payloop
|
6
|
+
# Thread-safe configuration for Payloop client
|
7
|
+
class Config
|
8
|
+
attr_reader :api_key, :collector_url, :api_url, :timeout, :version
|
9
|
+
|
10
|
+
def initialize(api_key: nil, collector_url: nil, api_url: nil, timeout: nil)
|
11
|
+
@api_key = api_key
|
12
|
+
@collector_url = collector_url || "https://collector.trypayloop.com"
|
13
|
+
@api_url = api_url || "https://api.trypayloop.com"
|
14
|
+
@timeout = timeout || 5
|
15
|
+
@version = Payloop::VERSION
|
16
|
+
@attribution = Concurrent::AtomicReference.new(nil)
|
17
|
+
@tx_uuid = Concurrent::AtomicReference.new(SecureRandom.uuid)
|
18
|
+
end
|
19
|
+
|
20
|
+
def attribution
|
21
|
+
@attribution.get
|
22
|
+
end
|
23
|
+
|
24
|
+
def attribution=(value)
|
25
|
+
@attribution.set(value)
|
26
|
+
end
|
27
|
+
|
28
|
+
def tx_uuid
|
29
|
+
@tx_uuid.get
|
30
|
+
end
|
31
|
+
|
32
|
+
def tx_uuid=(value)
|
33
|
+
@tx_uuid.set(value)
|
34
|
+
end
|
35
|
+
|
36
|
+
def new_transaction
|
37
|
+
@tx_uuid.set(SecureRandom.uuid)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Payloop
|
4
|
+
# Base error class for all Payloop errors
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Raised when API key is missing
|
8
|
+
class MissingAPIKeyError < Error
|
9
|
+
def initialize(msg = "API key is missing. Set PAYLOOP_API_KEY environment variable or pass api_key parameter.")
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Raised when validation fails
|
15
|
+
class ValidationError < Error; end
|
16
|
+
|
17
|
+
# Raised when client registration fails
|
18
|
+
class RegistrationError < Error; end
|
19
|
+
|
20
|
+
# Raised when API request fails
|
21
|
+
class APIError < Error
|
22
|
+
attr_reader :status_code, :response_body
|
23
|
+
|
24
|
+
def initialize(message, status_code: nil, response_body: nil)
|
25
|
+
@status_code = status_code
|
26
|
+
@response_body = response_body
|
27
|
+
super(message)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Payloop
|
4
|
+
module Wrappers
|
5
|
+
# Wrapper for Anthropic Ruby client
|
6
|
+
class Anthropic
|
7
|
+
def initialize(config, collector)
|
8
|
+
@config = config
|
9
|
+
@collector = collector
|
10
|
+
end
|
11
|
+
|
12
|
+
def register(client)
|
13
|
+
validate_client!(client)
|
14
|
+
|
15
|
+
# Prevent double registration
|
16
|
+
return client if client.instance_variable_defined?(:@payloop_registered)
|
17
|
+
|
18
|
+
# Store references in client instance
|
19
|
+
client.instance_variable_set(:@payloop_config, @config)
|
20
|
+
client.instance_variable_set(:@payloop_collector, @collector)
|
21
|
+
client.instance_variable_set(:@payloop_registered, true)
|
22
|
+
|
23
|
+
# Wrap the messages method
|
24
|
+
wrap_messages_method(client)
|
25
|
+
|
26
|
+
client
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate_client!(client)
|
32
|
+
return if client.respond_to?(:messages)
|
33
|
+
|
34
|
+
raise RegistrationError, "Client does not appear to be a valid Anthropic client (missing messages method)"
|
35
|
+
end
|
36
|
+
|
37
|
+
def wrap_messages_method(client)
|
38
|
+
# Get the messages resource
|
39
|
+
messages_resource = client.messages
|
40
|
+
|
41
|
+
# Store references on the messages resource (needed for Base module)
|
42
|
+
messages_resource.instance_variable_set(:@payloop_config, client.instance_variable_get(:@payloop_config))
|
43
|
+
messages_resource.instance_variable_set(:@payloop_collector, client.instance_variable_get(:@payloop_collector))
|
44
|
+
|
45
|
+
# Store the original create method
|
46
|
+
original_create = messages_resource.method(:create)
|
47
|
+
|
48
|
+
# Wrap the create method
|
49
|
+
messages_resource.define_singleton_method(:create) do |*args, **kwargs, &block|
|
50
|
+
# Include Base module methods
|
51
|
+
extend Base unless singleton_class.include?(Base)
|
52
|
+
|
53
|
+
start_time = Time.now
|
54
|
+
|
55
|
+
# Call original method
|
56
|
+
response = if kwargs.any?
|
57
|
+
original_create.call(**kwargs, &block)
|
58
|
+
else
|
59
|
+
original_create.call(*args, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Extract parameters for analytics
|
63
|
+
params = kwargs.any? ? kwargs : (args.first || {})
|
64
|
+
|
65
|
+
# Submit analytics
|
66
|
+
payloop_submit_analytics(
|
67
|
+
provider: "anthropic",
|
68
|
+
method: :create,
|
69
|
+
args: args,
|
70
|
+
kwargs: params,
|
71
|
+
response: response,
|
72
|
+
start_time: start_time,
|
73
|
+
end_time: Time.now
|
74
|
+
)
|
75
|
+
|
76
|
+
response
|
77
|
+
rescue StandardError => e
|
78
|
+
params = kwargs.any? ? kwargs : (args.first || {})
|
79
|
+
|
80
|
+
payloop_submit_error_analytics(
|
81
|
+
provider: "anthropic",
|
82
|
+
method: :create,
|
83
|
+
args: args,
|
84
|
+
kwargs: params,
|
85
|
+
error: e,
|
86
|
+
start_time: start_time,
|
87
|
+
end_time: Time.now
|
88
|
+
)
|
89
|
+
|
90
|
+
raise e
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
|
5
|
+
module Payloop
|
6
|
+
module Wrappers
|
7
|
+
# Base functionality for all provider wrappers
|
8
|
+
module Base
|
9
|
+
def payloop_wrap_method(method_name, provider_name)
|
10
|
+
return if method(method_name).source_location&.first&.include?("payloop")
|
11
|
+
|
12
|
+
original_method = instance_method(method_name)
|
13
|
+
|
14
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
15
|
+
start_time = Time.now
|
16
|
+
|
17
|
+
# Call original method
|
18
|
+
response = original_method.bind(self).call(*args, **kwargs, &block)
|
19
|
+
|
20
|
+
# Submit analytics asynchronously
|
21
|
+
payloop_submit_analytics(
|
22
|
+
provider: provider_name,
|
23
|
+
method: method_name,
|
24
|
+
args: args,
|
25
|
+
kwargs: kwargs,
|
26
|
+
response: response,
|
27
|
+
start_time: start_time,
|
28
|
+
end_time: Time.now
|
29
|
+
)
|
30
|
+
|
31
|
+
response
|
32
|
+
rescue StandardError => e
|
33
|
+
# Submit error analytics
|
34
|
+
payloop_submit_error_analytics(
|
35
|
+
provider: provider_name,
|
36
|
+
method: method_name,
|
37
|
+
args: args,
|
38
|
+
kwargs: kwargs,
|
39
|
+
error: e,
|
40
|
+
start_time: start_time,
|
41
|
+
end_time: Time.now
|
42
|
+
)
|
43
|
+
|
44
|
+
raise e
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def payloop_submit_analytics(provider:, method:, args:, kwargs:, response:, start_time:, end_time:)
|
49
|
+
collector = instance_variable_get(:@payloop_collector)
|
50
|
+
config = instance_variable_get(:@payloop_config)
|
51
|
+
|
52
|
+
return unless collector && config
|
53
|
+
|
54
|
+
payload = build_payload(
|
55
|
+
provider: provider,
|
56
|
+
query: extract_query(method, args, kwargs),
|
57
|
+
response: extract_response(response),
|
58
|
+
start_time: start_time,
|
59
|
+
end_time: end_time,
|
60
|
+
config: config,
|
61
|
+
status: "succeeded"
|
62
|
+
)
|
63
|
+
|
64
|
+
collector.submit_async(payload)
|
65
|
+
end
|
66
|
+
|
67
|
+
def payloop_submit_error_analytics(provider:, method:, args:, kwargs:, error:, start_time:, end_time:)
|
68
|
+
collector = instance_variable_get(:@payloop_collector)
|
69
|
+
config = instance_variable_get(:@payloop_config)
|
70
|
+
|
71
|
+
return unless collector && config
|
72
|
+
|
73
|
+
payload = build_payload(
|
74
|
+
provider: provider,
|
75
|
+
query: extract_query(method, args, kwargs),
|
76
|
+
response: { error: error.message, class: error.class.name },
|
77
|
+
start_time: start_time,
|
78
|
+
end_time: end_time,
|
79
|
+
config: config,
|
80
|
+
status: "failed",
|
81
|
+
exception: error.message
|
82
|
+
)
|
83
|
+
|
84
|
+
collector.submit_async(payload)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def extract_query(_method, _args, kwargs)
|
90
|
+
# Deep copy to avoid mutation issues
|
91
|
+
deep_copy(kwargs)
|
92
|
+
rescue StandardError
|
93
|
+
{}
|
94
|
+
end
|
95
|
+
|
96
|
+
def extract_response(response)
|
97
|
+
case response
|
98
|
+
when Hash
|
99
|
+
deep_copy(response)
|
100
|
+
when String
|
101
|
+
{ text: response }
|
102
|
+
else
|
103
|
+
if response.respond_to?(:to_h)
|
104
|
+
deep_copy(response.to_h)
|
105
|
+
elsif response.respond_to?(:to_hash)
|
106
|
+
deep_copy(response.to_hash)
|
107
|
+
else
|
108
|
+
{ raw: response.to_s }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
rescue StandardError
|
112
|
+
{ raw: response.to_s }
|
113
|
+
end
|
114
|
+
|
115
|
+
def deep_copy(obj)
|
116
|
+
Marshal.load(Marshal.dump(obj))
|
117
|
+
rescue StandardError
|
118
|
+
begin
|
119
|
+
obj.dup
|
120
|
+
rescue StandardError
|
121
|
+
obj
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def build_payload(provider:, query:, response:, start_time:, end_time:, config:, status:, exception: nil)
|
126
|
+
{
|
127
|
+
attribution: config.attribution&.to_h,
|
128
|
+
conversation: {
|
129
|
+
client: {
|
130
|
+
provider: provider,
|
131
|
+
title: provider,
|
132
|
+
version: nil
|
133
|
+
},
|
134
|
+
query: query,
|
135
|
+
response: response
|
136
|
+
},
|
137
|
+
meta: {
|
138
|
+
api: {
|
139
|
+
key: config.api_key
|
140
|
+
},
|
141
|
+
fnfg: {
|
142
|
+
status: status,
|
143
|
+
exc: exception
|
144
|
+
},
|
145
|
+
sdk: {
|
146
|
+
client: "ruby",
|
147
|
+
version: config.version
|
148
|
+
}
|
149
|
+
},
|
150
|
+
time: {
|
151
|
+
start: start_time.iso8601(3),
|
152
|
+
end: end_time.iso8601(3)
|
153
|
+
},
|
154
|
+
tx: {
|
155
|
+
uuid: config.tx_uuid
|
156
|
+
}
|
157
|
+
}
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Payloop
|
4
|
+
module Wrappers
|
5
|
+
# Wrapper for Google GenerativeAI Ruby client
|
6
|
+
class Google
|
7
|
+
def initialize(config, collector)
|
8
|
+
@config = config
|
9
|
+
@collector = collector
|
10
|
+
end
|
11
|
+
|
12
|
+
def register(client)
|
13
|
+
validate_client!(client)
|
14
|
+
|
15
|
+
# Prevent double registration
|
16
|
+
return client if client.instance_variable_defined?(:@payloop_registered)
|
17
|
+
|
18
|
+
# Store references in client instance
|
19
|
+
client.instance_variable_set(:@payloop_config, @config)
|
20
|
+
client.instance_variable_set(:@payloop_collector, @collector)
|
21
|
+
client.instance_variable_set(:@payloop_registered, true)
|
22
|
+
|
23
|
+
# Wrap the generate_content method
|
24
|
+
wrap_generate_content_method(client)
|
25
|
+
|
26
|
+
client
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate_client!(client)
|
32
|
+
return if client.respond_to?(:models)
|
33
|
+
|
34
|
+
raise RegistrationError,
|
35
|
+
"Client does not appear to be a valid Google GenAI client (missing models method)"
|
36
|
+
end
|
37
|
+
|
38
|
+
def wrap_generate_content_method(client)
|
39
|
+
# Get the models resource
|
40
|
+
models_resource = client.models
|
41
|
+
|
42
|
+
# Store references on the models resource (needed for Base module)
|
43
|
+
models_resource.instance_variable_set(:@payloop_config, client.instance_variable_get(:@payloop_config))
|
44
|
+
models_resource.instance_variable_set(:@payloop_collector, client.instance_variable_get(:@payloop_collector))
|
45
|
+
|
46
|
+
# Store the original generate_content method
|
47
|
+
original_generate_content = models_resource.method(:generate_content)
|
48
|
+
|
49
|
+
# Wrap the generate_content method
|
50
|
+
models_resource.define_singleton_method(:generate_content) do |*args, **kwargs, &block|
|
51
|
+
# Include Base module methods
|
52
|
+
extend Base unless singleton_class.include?(Base)
|
53
|
+
|
54
|
+
start_time = Time.now
|
55
|
+
|
56
|
+
# Call original method
|
57
|
+
response = if kwargs.any?
|
58
|
+
original_generate_content.call(**kwargs, &block)
|
59
|
+
else
|
60
|
+
original_generate_content.call(*args, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Extract parameters for analytics
|
64
|
+
params = kwargs.any? ? kwargs : (args.first || {})
|
65
|
+
|
66
|
+
# Submit analytics
|
67
|
+
payloop_submit_analytics(
|
68
|
+
provider: "google",
|
69
|
+
method: :generate_content,
|
70
|
+
args: args,
|
71
|
+
kwargs: params,
|
72
|
+
response: response,
|
73
|
+
start_time: start_time,
|
74
|
+
end_time: Time.now
|
75
|
+
)
|
76
|
+
|
77
|
+
response
|
78
|
+
rescue StandardError => e
|
79
|
+
params = kwargs.any? ? kwargs : (args.first || {})
|
80
|
+
|
81
|
+
payloop_submit_error_analytics(
|
82
|
+
provider: "google",
|
83
|
+
method: :generate_content,
|
84
|
+
args: args,
|
85
|
+
kwargs: params,
|
86
|
+
error: e,
|
87
|
+
start_time: start_time,
|
88
|
+
end_time: Time.now
|
89
|
+
)
|
90
|
+
|
91
|
+
raise e
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Payloop
|
4
|
+
module Wrappers
|
5
|
+
# Wrapper for OpenAI Ruby client (ruby-openai gem)
|
6
|
+
class OpenAI
|
7
|
+
def initialize(config, collector)
|
8
|
+
@config = config
|
9
|
+
@collector = collector
|
10
|
+
end
|
11
|
+
|
12
|
+
def register(client)
|
13
|
+
validate_client!(client)
|
14
|
+
|
15
|
+
# Prevent double registration
|
16
|
+
return client if client.instance_variable_defined?(:@payloop_registered)
|
17
|
+
|
18
|
+
# Store references in client instance
|
19
|
+
client.instance_variable_set(:@payloop_config, @config)
|
20
|
+
client.instance_variable_set(:@payloop_collector, @collector)
|
21
|
+
client.instance_variable_set(:@payloop_registered, true)
|
22
|
+
|
23
|
+
# Wrap the chat method
|
24
|
+
wrap_chat_method(client)
|
25
|
+
|
26
|
+
client
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate_client!(client)
|
32
|
+
return if client.respond_to?(:chat)
|
33
|
+
|
34
|
+
raise RegistrationError, "Client does not appear to be a valid OpenAI client (missing chat method)"
|
35
|
+
end
|
36
|
+
|
37
|
+
def wrap_chat_method(client)
|
38
|
+
client.singleton_class.class_eval do
|
39
|
+
include Base
|
40
|
+
|
41
|
+
alias_method :original_chat, :chat
|
42
|
+
|
43
|
+
define_method(:chat) do |parameters: {}|
|
44
|
+
start_time = Time.now
|
45
|
+
|
46
|
+
# Call original method
|
47
|
+
response = original_chat(parameters: parameters)
|
48
|
+
|
49
|
+
# Submit analytics
|
50
|
+
payloop_submit_analytics(
|
51
|
+
provider: "openai",
|
52
|
+
method: :chat,
|
53
|
+
args: [],
|
54
|
+
kwargs: { parameters: parameters },
|
55
|
+
response: response,
|
56
|
+
start_time: start_time,
|
57
|
+
end_time: Time.now
|
58
|
+
)
|
59
|
+
|
60
|
+
response
|
61
|
+
rescue StandardError => e
|
62
|
+
payloop_submit_error_analytics(
|
63
|
+
provider: "openai",
|
64
|
+
method: :chat,
|
65
|
+
args: [],
|
66
|
+
kwargs: { parameters: parameters },
|
67
|
+
error: e,
|
68
|
+
start_time: start_time,
|
69
|
+
end_time: Time.now
|
70
|
+
)
|
71
|
+
|
72
|
+
raise e
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/payloop.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "payloop/version"
|
4
|
+
require_relative "payloop/errors"
|
5
|
+
require_relative "payloop/config"
|
6
|
+
require_relative "payloop/attribution"
|
7
|
+
require_relative "payloop/collector"
|
8
|
+
require_relative "payloop/wrappers/base"
|
9
|
+
require_relative "payloop/wrappers/openai"
|
10
|
+
require_relative "payloop/wrappers/anthropic"
|
11
|
+
require_relative "payloop/wrappers/google"
|
12
|
+
require_relative "payloop/api/base"
|
13
|
+
require_relative "payloop/api/workflows"
|
14
|
+
require_relative "payloop/api/invocation"
|
15
|
+
require_relative "payloop/api/workflow"
|
16
|
+
require_relative "payloop/client"
|
17
|
+
|
18
|
+
# Payloop - Cost visibility for AI agents
|
19
|
+
#
|
20
|
+
# Payloop is a lightweight infrastructure layer that gives AI teams
|
21
|
+
# real-time visibility into the true costs of deploying agents.
|
22
|
+
#
|
23
|
+
# @example Basic usage with OpenAI
|
24
|
+
# require 'openai'
|
25
|
+
# require 'payloop'
|
26
|
+
#
|
27
|
+
# openai = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
|
28
|
+
# payloop = Payloop::Client.new(api_key: ENV['PAYLOOP_API_KEY'])
|
29
|
+
# payloop.openai.register(openai)
|
30
|
+
#
|
31
|
+
# # Use OpenAI normally - analytics tracked automatically
|
32
|
+
# response = openai.chat(
|
33
|
+
# parameters: {
|
34
|
+
# model: "gpt-4",
|
35
|
+
# messages: [{ role: "user", content: "Hello!" }]
|
36
|
+
# }
|
37
|
+
# )
|
38
|
+
#
|
39
|
+
# @example With attribution
|
40
|
+
# payloop.attribution(
|
41
|
+
# parent_id: "user-123",
|
42
|
+
# parent_name: "John Doe"
|
43
|
+
# )
|
44
|
+
#
|
45
|
+
# @see https://developers.trypayloop.com
|
46
|
+
module Payloop
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: payloop
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Payloop
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-10-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: concurrent-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.2'
|
27
|
+
description: Payloop gives AI teams real-time visibility into the true costs of deploying
|
28
|
+
agents - across tasks, workflows, and customers.
|
29
|
+
email:
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- CHANGELOG.md
|
35
|
+
- LICENSE
|
36
|
+
- README.md
|
37
|
+
- lib/payloop.rb
|
38
|
+
- lib/payloop/api/base.rb
|
39
|
+
- lib/payloop/api/invocation.rb
|
40
|
+
- lib/payloop/api/workflow.rb
|
41
|
+
- lib/payloop/api/workflows.rb
|
42
|
+
- lib/payloop/attribution.rb
|
43
|
+
- lib/payloop/client.rb
|
44
|
+
- lib/payloop/collector.rb
|
45
|
+
- lib/payloop/config.rb
|
46
|
+
- lib/payloop/errors.rb
|
47
|
+
- lib/payloop/version.rb
|
48
|
+
- lib/payloop/wrappers/anthropic.rb
|
49
|
+
- lib/payloop/wrappers/base.rb
|
50
|
+
- lib/payloop/wrappers/google.rb
|
51
|
+
- lib/payloop/wrappers/openai.rb
|
52
|
+
homepage: https://trypayloop.com
|
53
|
+
licenses:
|
54
|
+
- MIT
|
55
|
+
metadata:
|
56
|
+
homepage_uri: https://trypayloop.com
|
57
|
+
documentation_uri: https://developers.trypayloop.com
|
58
|
+
rubygems_mfa_required: 'true'
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 2.7.0
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubygems_version: 3.4.10
|
75
|
+
signing_key:
|
76
|
+
specification_version: 4
|
77
|
+
summary: Cost visibility for AI agents
|
78
|
+
test_files: []
|