brute 0.4.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/brute/agent.rb +14 -0
- data/lib/brute/diff.rb +18 -28
- data/lib/brute/loop/agent_stream.rb +118 -0
- data/lib/brute/loop/agent_turn.rb +520 -0
- data/lib/brute/{compactor.rb → loop/compactor.rb} +2 -0
- data/lib/brute/{doom_loop.rb → loop/doom_loop.rb} +2 -0
- data/lib/brute/loop/step.rb +332 -0
- data/lib/brute/loop/tool_call_step.rb +90 -0
- data/lib/brute/middleware/compaction_check.rb +60 -146
- data/lib/brute/middleware/doom_loop_detection.rb +95 -92
- data/lib/brute/middleware/llm_call.rb +78 -80
- data/lib/brute/middleware/message_tracking.rb +115 -162
- data/lib/brute/middleware/otel/span.rb +25 -106
- data/lib/brute/middleware/otel/token_usage.rb +29 -84
- data/lib/brute/middleware/otel/tool_calls.rb +23 -107
- data/lib/brute/middleware/otel/tool_results.rb +22 -86
- data/lib/brute/middleware/reasoning_normalizer.rb +78 -103
- data/lib/brute/middleware/retry.rb +95 -76
- data/lib/brute/middleware/session_persistence.rb +38 -37
- data/lib/brute/middleware/token_tracking.rb +64 -63
- data/lib/brute/middleware/tool_error_tracking.rb +108 -82
- data/lib/brute/middleware/tool_use_guard.rb +57 -90
- data/lib/brute/middleware/tracing.rb +53 -63
- data/lib/brute/middleware.rb +18 -0
- data/lib/brute/orchestrator/turn.rb +105 -0
- data/lib/brute/pipeline.rb +77 -133
- data/lib/brute/prompts/build_switch.rb +21 -25
- data/lib/brute/prompts/environment.rb +31 -35
- data/lib/brute/prompts/identity.rb +22 -29
- data/lib/brute/prompts/instructions.rb +15 -18
- data/lib/brute/prompts/max_steps.rb +18 -25
- data/lib/brute/prompts/plan_reminder.rb +18 -26
- data/lib/brute/prompts/skills.rb +8 -30
- data/lib/brute/prompts.rb +28 -0
- data/lib/brute/providers/ollama.rb +135 -0
- data/lib/brute/providers/shell.rb +2 -2
- data/lib/brute/providers/shell_response.rb +2 -2
- data/lib/brute/providers.rb +62 -0
- data/lib/brute/queue/base_queue.rb +222 -0
- data/lib/brute/{file_mutation_queue.rb → queue/file_mutation_queue.rb} +28 -26
- data/lib/brute/queue/parallel_queue.rb +66 -0
- data/lib/brute/queue/sequential_queue.rb +63 -0
- data/lib/brute/store/message_store.rb +362 -0
- data/lib/brute/store/session.rb +106 -0
- data/lib/brute/{snapshot_store.rb → store/snapshot_store.rb} +2 -0
- data/lib/brute/{todo_store.rb → store/todo_store.rb} +2 -0
- data/lib/brute/system_prompt.rb +81 -194
- data/lib/brute/tools/delegate.rb +46 -116
- data/lib/brute/tools/fs_patch.rb +36 -37
- data/lib/brute/tools/fs_remove.rb +2 -2
- data/lib/brute/tools/fs_undo.rb +2 -2
- data/lib/brute/tools/fs_write.rb +29 -41
- data/lib/brute/tools/todo_read.rb +1 -1
- data/lib/brute/tools/todo_write.rb +1 -1
- data/lib/brute/tools.rb +31 -0
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +40 -204
- metadata +31 -20
- data/lib/brute/agent_stream.rb +0 -181
- data/lib/brute/hooks.rb +0 -84
- data/lib/brute/message_store.rb +0 -463
- data/lib/brute/orchestrator.rb +0 -550
- data/lib/brute/session.rb +0 -161
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require "brute"
|
|
6
|
-
end
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "brute"
|
|
7
5
|
|
|
8
6
|
module Brute
|
|
9
7
|
module Middleware
|
|
@@ -14,7 +12,7 @@ module Brute
|
|
|
14
12
|
# propagate immediately.
|
|
15
13
|
#
|
|
16
14
|
# Unlike forgecode's separate retry.rs, this middleware wraps the LLM call
|
|
17
|
-
# directly — it sees the error and retries without the
|
|
15
|
+
# directly — it sees the error and retries without the agent loop knowing.
|
|
18
16
|
#
|
|
19
17
|
class Retry < Base
|
|
20
18
|
DEFAULT_MAX_ATTEMPTS = 3
|
|
@@ -49,90 +47,111 @@ module Brute
|
|
|
49
47
|
end
|
|
50
48
|
end
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
require_relative "../../../spec/
|
|
54
|
-
|
|
55
|
-
RSpec.describe Brute::Middleware::Retry do
|
|
56
|
-
let(:response) { MockResponse.new(content: "success") }
|
|
57
|
-
|
|
58
|
-
it "returns the response on first successful call" do
|
|
59
|
-
app, calls = mock_inner_app(response: response)
|
|
60
|
-
middleware = described_class.new(app)
|
|
61
|
-
env = build_env
|
|
62
|
-
|
|
63
|
-
result = middleware.call(env)
|
|
64
|
-
|
|
65
|
-
expect(result).to eq(response)
|
|
66
|
-
expect(calls.size).to eq(1)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
it "retries on LLM::RateLimitError and succeeds" do
|
|
70
|
-
app = flaky_inner_app(LLM::RateLimitError, fail_count: 2, response: response)
|
|
71
|
-
middleware = described_class.new(app, max_attempts: 3, base_delay: 2)
|
|
72
|
-
allow(middleware).to receive(:sleep)
|
|
73
|
-
env = build_env
|
|
50
|
+
test do
|
|
51
|
+
require_relative "../../../spec/support/mock_provider"
|
|
52
|
+
require_relative "../../../spec/support/mock_response"
|
|
74
53
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
54
|
+
def build_env(**overrides)
|
|
55
|
+
{ provider: MockProvider.new, model: nil, input: "test prompt", tools: [],
|
|
56
|
+
messages: [], stream: nil, params: {}, metadata: {}, callbacks: {},
|
|
57
|
+
tool_results: nil, streaming: false, should_exit: nil, pending_functions: [] }.merge(overrides)
|
|
58
|
+
end
|
|
80
59
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
60
|
+
def mock_inner_app(response:)
|
|
61
|
+
calls = []
|
|
62
|
+
app = ->(env) { calls << env; response }
|
|
63
|
+
[app, calls]
|
|
64
|
+
end
|
|
86
65
|
|
|
87
|
-
|
|
66
|
+
def flaky_inner_app(error_class, fail_count:, response:)
|
|
67
|
+
attempt = 0
|
|
68
|
+
->(env) { attempt += 1; raise error_class, "transient" if attempt <= fail_count; response }
|
|
69
|
+
end
|
|
88
70
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
71
|
+
def no_sleep_retry(*args, **kwargs)
|
|
72
|
+
mw = Brute::Middleware::Retry.new(*args, **kwargs)
|
|
73
|
+
mw.define_singleton_method(:sleep) { |_| }
|
|
74
|
+
mw
|
|
75
|
+
end
|
|
92
76
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
77
|
+
it "returns the response on first successful call" do
|
|
78
|
+
response = MockResponse.new(content: "success")
|
|
79
|
+
app, calls = mock_inner_app(response: response)
|
|
80
|
+
middleware = Brute::Middleware::Retry.new(app)
|
|
81
|
+
result = middleware.call(build_env)
|
|
82
|
+
result.should == response
|
|
83
|
+
end
|
|
98
84
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
85
|
+
it "calls inner app exactly once on success" do
|
|
86
|
+
response = MockResponse.new(content: "success")
|
|
87
|
+
app, calls = mock_inner_app(response: response)
|
|
88
|
+
Brute::Middleware::Retry.new(app).call(build_env)
|
|
89
|
+
calls.size.should == 1
|
|
90
|
+
end
|
|
102
91
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
92
|
+
it "retries on LLM::RateLimitError and succeeds" do
|
|
93
|
+
response = MockResponse.new(content: "success")
|
|
94
|
+
app = flaky_inner_app(LLM::RateLimitError, fail_count: 2, response: response)
|
|
95
|
+
middleware = no_sleep_retry(app, max_attempts: 3, base_delay: 2)
|
|
96
|
+
env = build_env
|
|
97
|
+
result = middleware.call(env)
|
|
98
|
+
result.should == response
|
|
99
|
+
end
|
|
108
100
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
101
|
+
it "records retry_attempt in metadata after retries" do
|
|
102
|
+
response = MockResponse.new(content: "success")
|
|
103
|
+
app = flaky_inner_app(LLM::RateLimitError, fail_count: 2, response: response)
|
|
104
|
+
middleware = no_sleep_retry(app, max_attempts: 3, base_delay: 2)
|
|
105
|
+
env = build_env
|
|
106
|
+
middleware.call(env)
|
|
107
|
+
env[:metadata][:retry_attempt].should == 2
|
|
108
|
+
end
|
|
112
109
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
it "retries on LLM::ServerError and succeeds" do
|
|
111
|
+
response = MockResponse.new(content: "success")
|
|
112
|
+
app = flaky_inner_app(LLM::ServerError, fail_count: 1, response: response)
|
|
113
|
+
middleware = no_sleep_retry(app, max_attempts: 3, base_delay: 2)
|
|
114
|
+
result = middleware.call(build_env)
|
|
115
|
+
result.should == response
|
|
116
|
+
end
|
|
118
117
|
|
|
119
|
-
|
|
118
|
+
it "re-raises after exhausting all attempts" do
|
|
119
|
+
app = ->(_env) { raise LLM::RateLimitError, "rate limited" }
|
|
120
|
+
middleware = no_sleep_retry(app, max_attempts: 3, base_delay: 2)
|
|
121
|
+
lambda { middleware.call(build_env) }.should.raise(LLM::RateLimitError)
|
|
122
|
+
end
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
it "does not retry non-retryable errors" do
|
|
125
|
+
call_count = 0
|
|
126
|
+
app = ->(_env) { call_count += 1; raise ArgumentError, "bad input" }
|
|
127
|
+
middleware = Brute::Middleware::Retry.new(app)
|
|
128
|
+
lambda { middleware.call(build_env) }.should.raise(ArgumentError)
|
|
129
|
+
end
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
it "only calls inner app once for non-retryable errors" do
|
|
132
|
+
call_count = 0
|
|
133
|
+
app = ->(_env) { call_count += 1; raise ArgumentError, "bad input" }
|
|
134
|
+
middleware = Brute::Middleware::Retry.new(app)
|
|
135
|
+
begin; middleware.call(build_env); rescue ArgumentError; end
|
|
136
|
+
call_count.should == 1
|
|
137
|
+
end
|
|
131
138
|
|
|
132
|
-
|
|
139
|
+
it "records retry_delay in metadata" do
|
|
140
|
+
response = MockResponse.new(content: "success")
|
|
141
|
+
app = flaky_inner_app(LLM::RateLimitError, fail_count: 1, response: response)
|
|
142
|
+
middleware = no_sleep_retry(app, max_attempts: 3, base_delay: 3)
|
|
143
|
+
env = build_env
|
|
144
|
+
middleware.call(env)
|
|
145
|
+
env[:metadata][:retry_delay].should == 3
|
|
146
|
+
end
|
|
133
147
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
it "tracks sleep delays for exponential backoff" do
|
|
149
|
+
response = MockResponse.new(content: "success")
|
|
150
|
+
app = flaky_inner_app(LLM::RateLimitError, fail_count: 2, response: response)
|
|
151
|
+
delays = []
|
|
152
|
+
mw = Brute::Middleware::Retry.new(app, max_attempts: 3, base_delay: 2)
|
|
153
|
+
mw.define_singleton_method(:sleep) { |d| delays << d }
|
|
154
|
+
mw.call(build_env)
|
|
155
|
+
delays.should == [2, 4]
|
|
137
156
|
end
|
|
138
157
|
end
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require "brute"
|
|
6
|
-
end
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "brute"
|
|
7
5
|
|
|
8
6
|
module Brute
|
|
9
7
|
module Middleware
|
|
10
8
|
# Saves the conversation to disk after each LLM call.
|
|
11
9
|
#
|
|
12
|
-
# Runs POST-call:
|
|
13
|
-
# a broken session save should never crash
|
|
10
|
+
# Runs POST-call: serializes env[:messages] via Session#save_messages.
|
|
11
|
+
# Failures are non-fatal — a broken session save should never crash
|
|
12
|
+
# the agent loop.
|
|
14
13
|
#
|
|
15
14
|
class SessionPersistence < Base
|
|
16
15
|
def initialize(app, session:)
|
|
@@ -22,7 +21,7 @@ module Brute
|
|
|
22
21
|
response = @app.call(env)
|
|
23
22
|
|
|
24
23
|
begin
|
|
25
|
-
@session.
|
|
24
|
+
@session.save_messages(env[:messages])
|
|
26
25
|
rescue => e
|
|
27
26
|
warn "[brute] Session save failed: #{e.message}"
|
|
28
27
|
end
|
|
@@ -33,39 +32,41 @@ module Brute
|
|
|
33
32
|
end
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
require_relative "../../../spec/
|
|
38
|
-
|
|
39
|
-
RSpec.describe Brute::Middleware::SessionPersistence do
|
|
40
|
-
let(:response) { MockResponse.new(content: "saved response") }
|
|
41
|
-
let(:inner_app) { ->(_env) { response } }
|
|
42
|
-
let(:session) { double("session", save: nil) }
|
|
43
|
-
let(:middleware) { described_class.new(inner_app, session: session) }
|
|
44
|
-
|
|
45
|
-
it "passes the response through unchanged" do
|
|
46
|
-
env = build_env
|
|
47
|
-
result = middleware.call(env)
|
|
48
|
-
expect(result).to eq(response)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
it "calls session.save with the context after a successful LLM call" do
|
|
52
|
-
env = build_env
|
|
53
|
-
middleware.call(env)
|
|
54
|
-
expect(session).to have_received(:save).with(env[:context])
|
|
55
|
-
end
|
|
35
|
+
test do
|
|
36
|
+
require_relative "../../../spec/support/mock_provider"
|
|
37
|
+
require_relative "../../../spec/support/mock_response"
|
|
56
38
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
39
|
+
def build_env(**overrides)
|
|
40
|
+
{ provider: MockProvider.new, model: nil, input: "test prompt", tools: [],
|
|
41
|
+
messages: [], stream: nil, params: {}, metadata: {}, callbacks: {},
|
|
42
|
+
tool_results: nil, streaming: false, should_exit: nil, pending_functions: [] }.merge(overrides)
|
|
43
|
+
end
|
|
60
44
|
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
it "passes the response through unchanged" do
|
|
46
|
+
response = MockResponse.new(content: "saved response")
|
|
47
|
+
session = Struct.new(:saved) { def save_messages(m); self.saved = m; end }.new
|
|
48
|
+
inner_app = ->(_env) { response }
|
|
49
|
+
middleware = Brute::Middleware::SessionPersistence.new(inner_app, session: session)
|
|
50
|
+
result = middleware.call(build_env)
|
|
51
|
+
result.should == response
|
|
52
|
+
end
|
|
63
53
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
it "calls session.save_messages with env messages" do
|
|
55
|
+
response = MockResponse.new(content: "saved response")
|
|
56
|
+
session = Struct.new(:saved) { def save_messages(m); self.saved = m; end }.new
|
|
57
|
+
inner_app = ->(_env) { response }
|
|
58
|
+
middleware = Brute::Middleware::SessionPersistence.new(inner_app, session: session)
|
|
59
|
+
messages = [LLM::Message.new(:user, "hello")]
|
|
60
|
+
middleware.call(build_env(messages: messages))
|
|
61
|
+
session.saved.should == messages
|
|
62
|
+
end
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
it "does not propagate session save failures" do
|
|
65
|
+
response = MockResponse.new(content: "saved response")
|
|
66
|
+
session = Object.new
|
|
67
|
+
session.define_singleton_method(:save_messages) { |_| raise RuntimeError, "disk full" }
|
|
68
|
+
inner_app = ->(_env) { response }
|
|
69
|
+
middleware = Brute::Middleware::SessionPersistence.new(inner_app, session: session)
|
|
70
|
+
lambda { middleware.call(build_env) }.should.not.raise
|
|
70
71
|
end
|
|
71
72
|
end
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require "brute"
|
|
6
|
-
end
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "brute"
|
|
7
5
|
|
|
8
6
|
module Brute
|
|
9
7
|
module Middleware
|
|
@@ -50,74 +48,77 @@ module Brute
|
|
|
50
48
|
end
|
|
51
49
|
end
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
require_relative "../../../spec/
|
|
55
|
-
|
|
56
|
-
RSpec.describe Brute::Middleware::TokenTracking do
|
|
57
|
-
let(:response) do
|
|
58
|
-
MockResponse.new(
|
|
59
|
-
content: "hello",
|
|
60
|
-
usage: LLM::Usage.new(input_tokens: 100, output_tokens: 50, reasoning_tokens: 10, total_tokens: 160)
|
|
61
|
-
)
|
|
62
|
-
end
|
|
51
|
+
test do
|
|
52
|
+
require_relative "../../../spec/support/mock_provider"
|
|
53
|
+
require_relative "../../../spec/support/mock_response"
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
result = middleware.call(env)
|
|
70
|
-
expect(result).to eq(response)
|
|
71
|
-
end
|
|
55
|
+
def build_env(**overrides)
|
|
56
|
+
{ provider: MockProvider.new, model: nil, input: "test prompt", tools: [],
|
|
57
|
+
messages: [], stream: nil, params: {}, metadata: {}, callbacks: {},
|
|
58
|
+
tool_results: nil, streaming: false, should_exit: nil, pending_functions: [] }.merge(overrides)
|
|
59
|
+
end
|
|
72
60
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
tokens = env[:metadata][:tokens]
|
|
78
|
-
expect(tokens[:total_input]).to eq(100)
|
|
79
|
-
expect(tokens[:total_output]).to eq(50)
|
|
80
|
-
expect(tokens[:total_reasoning]).to eq(10)
|
|
81
|
-
expect(tokens[:total]).to eq(150) # input + output
|
|
82
|
-
expect(tokens[:call_count]).to eq(1)
|
|
83
|
-
expect(tokens[:last_call]).to eq(input: 100, output: 50, total: 160)
|
|
84
|
-
end
|
|
61
|
+
def make_response
|
|
62
|
+
MockResponse.new(content: "hello",
|
|
63
|
+
usage: LLM::Usage.new(input_tokens: 100, output_tokens: 50, reasoning_tokens: 10, total_tokens: 160))
|
|
64
|
+
end
|
|
85
65
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
66
|
+
it "passes the response through unchanged" do
|
|
67
|
+
response = make_response
|
|
68
|
+
middleware = Brute::Middleware::TokenTracking.new(->(_env) { response })
|
|
69
|
+
result = middleware.call(build_env)
|
|
70
|
+
result.should == response
|
|
71
|
+
end
|
|
90
72
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
73
|
+
it "populates total_input tokens" do
|
|
74
|
+
middleware = Brute::Middleware::TokenTracking.new(->(_env) { make_response })
|
|
75
|
+
env = build_env
|
|
76
|
+
middleware.call(env)
|
|
77
|
+
env[:metadata][:tokens][:total_input].should == 100
|
|
78
|
+
end
|
|
97
79
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
80
|
+
it "populates total_output tokens" do
|
|
81
|
+
middleware = Brute::Middleware::TokenTracking.new(->(_env) { make_response })
|
|
82
|
+
env = build_env
|
|
83
|
+
middleware.call(env)
|
|
84
|
+
env[:metadata][:tokens][:total_output].should == 50
|
|
85
|
+
end
|
|
103
86
|
|
|
104
|
-
|
|
105
|
-
|
|
87
|
+
it "populates total_reasoning tokens" do
|
|
88
|
+
middleware = Brute::Middleware::TokenTracking.new(->(_env) { make_response })
|
|
89
|
+
env = build_env
|
|
90
|
+
middleware.call(env)
|
|
91
|
+
env[:metadata][:tokens][:total_reasoning].should == 10
|
|
92
|
+
end
|
|
106
93
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
94
|
+
it "populates call_count" do
|
|
95
|
+
middleware = Brute::Middleware::TokenTracking.new(->(_env) { make_response })
|
|
96
|
+
env = build_env
|
|
97
|
+
middleware.call(env)
|
|
98
|
+
env[:metadata][:tokens][:call_count].should == 1
|
|
99
|
+
end
|
|
110
100
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
it "accumulates token counts across multiple calls" do
|
|
102
|
+
middleware = Brute::Middleware::TokenTracking.new(->(_env) { make_response })
|
|
103
|
+
env = build_env
|
|
104
|
+
middleware.call(env)
|
|
105
|
+
middleware.call(env)
|
|
106
|
+
env[:metadata][:tokens][:total_input].should == 200
|
|
107
|
+
end
|
|
116
108
|
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
it "handles a response without usage gracefully" do
|
|
110
|
+
no_usage = Object.new
|
|
111
|
+
middleware = Brute::Middleware::TokenTracking.new(->(_env) { no_usage })
|
|
112
|
+
env = build_env
|
|
113
|
+
middleware.call(env)
|
|
114
|
+
env[:metadata][:tokens].should.be.nil
|
|
115
|
+
end
|
|
119
116
|
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
it "handles a response where usage returns nil" do
|
|
118
|
+
nil_usage = Struct.new(:usage).new(nil)
|
|
119
|
+
middleware = Brute::Middleware::TokenTracking.new(->(_env) { nil_usage })
|
|
120
|
+
env = build_env
|
|
121
|
+
middleware.call(env)
|
|
122
|
+
env[:metadata][:tokens].should.be.nil
|
|
122
123
|
end
|
|
123
124
|
end
|