prompt_warden 0.1.0 → 0.1.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.
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/prompt_warden/instrumentation/openai'
4
+
5
+ RSpec.describe 'PromptWarden OpenAI adapter' do
6
+ before do
7
+ require 'prompt_warden'
8
+ PromptWarden.configure { |c| c.project_token = 'tok' }
9
+ allow(PromptWarden).to receive(:record)
10
+ allow(PromptWarden::Policy.instance).to receive(:check_alerts).and_return([])
11
+ allow(PromptWarden).to receive(:calculate_cost).and_return(0.005)
12
+ end
13
+
14
+ it 'records prompt usage via instrumentation for chat method' do
15
+ client = Object.new
16
+ client.extend(PromptWarden::Instrumentation::OpenAI)
17
+ # Call the instrumentation method directly
18
+ client.send(:_pw_wrap, :chat, {
19
+ messages: [{ role: 'user', content: 'hi' }],
20
+ model: 'gpt-4o',
21
+ max_tokens: 1000
22
+ }) do
23
+ { 'choices' => [{ 'message' => { 'content' => 'openai reply' } }] }
24
+ end
25
+ expect(PromptWarden).to have_received(:record).with(hash_including(
26
+ prompt: 'hi',
27
+ model: 'gpt-4o',
28
+ cost_usd: 0.005,
29
+ status: 'ok',
30
+ alerts: []
31
+ ))
32
+ end
33
+
34
+ it 'records prompt usage via instrumentation for completions method' do
35
+ client = Object.new
36
+ client.extend(PromptWarden::Instrumentation::OpenAI)
37
+ # Call the instrumentation method directly
38
+ client.send(:_pw_wrap, :completions, {
39
+ prompt: 'hello',
40
+ model: 'gpt-3.5-turbo',
41
+ max_tokens: 500
42
+ }) do
43
+ { 'choices' => [{ 'message' => { 'content' => 'openai completion' } }] }
44
+ end
45
+ expect(PromptWarden).to have_received(:record).with(hash_including(
46
+ prompt: 'hello',
47
+ model: 'gpt-3.5-turbo',
48
+ cost_usd: 0.005,
49
+ status: 'ok',
50
+ alerts: []
51
+ ))
52
+ end
53
+
54
+ it 'includes alerts when policy alerts are detected' do
55
+ alerts = [{ type: 'regex', rule: '/confidential/i', level: 'warn' }]
56
+ allow(PromptWarden::Policy.instance).to receive(:check_alerts).and_return(alerts)
57
+ client = Object.new
58
+ client.extend(PromptWarden::Instrumentation::OpenAI)
59
+ # Call the instrumentation method directly
60
+ client.send(:_pw_wrap, :chat, {
61
+ messages: [{ role: 'user', content: 'This is confidential information' }],
62
+ model: 'gpt-4o'
63
+ }) do
64
+ { 'choices' => [{ 'message' => { 'content' => 'openai reply' } }] }
65
+ end
66
+ expect(PromptWarden).to have_received(:record).with(hash_including(
67
+ prompt: 'This is confidential information',
68
+ model: 'gpt-4o',
69
+ status: 'ok',
70
+ alerts: alerts
71
+ ))
72
+ end
73
+
74
+ it 'records failed status on error with alerts' do
75
+ alerts = [{ type: 'regex', rule: '/ETA/i', level: 'warn' }]
76
+ allow(PromptWarden::Policy.instance).to receive(:check_alerts).and_return(alerts)
77
+ client = Object.new
78
+ client.extend(PromptWarden::Instrumentation::OpenAI)
79
+ expect {
80
+ client.send(:_pw_wrap, :chat, {
81
+ messages: [{ role: 'user', content: 'What is the ETA?' }],
82
+ model: 'gpt-4o'
83
+ }) do
84
+ raise 'test error'
85
+ end
86
+ }.to raise_error('test error')
87
+ expect(PromptWarden).to have_received(:record).with(hash_including(
88
+ prompt: 'What is the ETA?',
89
+ model: 'gpt-4o',
90
+ status: 'failed',
91
+ alerts: alerts
92
+ ))
93
+ end
94
+
95
+ it 'extracts prompt from messages array' do
96
+ client = Object.new
97
+ client.extend(PromptWarden::Instrumentation::OpenAI)
98
+ # Call the instrumentation method directly
99
+ client.send(:_pw_wrap, :chat, {
100
+ messages: [
101
+ { role: 'user', content: 'Hello' },
102
+ { role: 'assistant', content: 'Hi there' },
103
+ { role: 'user', content: 'How are you?' }
104
+ ],
105
+ model: 'gpt-4o'
106
+ }) do
107
+ { 'choices' => [{ 'message' => { 'content' => 'openai reply' } }] }
108
+ end
109
+ expect(PromptWarden).to have_received(:record).with(hash_including(
110
+ prompt: "Hello\nHi there\nHow are you?",
111
+ model: 'gpt-4o',
112
+ status: 'ok',
113
+ alerts: []
114
+ ))
115
+ end
116
+
117
+ it 'uses enhanced cost calculation for estimates and actual costs' do
118
+ client = Object.new
119
+ client.extend(PromptWarden::Instrumentation::OpenAI)
120
+
121
+ # Mock cost calculation calls
122
+ allow(PromptWarden).to receive(:calculate_cost).with(
123
+ prompt: 'hi',
124
+ model: 'gpt-4o'
125
+ ).and_return(0.003) # Estimate
126
+
127
+ allow(PromptWarden).to receive(:calculate_cost).with(
128
+ prompt: 'hi',
129
+ model: 'gpt-4o',
130
+ response_tokens: 50
131
+ ).and_return(0.005) # Actual cost
132
+
133
+ client.send(:_pw_wrap, :chat, {
134
+ messages: [{ role: 'user', content: 'hi' }],
135
+ model: 'gpt-4o'
136
+ }) do
137
+ {
138
+ 'choices' => [{ 'message' => { 'content' => 'openai reply' } }],
139
+ 'usage' => { 'completion_tokens' => 50 }
140
+ }
141
+ end
142
+
143
+ expect(PromptWarden).to have_received(:calculate_cost).with(
144
+ prompt: 'hi',
145
+ model: 'gpt-4o'
146
+ )
147
+ expect(PromptWarden).to have_received(:calculate_cost).with(
148
+ prompt: 'hi',
149
+ model: 'gpt-4o',
150
+ response_tokens: 50
151
+ )
152
+ end
153
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tmpdir'
4
+ require 'yaml'
5
+
6
+ RSpec.describe PromptWarden::Policy do
7
+ let(:policy_path) { File.join(Dir.mktmpdir, 'policy.yml') }
8
+
9
+ def write_rules(rules)
10
+ File.write(policy_path, rules.to_yaml)
11
+ ENV['PROMPT_WARDEN_POLICY'] = policy_path
12
+ PromptWarden::Policy.instance.reload!
13
+ end
14
+
15
+ after { ENV.delete('PROMPT_WARDEN_POLICY') }
16
+
17
+ # -----------------------------------------------------------
18
+ it 'blocks when cost exceeds max_cost_usd' do
19
+ write_rules(max_cost_usd: 0.01)
20
+
21
+ policy = described_class.instance
22
+ expect do
23
+ policy.check!(prompt: 'hi', cost_estimate: 0.02)
24
+ end.to raise_error(PromptWarden::PolicyError, /exceeds/)
25
+ end
26
+
27
+ # -----------------------------------------------------------
28
+ it 'blocks when prompt matches reject regex' do
29
+ write_rules(reject_if_regex: ['/password/i'])
30
+
31
+ policy = described_class.instance
32
+ expect do
33
+ policy.check!(prompt: 'My password is 123', cost_estimate: 0)
34
+ end.to raise_error(PromptWarden::PolicyError, /reject regex/)
35
+ end
36
+
37
+ # -----------------------------------------------------------
38
+ it 'returns :ok for compliant prompt' do
39
+ write_rules(max_cost_usd: 1, reject_if_regex: [])
40
+
41
+ policy = described_class.instance
42
+ result = policy.check!(prompt: 'hello', cost_estimate: 0.5)
43
+ expect(result).to eq :ok
44
+ end
45
+
46
+ # -----------------------------------------------------------
47
+ describe '#check_alerts' do
48
+ it 'returns empty array when no alert rules configured' do
49
+ write_rules({})
50
+
51
+ policy = described_class.instance
52
+ alerts = policy.check_alerts(prompt: 'any text', cost_estimate: 0.1)
53
+ expect(alerts).to eq []
54
+ end
55
+
56
+ it 'returns empty array when prompt does not match warn regex' do
57
+ write_rules(warn_if_regex: ['/password/i'])
58
+
59
+ policy = described_class.instance
60
+ alerts = policy.check_alerts(prompt: 'hello world', cost_estimate: 0.1)
61
+ expect(alerts).to eq []
62
+ end
63
+
64
+ it 'returns alert when prompt matches warn regex' do
65
+ write_rules(warn_if_regex: ['/password/i'])
66
+
67
+ policy = described_class.instance
68
+ alerts = policy.check_alerts(prompt: 'My password is 123', cost_estimate: 0.1)
69
+ expect(alerts).to eq [{ type: 'regex', rule: '/password/i', level: 'warn' }]
70
+ end
71
+
72
+ it 'returns multiple alerts when prompt matches multiple warn regexes' do
73
+ write_rules(warn_if_regex: ['/password/i', '/ssn/i'])
74
+
75
+ policy = described_class.instance
76
+ alerts = policy.check_alerts(prompt: 'My password is 123 and SSN is 456', cost_estimate: 0.1)
77
+ expect(alerts).to contain_exactly(
78
+ { type: 'regex', rule: '/password/i', level: 'warn' },
79
+ { type: 'regex', rule: '/ssn/i', level: 'warn' }
80
+ )
81
+ end
82
+
83
+ it 'returns cost alert when cost exceeds limit' do
84
+ write_rules(max_cost_usd: 0.05)
85
+
86
+ policy = described_class.instance
87
+ alerts = policy.check_alerts(prompt: 'hello', cost_estimate: 0.1)
88
+ expect(alerts).to eq [{ type: 'cost', limit: 0.05, level: 'block' }]
89
+ end
90
+
91
+ it 'returns both regex and cost alerts when both conditions are met' do
92
+ write_rules(max_cost_usd: 0.05, warn_if_regex: ['/password/i'])
93
+
94
+ policy = described_class.instance
95
+ alerts = policy.check_alerts(prompt: 'My password is 123', cost_estimate: 0.1)
96
+ expect(alerts).to contain_exactly(
97
+ { type: 'regex', rule: '/password/i', level: 'warn' },
98
+ { type: 'cost', limit: 0.05, level: 'block' }
99
+ )
100
+ end
101
+
102
+ it 'handles case insensitive regex flags' do
103
+ write_rules(warn_if_regex: ['/ETA/i'])
104
+
105
+ policy = described_class.instance
106
+ alerts = policy.check_alerts(prompt: 'The eta is 5 minutes', cost_estimate: 0.1)
107
+ expect(alerts).to eq [{ type: 'regex', rule: '/ETA/i', level: 'warn' }]
108
+ end
109
+
110
+ it 'handles case sensitive regex when no flags' do
111
+ write_rules(warn_if_regex: ['/ETA'])
112
+
113
+ policy = described_class.instance
114
+ alerts = policy.check_alerts(prompt: 'The eta is 5 minutes', cost_estimate: 0.1)
115
+ expect(alerts).to eq []
116
+ end
117
+
118
+ it 'handles regex with multiple flags' do
119
+ write_rules(warn_if_regex: ['/eta/im'])
120
+
121
+ policy = described_class.instance
122
+ alerts = policy.check_alerts(prompt: "ETA\nis 5 minutes", cost_estimate: 0.1)
123
+ expect(alerts).to eq [{ type: 'regex', rule: '/eta/im', level: 'warn' }]
124
+ end
125
+
126
+ it 'handles plain string patterns' do
127
+ write_rules(warn_if_regex: ['password'])
128
+
129
+ policy = described_class.instance
130
+ alerts = policy.check_alerts(prompt: 'My password is 123', cost_estimate: 0.1)
131
+ expect(alerts).to eq [{ type: 'regex', rule: 'password', level: 'warn' }]
132
+ end
133
+
134
+ it 'handles empty warn_if_regex array' do
135
+ write_rules(warn_if_regex: [])
136
+
137
+ policy = described_class.instance
138
+ alerts = policy.check_alerts(prompt: 'any text', cost_estimate: 0.1)
139
+ expect(alerts).to eq []
140
+ end
141
+ end
142
+
143
+ # -----------------------------------------------------------
144
+ describe 'integration with check!' do
145
+ it 'allows check! to pass even when alerts are present' do
146
+ write_rules(
147
+ max_cost_usd: 1,
148
+ reject_if_regex: [],
149
+ warn_if_regex: ['/password/i']
150
+ )
151
+
152
+ policy = described_class.instance
153
+ result = policy.check!(prompt: 'My password is 123', cost_estimate: 0.5)
154
+ expect(result).to eq :ok
155
+ end
156
+
157
+ it 'still blocks on reject regex even when alerts are present' do
158
+ write_rules(
159
+ max_cost_usd: 1,
160
+ reject_if_regex: ['/ssn/i'],
161
+ warn_if_regex: ['/password/i']
162
+ )
163
+
164
+ policy = described_class.instance
165
+ expect do
166
+ policy.check!(prompt: 'My password is 123 and SSN is 456', cost_estimate: 0.5)
167
+ end.to raise_error(PromptWarden::PolicyError, /reject regex/)
168
+ end
169
+ end
170
+ end
@@ -3,11 +3,11 @@
3
3
  RSpec.describe PromptWarden do
