brute 0.4.1 → 1.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 +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
|
|
@@ -20,7 +18,7 @@ module Brute
|
|
|
20
18
|
|
|
21
19
|
span = env[:span]
|
|
22
20
|
if span
|
|
23
|
-
functions = env[:
|
|
21
|
+
functions = env[:pending_functions]
|
|
24
22
|
if functions && !functions.empty?
|
|
25
23
|
span.set_attribute("brute.tool_calls.count", functions.size)
|
|
26
24
|
|
|
@@ -43,110 +41,28 @@ module Brute
|
|
|
43
41
|
end
|
|
44
42
|
end
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
require_relative "../../../../spec/
|
|
48
|
-
|
|
49
|
-
RSpec.describe Brute::Middleware::OTel::ToolCalls do
|
|
50
|
-
let(:response) { MockResponse.new(content: "here's my plan") }
|
|
51
|
-
let(:inner_app) { ->(_env) { response } }
|
|
52
|
-
let(:middleware) { described_class.new(inner_app) }
|
|
53
|
-
|
|
54
|
-
it "passes the response through unchanged" do
|
|
55
|
-
env = build_env
|
|
56
|
-
result = middleware.call(env)
|
|
57
|
-
expect(result).to eq(response)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
context "when env[:span] is nil" do
|
|
61
|
-
it "passes through without error even with pending functions" do
|
|
62
|
-
ctx = build_env[:context]
|
|
63
|
-
fn = double("function", name: "fs_read", id: "tc_001", arguments: { "path" => "/tmp" })
|
|
64
|
-
allow(ctx).to receive(:functions).and_return([fn])
|
|
65
|
-
|
|
66
|
-
env = build_env(context: ctx)
|
|
67
|
-
result = middleware.call(env)
|
|
68
|
-
expect(result).to eq(response)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
context "when env[:span] is present" do
|
|
73
|
-
let(:span) { mock_span }
|
|
74
|
-
|
|
75
|
-
it "does nothing when there are no pending functions" do
|
|
76
|
-
ctx = build_env[:context]
|
|
77
|
-
allow(ctx).to receive(:functions).and_return([])
|
|
78
|
-
|
|
79
|
-
env = build_env(context: ctx, span: span)
|
|
80
|
-
middleware.call(env)
|
|
81
|
-
|
|
82
|
-
expect(span).not_to have_received(:add_event)
|
|
83
|
-
expect(span).not_to have_received(:set_attribute)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
it "does nothing when functions is nil" do
|
|
87
|
-
ctx = build_env[:context]
|
|
88
|
-
allow(ctx).to receive(:functions).and_return(nil)
|
|
89
|
-
|
|
90
|
-
env = build_env(context: ctx, span: span)
|
|
91
|
-
middleware.call(env)
|
|
44
|
+
test do
|
|
45
|
+
require_relative "../../../../spec/support/mock_provider"
|
|
46
|
+
require_relative "../../../../spec/support/mock_response"
|
|
92
47
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
fn1 = double("function", name: "fs_read", id: "tc_001", arguments: { "path" => "/src/main.rb" })
|
|
99
|
-
fn2 = double("function", name: "shell", id: "tc_002", arguments: { "command" => "rspec" })
|
|
100
|
-
allow(ctx).to receive(:functions).and_return([fn1, fn2])
|
|
101
|
-
|
|
102
|
-
env = build_env(context: ctx, span: span)
|
|
103
|
-
middleware.call(env)
|
|
104
|
-
|
|
105
|
-
expect(span).to have_received(:set_attribute).with("brute.tool_calls.count", 2)
|
|
106
|
-
expect(span).to have_received(:add_event).with(
|
|
107
|
-
"tool_call",
|
|
108
|
-
attributes: hash_including(
|
|
109
|
-
"tool.name" => "fs_read",
|
|
110
|
-
"tool.id" => "tc_001"
|
|
111
|
-
)
|
|
112
|
-
)
|
|
113
|
-
expect(span).to have_received(:add_event).with(
|
|
114
|
-
"tool_call",
|
|
115
|
-
attributes: hash_including(
|
|
116
|
-
"tool.name" => "shell",
|
|
117
|
-
"tool.id" => "tc_002"
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
it "serializes arguments as JSON" do
|
|
123
|
-
ctx = build_env[:context]
|
|
124
|
-
args = { "path" => "/tmp/test.rb", "content" => "puts 'hi'" }
|
|
125
|
-
fn = double("function", name: "fs_write", id: "tc_003", arguments: args)
|
|
126
|
-
allow(ctx).to receive(:functions).and_return([fn])
|
|
127
|
-
|
|
128
|
-
env = build_env(context: ctx, span: span)
|
|
129
|
-
middleware.call(env)
|
|
130
|
-
|
|
131
|
-
expect(span).to have_received(:add_event).with(
|
|
132
|
-
"tool_call",
|
|
133
|
-
attributes: hash_including("tool.arguments" => args.to_json)
|
|
134
|
-
)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
it "handles nil arguments" do
|
|
138
|
-
ctx = build_env[:context]
|
|
139
|
-
fn = double("function", name: "todo_read", id: "tc_004", arguments: nil)
|
|
140
|
-
allow(ctx).to receive(:functions).and_return([fn])
|
|
48
|
+
def build_env(**overrides)
|
|
49
|
+
{ provider: MockProvider.new, model: nil, input: "test prompt", tools: [],
|
|
50
|
+
messages: [], stream: nil, params: {}, metadata: {}, callbacks: {},
|
|
51
|
+
tool_results: nil, streaming: false, should_exit: nil, pending_functions: [] }.merge(overrides)
|
|
52
|
+
end
|
|
141
53
|
|
|
142
|
-
|
|
143
|
-
|
|
54
|
+
it "passes the response through unchanged" do
|
|
55
|
+
response = MockResponse.new(content: "here's my plan")
|
|
56
|
+
middleware = Brute::Middleware::OTel::ToolCalls.new(->(_env) { response })
|
|
57
|
+
result = middleware.call(build_env)
|
|
58
|
+
result.should == response
|
|
59
|
+
end
|
|
144
60
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
61
|
+
it "passes through without error when span is nil with pending functions" do
|
|
62
|
+
response = MockResponse.new(content: "here's my plan")
|
|
63
|
+
fn = Struct.new(:name, :id, :arguments, keyword_init: true).new(name: "fs_read", id: "tc_001", arguments: { "path" => "/tmp" })
|
|
64
|
+
middleware = Brute::Middleware::OTel::ToolCalls.new(->(_env) { response })
|
|
65
|
+
result = middleware.call(build_env(pending_functions: [fn]))
|
|
66
|
+
result.should == response
|
|
151
67
|
end
|
|
152
68
|
end
|
|
@@ -1,16 +1,14 @@
|
|
|
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
|
module OTel
|
|
11
9
|
# Records tool results being sent back to the LLM as span events.
|
|
12
10
|
#
|
|
13
|
-
# Runs PRE-call: when env[:tool_results] is present, the
|
|
11
|
+
# Runs PRE-call: when env[:tool_results] is present, the agent loop
|
|
14
12
|
# is sending tool execution results back to the LLM. Each result gets
|
|
15
13
|
# a span event with the tool name and success/error status.
|
|
16
14
|
#
|
|
@@ -41,89 +39,27 @@ module Brute
|
|
|
41
39
|
end
|
|
42
40
|
end
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
require_relative "../../../../spec/
|
|
46
|
-
|
|
47
|
-
RSpec.describe Brute::Middleware::OTel::ToolResults do
|
|
48
|
-
let(:response) { MockResponse.new(content: "processed") }
|
|
49
|
-
let(:inner_app) { ->(_env) { response } }
|
|
50
|
-
let(:middleware) { described_class.new(inner_app) }
|
|
51
|
-
|
|
52
|
-
it "passes the response through unchanged" do
|
|
53
|
-
env = build_env
|
|
54
|
-
result = middleware.call(env)
|
|
55
|
-
expect(result).to eq(response)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
context "when env[:span] is nil" do
|
|
59
|
-
it "passes through without error" do
|
|
60
|
-
results = [["fs_read", { content: "data" }]]
|
|
61
|
-
env = build_env(tool_results: results)
|
|
42
|
+
test do
|
|
43
|
+
require_relative "../../../../spec/support/mock_provider"
|
|
44
|
+
require_relative "../../../../spec/support/mock_response"
|
|
62
45
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
context "when env[:span] is present" do
|
|
69
|
-
let(:span) { mock_span }
|
|
70
|
-
|
|
71
|
-
it "does nothing when tool_results is nil" do
|
|
72
|
-
env = build_env(span: span, tool_results: nil)
|
|
73
|
-
middleware.call(env)
|
|
74
|
-
|
|
75
|
-
expect(span).not_to have_received(:add_event)
|
|
76
|
-
expect(span).not_to have_received(:set_attribute)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
it "records a tool_result event per result" do
|
|
80
|
-
results = [
|
|
81
|
-
["fs_read", { content: "file data" }],
|
|
82
|
-
["shell", { output: "ok" }],
|
|
83
|
-
]
|
|
84
|
-
env = build_env(span: span, tool_results: results)
|
|
85
|
-
middleware.call(env)
|
|
86
|
-
|
|
87
|
-
expect(span).to have_received(:set_attribute).with("brute.tool_results.count", 2)
|
|
88
|
-
expect(span).to have_received(:add_event).with(
|
|
89
|
-
"tool_result",
|
|
90
|
-
attributes: hash_including("tool.name" => "fs_read", "tool.status" => "ok")
|
|
91
|
-
)
|
|
92
|
-
expect(span).to have_received(:add_event).with(
|
|
93
|
-
"tool_result",
|
|
94
|
-
attributes: hash_including("tool.name" => "shell", "tool.status" => "ok")
|
|
95
|
-
)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
it "records error status and message for failed tool results" do
|
|
99
|
-
results = [
|
|
100
|
-
["fs_read", { error: "not found" }],
|
|
101
|
-
]
|
|
102
|
-
env = build_env(span: span, tool_results: results)
|
|
103
|
-
middleware.call(env)
|
|
104
|
-
|
|
105
|
-
expect(span).to have_received(:add_event).with(
|
|
106
|
-
"tool_result",
|
|
107
|
-
attributes: hash_including(
|
|
108
|
-
"tool.name" => "fs_read",
|
|
109
|
-
"tool.status" => "error",
|
|
110
|
-
"tool.error" => "not found"
|
|
111
|
-
)
|
|
112
|
-
)
|
|
113
|
-
end
|
|
46
|
+
def build_env(**overrides)
|
|
47
|
+
{ provider: MockProvider.new, model: nil, input: "test prompt", tools: [],
|
|
48
|
+
messages: [], stream: nil, params: {}, metadata: {}, callbacks: {},
|
|
49
|
+
tool_results: nil, streaming: false, should_exit: nil, pending_functions: [] }.merge(overrides)
|
|
50
|
+
end
|
|
114
51
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
env = build_env(span: span, tool_results: results)
|
|
122
|
-
middleware.call(env)
|
|
52
|
+
it "passes the response through unchanged" do
|
|
53
|
+
response = MockResponse.new(content: "processed")
|
|
54
|
+
middleware = Brute::Middleware::OTel::ToolResults.new(->(_env) { response })
|
|
55
|
+
result = middleware.call(build_env)
|
|
56
|
+
result.should == response
|
|
57
|
+
end
|
|
123
58
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
59
|
+
it "passes through without error when span is nil" do
|
|
60
|
+
response = MockResponse.new(content: "processed")
|
|
61
|
+
middleware = Brute::Middleware::OTel::ToolResults.new(->(_env) { response })
|
|
62
|
+
result = middleware.call(build_env(tool_results: [["fs_read", { content: "data" }]]))
|
|
63
|
+
result.should == response
|
|
128
64
|
end
|
|
129
65
|
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
|
|
@@ -102,116 +100,93 @@ module Brute
|
|
|
102
100
|
end
|
|
103
101
|
end
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
require_relative "../../../spec/
|
|
107
|
-
|
|
108
|
-
RSpec.describe Brute::Middleware::ReasoningNormalizer do
|
|
109
|
-
let(:response) { MockResponse.new(content: "reasoned response") }
|
|
110
|
-
let(:inner_app) { ->(_env) { response } }
|
|
111
|
-
|
|
112
|
-
# Build a provider whose class name contains the given string.
|
|
113
|
-
def make_provider(type_name)
|
|
114
|
-
klass = Class.new do
|
|
115
|
-
define_method(:name) { :mock }
|
|
116
|
-
define_method(:default_model) { "mock-model" }
|
|
117
|
-
define_method(:user_role) { :user }
|
|
118
|
-
define_method(:system_role) { :system }
|
|
119
|
-
define_method(:assistant_role) { :assistant }
|
|
120
|
-
define_method(:tool_role) { :tool }
|
|
121
|
-
define_method(:tracer) { nil }
|
|
122
|
-
define_method(:tracer=) { |*| }
|
|
123
|
-
define_method(:complete) { |*_args, **_kw| MockResponse.new(content: "ok") }
|
|
124
|
-
end
|
|
125
|
-
# Override class name to trigger provider_type detection
|
|
126
|
-
klass.define_method(:class) do
|
|
127
|
-
c = super()
|
|
128
|
-
name_str = "LLM::#{type_name}"
|
|
129
|
-
c.define_singleton_method(:name) { name_str }
|
|
130
|
-
c
|
|
131
|
-
end
|
|
132
|
-
klass.new
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
context "with Anthropic provider and budget_tokens" do
|
|
136
|
-
it "injects thinking param into env[:params]" do
|
|
137
|
-
provider = make_provider("Anthropic")
|
|
138
|
-
middleware = described_class.new(inner_app, model_id: "claude-4", budget_tokens: 8000, enabled: true)
|
|
139
|
-
env = build_env(provider: provider, params: {})
|
|
103
|
+
test do
|
|
104
|
+
require_relative "../../../spec/support/mock_provider"
|
|
105
|
+
require_relative "../../../spec/support/mock_response"
|
|
140
106
|
|
|
141
|
-
|
|
107
|
+
def build_env(**overrides)
|
|
108
|
+
{ provider: MockProvider.new, model: nil, input: "test prompt", tools: [],
|
|
109
|
+
messages: [], stream: nil, params: {}, metadata: {}, callbacks: {},
|
|
110
|
+
tool_results: nil, streaming: false, should_exit: nil, pending_functions: [] }.merge(overrides)
|
|
111
|
+
end
|
|
142
112
|
|
|
143
|
-
|
|
144
|
-
|
|
113
|
+
def make_provider(type_name)
|
|
114
|
+
klass = Class.new do
|
|
115
|
+
define_method(:name) { :mock }
|
|
116
|
+
define_method(:default_model) { "mock-model" }
|
|
117
|
+
define_method(:user_role) { :user }
|
|
118
|
+
define_method(:system_role) { :system }
|
|
119
|
+
define_method(:assistant_role) { :assistant }
|
|
120
|
+
define_method(:tool_role) { :tool }
|
|
121
|
+
define_method(:tracer) { nil }
|
|
122
|
+
define_method(:tracer=) { |*| }
|
|
123
|
+
define_method(:complete) { |*_args, **_kw| MockResponse.new(content: "ok") }
|
|
145
124
|
end
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
env = build_env(provider: provider, params: {})
|
|
152
|
-
|
|
153
|
-
middleware.call(env)
|
|
154
|
-
|
|
155
|
-
expect(env[:params][:thinking]).to be_nil
|
|
156
|
-
end
|
|
125
|
+
klass.define_method(:class) do
|
|
126
|
+
c = super()
|
|
127
|
+
name_str = "LLM::#{type_name}"
|
|
128
|
+
c.define_singleton_method(:name) { name_str }
|
|
129
|
+
c
|
|
157
130
|
end
|
|
131
|
+
klass.new
|
|
132
|
+
end
|
|
158
133
|
|
|
159
|
-
|
|
160
|
-
it "injects reasoning_effort based on effort level" do
|
|
161
|
-
provider = make_provider("OpenAI")
|
|
162
|
-
middleware = described_class.new(inner_app, model_id: "o3", effort: :high, enabled: true)
|
|
163
|
-
env = build_env(provider: provider, params: {})
|
|
164
|
-
|
|
165
|
-
middleware.call(env)
|
|
166
|
-
|
|
167
|
-
expect(env[:params][:reasoning_effort]).to eq("high")
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
it "maps effort levels correctly" do
|
|
171
|
-
provider = make_provider("OpenAI")
|
|
172
|
-
|
|
173
|
-
{ low: "low", medium: "medium", high: "high", minimal: "low", max: "high" }.each do |effort, expected|
|
|
174
|
-
middleware = described_class.new(inner_app, model_id: "o3", effort: effort, enabled: true)
|
|
175
|
-
env = build_env(provider: provider, params: {})
|
|
176
|
-
middleware.call(env)
|
|
177
|
-
expect(env[:params][:reasoning_effort]).to eq(expected), "Expected effort #{effort} to map to #{expected}"
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
context "with unknown provider" do
|
|
183
|
-
it "does not inject any reasoning params" do
|
|
184
|
-
provider = make_provider("Mistral")
|
|
185
|
-
middleware = described_class.new(inner_app, model_id: "mistral-large", enabled: true)
|
|
186
|
-
env = build_env(provider: provider, params: {})
|
|
187
|
-
|
|
188
|
-
middleware.call(env)
|
|
134
|
+
inner_app = ->(_env) { MockResponse.new(content: "reasoned response") }
|
|
189
135
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
136
|
+
it "injects thinking param for Anthropic with budget_tokens" do
|
|
137
|
+
provider = make_provider("Anthropic")
|
|
138
|
+
middleware = Brute::Middleware::ReasoningNormalizer.new(inner_app, model_id: "claude-4", budget_tokens: 8000, enabled: true)
|
|
139
|
+
env = build_env(provider: provider, params: {})
|
|
140
|
+
middleware.call(env)
|
|
141
|
+
env[:params][:thinking].should == { type: "enabled", budget_tokens: 8000 }
|
|
142
|
+
end
|
|
193
143
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
144
|
+
it "does not inject thinking param for Anthropic without budget_tokens" do
|
|
145
|
+
provider = make_provider("Anthropic")
|
|
146
|
+
middleware = Brute::Middleware::ReasoningNormalizer.new(inner_app, model_id: "claude-4", enabled: true)
|
|
147
|
+
env = build_env(provider: provider, params: {})
|
|
148
|
+
middleware.call(env)
|
|
149
|
+
env[:params][:thinking].should.be.nil
|
|
150
|
+
end
|
|
199
151
|
|
|
200
|
-
|
|
152
|
+
it "injects reasoning_effort for OpenAI" do
|
|
153
|
+
provider = make_provider("OpenAI")
|
|
154
|
+
middleware = Brute::Middleware::ReasoningNormalizer.new(inner_app, model_id: "o3", effort: :high, enabled: true)
|
|
155
|
+
env = build_env(provider: provider, params: {})
|
|
156
|
+
middleware.call(env)
|
|
157
|
+
env[:params][:reasoning_effort].should == "high"
|
|
158
|
+
end
|
|
201
159
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
160
|
+
it "maps low effort correctly for OpenAI" do
|
|
161
|
+
provider = make_provider("OpenAI")
|
|
162
|
+
middleware = Brute::Middleware::ReasoningNormalizer.new(inner_app, model_id: "o3", effort: :low, enabled: true)
|
|
163
|
+
env = build_env(provider: provider, params: {})
|
|
164
|
+
middleware.call(env)
|
|
165
|
+
env[:params][:reasoning_effort].should == "low"
|
|
166
|
+
end
|
|
205
167
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
168
|
+
it "does not inject params for unknown provider" do
|
|
169
|
+
provider = make_provider("Mistral")
|
|
170
|
+
middleware = Brute::Middleware::ReasoningNormalizer.new(inner_app, model_id: "mistral-large", enabled: true)
|
|
171
|
+
env = build_env(provider: provider, params: {})
|
|
172
|
+
middleware.call(env)
|
|
173
|
+
env[:params].should == {}
|
|
174
|
+
end
|
|
209
175
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
176
|
+
it "does not inject params when disabled" do
|
|
177
|
+
provider = make_provider("Anthropic")
|
|
178
|
+
middleware = Brute::Middleware::ReasoningNormalizer.new(inner_app, model_id: "claude-4", budget_tokens: 8000, enabled: false)
|
|
179
|
+
env = build_env(provider: provider, params: {})
|
|
180
|
+
middleware.call(env)
|
|
181
|
+
env[:params].should == {}
|
|
182
|
+
end
|
|
213
183
|
|
|
214
|
-
|
|
215
|
-
|
|
184
|
+
it "allows model_id to be updated mid-session" do
|
|
185
|
+
middleware = Brute::Middleware::ReasoningNormalizer.new(inner_app, model_id: "old", enabled: true)
|
|
186
|
+
middleware.model_id = "new"
|
|
187
|
+
provider = make_provider("OpenAI")
|
|
188
|
+
env = build_env(provider: provider, params: {})
|
|
189
|
+
middleware.call(env)
|
|
190
|
+
env[:params][:reasoning_effort].should.not.be.nil
|
|
216
191
|
end
|
|
217
192
|
end
|