brute 3.2.1 → 3.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3375f62e8d019bd4a1dd45d2407112ef421c7ce437f604addb0fac51586c5ec
4
- data.tar.gz: 15fbc6ed0dce515a143f2424957de3c120ff4f68e5b097d732b418f9fe43aecc
3
+ metadata.gz: fd242366b30cf384cb0326b8fa006f43883f1cf117d410b830288164a4d13582
4
+ data.tar.gz: 3d4973d9a6fa69e6fd04d464343d8d2d85fb8455236e9d03f32df17888182e1f
5
5
  SHA512:
6
- metadata.gz: f2ea1006786ddbb531aaa2bf708f60a362422acf87159f731d7ed459bf409d89103ea48a6530953eb31382eb6ca44ea314afdfb4b61bf870a6153b8128f498cd
7
- data.tar.gz: 4ba3e5a9822d3c50b498cab1f33730decc3d3298eb289bd0a450c52e681eaa06fdc002cca73bf0c45b353283e95a0f12ec9cc8605a2623a0f43e1df7eb064789
6
+ metadata.gz: f0659b341410c0cb87db3cefe95e3c8c99bbf5ea9c88ba974d6b8f1adc5835919e256a577435748d652e1e229b30fb6cbaac321d297688fc9fac0812a028e369
7
+ data.tar.gz: 5f693c1c49b4bd74ce901e78bcd459f04c6316fe044bb871a04142b09154cb04767a1166dd64d02d2332fb9fac63572a6d13b28fbef40520c18737b1a11f98f4
@@ -3,28 +3,102 @@
3
3
  require "bundler/setup"
4
4
  require "brute"
5
5
  require "brute/message_transport"
6
+ require "json"
6
7
 
7
8
  module Brute
8
9
  class MessageTransport
10
+ # MessageTransport for the open_router_enhanced gem
11
+ # (https://github.com/estiens/open_router_enhanced). Brute does not
12
+ # require it — you do:
13
+ #
14
+ # require "open_router"
9
15
  class OpenRouter < MessageTransport
10
16
  def self.dump(message)
11
17
  message.to_h
12
18
  end
13
19
 
20
+ # An OpenRouter::Response's messages (one per choice; in practice
21
+ # OpenRouter returns exactly one).
22
+ def messages
23
+ return @result.choices.map { |choice| choice["message"] || choice[:message] } if @result.respond_to?(:choices)
24
+
25
+ super
26
+ end
27
+
14
28
  private
15
29
 
16
30
  def wrap(message)
17
31
  # Coerce string keys to symbol keys if necessary
18
32
  hash = message.to_h.transform_keys(&:to_sym)
19
-
33
+ hash[:role] = hash[:role].to_sym if hash.key?(:role)
34
+ hash[:tool_calls] = hash[:tool_calls].map { |tc| wrap_tool_call(tc) } if hash[:tool_calls]
35
+
20
36
  case hash
21
37
  in { role: (:system | :user | :assistant | :tool) }
22
- # Message#initialize handles converting tool_calls & symbolising role!
23
- Brute::Message.new(**hash)
38
+ # Slice away provider extras (refusal, reasoning, model, ...)
39
+ # that Brute::Message doesn't know.
40
+ Brute::Message.new(**hash.slice(:role, :content, :tool_calls, :tool_call_id))
24
41
  else
25
42
  raise "Unrecognised message format #{message.inspect}"
26
43
  end
27
44
  end
45
+
46
+ # An OpenAI-wire tool call ({ id:, type:, function: { name:, arguments: JSON } })
47
+ # -> the flat { id:, name:, arguments: Hash } Brute::Message understands.
48
+ def wrap_tool_call(tool_call)
49
+ tc = tool_call.to_h.transform_keys(&:to_sym)
50
+ return tc unless tc[:function] # already flat { id:, name:, arguments: }
51
+
52
+ function = tc[:function].to_h.transform_keys(&:to_sym)
53
+ arguments = function[:arguments].to_s
54
+
55
+ {
56
+ id: tc[:id],
57
+ name: function[:name],
58
+ arguments: JSON.parse(arguments.empty? ? "{}" : arguments),
59
+ }
60
+ end
28
61
  end