4
4
  subject(:mod) { described_class }
5
5
 
6
- it "has a semantic version number" do
6
+ it 'has a semantic version number' do
7
7
  expect(PromptWarden::VERSION).to match(/\A\d+\.\d+\.\d+\z/)
8
8
  end
9
9
 
10
- it "is defined as a module" do
10
+ it 'is defined as a module' do
11
11
  expect(mod).to be_a(Module)
12
12
  end
13
13
  end
data/spec/spec_helper.rb CHANGED
@@ -1,15 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "prompt_warden"
3
+ require_relative '../lib/prompt_warden'
4
+ require 'webmock/rspec'
5
+ WebMock.disable_net_connect!(allow_localhost: true)
4
6
 
5
7
  RSpec.configure do |config|
6
- # Enable flags like --only-failures and --next-failure
7
- config.example_status_persistence_file_path = ".rspec_status"
8
-
9
- # Disable RSpec exposing methods globally on `Module` and `main`
8
+ config.example_status_persistence_file_path = '.rspec_status'
10
9
  config.disable_monkey_patching!
10
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
11
11
 
12
- config.expect_with :rspec do |c|
13
- c.syntax = :expect
14
- end
12
+ # fresh configuration between examples
13
+ config.before { PromptWarden.reset! if defined?(PromptWarden) }
15
14
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'webmock/rspec'
4
+
5
+ RSpec.describe PromptWarden::Uploader do
6
+ let(:api_url) { 'https://example.com/ingest' }
7
+ let(:config) do
8
+ PromptWarden::Configuration.new.tap do |c|
9
+ c.project_token = 'tok_123'
10
+ c.api_url = api_url
11
+ c.max_retries = 2
12
+ end
13
+ end
14
+
15
+ before do
16
+ PromptWarden.reset!
17
+ PromptWarden.configure do |c|
18
+ c.project_token = config.project_token
19
+ c.api_url = config.api_url
20
+ c.max_retries = config.max_retries
21
+ end
22
+ end
23
+
24
+ after do
25
+ # Clean up any failed files in tmpdir
26
+ Dir.glob(File.join(Dir.tmpdir, 'pw_failed_*.json.gz')).each { |f| File.delete(f) }
27
+ end
28
+
29
+ it 'POSTs gzip payload with auth header' do
30
+ stub = stub_request(:post, api_url)
31
+ .with(headers: { 'Authorization' => 'Bearer tok_123',
32
+ 'Content-Encoding' => 'gzip' })
33
+ .to_return(status: 200)
34
+
35
+ uploader = described_class.instance
36
+ uploader.enqueue('compressed bytes')
37
+
38
+ # give worker thread a tick
39
+ sleep 0.05
40
+
41
+ expect(stub).to have_been_requested
42
+ end
43
+
44
+ it 'retries once on 500 then succeeds' do
45
+ stub_request(:post, api_url)
46
+ .to_return({ status: 500 }, { status: 200 })
47
+
48
+ uploader = described_class.instance
49
+ uploader.enqueue('data')
50
+
51
+ # wait long enough for retry thread (interval=0.05, backoff 2x)
52
+ sleep 0.2
53
+
54
+ expect(a_request(:post, api_url)).to have_been_made.times(2)
55
+ end
56
+
57
+ it 'writes failed batch to disk on network error' do
58
+ # Simulate network error
59
+ stub_request(:post, api_url).to_raise(Faraday::ConnectionFailed.new('fail'))
60
+ uploader = described_class.instance
61
+ uploader.enqueue('diskfail')
62
+ sleep 0.05
63
+ failed_files = Dir.glob(File.join(Dir.tmpdir, 'pw_failed_*.json.gz'))
64
+ expect(failed_files.size).to be >= 1
65
+ expect(File.binread(failed_files.first)).to eq 'diskfail'
66
+ end
67
+
68
+ it 'pw:retry_failed task uploads and deletes failed files' do
69
+ # Write a failed file manually
70
+ path = File.join(Dir.tmpdir, 'pw_failed_test.json.gz')
71
+ File.binwrite(path, 'retryme')
72
+ stub = stub_request(:post, api_url).to_return(status: 200)
73
+ # Run the rake task logic directly
74
+ PromptWarden::Uploader.retry_failed!
75
+ sleep 0.05
76
+ expect(stub).to have_been_requested
77
+ expect(File.exist?(path)).to be false
78
+ end
79
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prompt_warden
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Hammett
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-22 00:00:00.000000000 Z
11
+ date: 2025-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-retry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yaml
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: activesupport
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +66,34 @@ dependencies:
38
66
  - - ">="
39
67
  - !ruby/object:Gem::Version
40
68
  version: '6.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 2.3.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 2.3.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '13.2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '13.2'
41
97
  - !ruby/object:Gem::Dependency
42
98
  name: rspec
43
99
  requirement: !ruby/object:Gem::Requirement
@@ -81,39 +137,41 @@ dependencies:
81
137
  - !ruby/object:Gem::Version
82
138
  version: '2.26'
83
139
  - !ruby/object:Gem::Dependency
84
- name: bundler
140
+ name: timecop
85
141
  requirement: !ruby/object:Gem::Requirement
86
142
  requirements:
87
- - - ">="
143
+ - - "~>"
88
144
  - !ruby/object:Gem::Version
89
- version: 2.3.0
145
+ version: '0.9'
90
146
  type: :development
91
147
  prerelease: false
92
148
  version_requirements: !ruby/object:Gem::Requirement
93
149
  requirements:
94
- - - ">="
150
+ - - "~>"
95
151
  - !ruby/object:Gem::Version
96
- version: 2.3.0
152
+ version: '0.9'
97
153
  - !ruby/object:Gem::Dependency
98
- name: rake
154
+ name: webmock
99
155
  requirement: !ruby/object:Gem::Requirement
100
156
  requirements:
101
157
  - - "~>"