29
62
  end
30
63
  end
64
+
65
+ __END__
66
+
67
+ describe "brute/message_transport/open_router" do
68
+ require "brute/messages"
69
+
70
+ it "dumps a message to the wire format" do
71
+ m = Brute::Message.new(role: :user, content: "hi")
72
+ Brute::MessageTransport::OpenRouter.dump(m).should == { role: :user, content: "hi" }
73
+ end
74
+
75
+ it "wraps a response's choice messages with symbolised roles, dropping provider extras" do
76
+ fake_response = Struct.new(:choices).new([
77
+ { "message" => { "role" => "assistant", "content" => "hi there",
78
+ "refusal" => nil, "reasoning" => nil, "model" => "openrouter/auto" } },
79
+ ])
80
+
81
+ out = Brute::MessageTransport::OpenRouter.new(fake_response).wrap_each.to_a
82
+
83
+ out.size.should == 1
84
+ out.first.role.should == :assistant
85
+ out.first.content.should == "hi there"
86
+ end
87
+
88
+ it "unwraps OpenAI-wire tool calls and parses their JSON arguments" do
89
+ fake_response = Struct.new(:choices).new([
90
+ { "message" => {
91
+ "role" => "assistant", "content" => nil,
92
+ "tool_calls" => [{ "id" => "tc1", "type" => "function",
93
+ "function" => { "name" => "shell", "arguments" => '{"command":"ls"}' } }],
94
+ } },
95
+ ])
96
+
97
+ out = Brute::MessageTransport::OpenRouter.new(fake_response).wrap_each.to_a.first
98
+
99
+ out.tool_call?.should.be.true
100
+ out.tool_calls.first.id.should == "tc1"
101
+ out.tool_calls.first.name.should == "shell"
102
+ out.tool_calls.first.arguments.should == { "command" => "ls" }
103
+ end
104
+ end
@@ -4,20 +4,25 @@ module Brute
4
4
  module Middleware
5
5
  module OpenRouter
6
6
  class Completion
7
+ # config: keyword arguments for OpenRouter::Client.new
8
+ # (access_token:, request_timeout:, uri_base:, extra_headers:).
9
+ # Defaults to OpenRouter.configuration's global settings.
10
+ # options: keyword arguments for OpenRouter::CompletionOptions.new
11
+ # (model:, temperature:, tools:, ...).
7
12
  def initialize(app, config: {}, **options)
8
13
  @app = app
9
- @config = ::OpenRouter::Configuration.new(config)
10
- @options = ::OpenRouter::CompletionOptions.new(options)
14
+ @config = config
15
+ @options = ::OpenRouter::CompletionOptions.new(**options)
11
16
  end
12
17
 
13
18
  def call(env)
14
19
  messages = Brute::MessageTransport::OpenRouter.dump_all(env[:messages])
15
20
 
16
- ::OpenRouter::Client.new(@config).then do |client|
21
+ ::OpenRouter::Client.new(**@config).then do |client|
17
22
  client.complete(messages, @options).then do |response|
18
23
 
19
24
  # OpenRouter in fact only returns a single message...
20
- # https://github.com/estiens/open_router_enhanced/blob/main/lib/open_router/response.rb#L66
25
+ # https://github.com/estiens/open_router_enhanced/blob/main/lib/open_router/response.rb
21
26
  Brute::MessageTransport::OpenRouter.wrap_each(response) do |message|
22
27
  env[:messages] << message
23
28
  end
data/lib/brute/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Brute
4
- VERSION = "3.2.1"
4
+ VERSION = "3.2.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brute
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.1
4
+ version: 3.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brute Contributors