102
158
  - !ruby/object:Gem::Version
103
- version: '13.2'
159
+ version: '3.20'
104
160
  type: :development
105
161
  prerelease: false
106
162
  version_requirements: !ruby/object:Gem::Requirement
107
163
  requirements:
108
164
  - - "~>"
109
165
  - !ruby/object:Gem::Version
110
- version: '13.2'
166
+ version: '3.20'
111
167
  description: |
112
- PromptWarden watches every OpenAI / Anthropic request,
113
- blocks risky prompts, and uploads JSON batches to the PromptWarden SaaS.
168
+ PromptWarden provides automatic instrumentation for OpenAI, Anthropic, and Langchain SDKs.
169
+ Features include policy enforcement, cost calculation, real-time monitoring, and alert recording.
170
+ Includes CLI tool for live event streaming and comprehensive test coverage.
114
171
  email:
115
172
  - hello@promptwarden.io
116
- executables: []
173
+ executables:
174
+ - pw_tail
117
175
  extensions: []
118
176
  extra_rdoc_files: []
119
177
  files:
@@ -128,13 +186,38 @@ files:
128
186
  - README.md
129
187
  - Rakefile
130
188
  - bin/console
189
+ - bin/pw_tail
131
190
  - bin/setup
191
+ - examples/policy.yml
132
192
  - lib/prompt_warden.rb
193
+ - lib/prompt_warden/adapter.rb
194
+ - lib/prompt_warden/buffer.rb
195
+ - lib/prompt_warden/cli.rb
196
+ - lib/prompt_warden/configuration.rb
197
+ - lib/prompt_warden/cost_calculator.rb
198
+ - lib/prompt_warden/event.rb
199
+ - lib/prompt_warden/instrumentation/anthropic.rb
200
+ - lib/prompt_warden/instrumentation/langchain.rb
201
+ - lib/prompt_warden/instrumentation/openai.rb
202
+ - lib/prompt_warden/policy.rb
203
+ - lib/prompt_warden/railtie.rb
204
+ - lib/prompt_warden/uploader.rb
133
205
  - lib/prompt_warden/version.rb
134
206
  - prompt_warden.gemspec
135
207
  - sig/prompt_warden.rbs
208
+ - spec/adapter_auto_detect_spec.rb
209
+ - spec/anthropic_adapter_spec.rb
210
+ - spec/buffer_spec.rb
211
+ - spec/cli_spec.rb
212
+ - spec/configuration_spec.rb
213
+ - spec/cost_calculator_spec.rb
214
+ - spec/event_spec.rb
215
+ - spec/langchain_adapter_spec.rb
216
+ - spec/openai_adapter_spec.rb
217
+ - spec/policy_spec.rb
136
218
  - spec/prompt_warden_spec.rb
137
219
  - spec/spec_helper.rb
220
+ - spec/uploader_spec.rb
138
221
  homepage: https://promptwarden.io
139
222
  licenses:
140
223
  - MIT
@@ -157,5 +240,5 @@ requirements: []
157
240
  rubygems_version: 3.4.19
158
241
  signing_key:
159
242
  specification_version: 4
160
- summary: Rails-native guard-rails & cost logging for LLM calls.
243
+ summary: Record, audit, and guard AI prompt usage with automatic SDK instrumentation.
161
244
  test_files: []