open_router_enhanced 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a260aa7386fa0f23bc13f963953db3b35f560b18bae0d39c93e4aa8c28562837
4
- data.tar.gz: af442633daa38c99141fbda7b27d5708f101338c98375863fafd56425b1bdf21
3
+ metadata.gz: 34c685a89892a839ba66999ed50a0ed066db83f3a21c95c96edfdc25ade6a2b5
4
+ data.tar.gz: e3fdeb547d1588933cb85984f582d5d18db0b12e40c9adaa2dcb02f400f1b012
5
5
  SHA512:
6
- metadata.gz: 2eef8670903a1b78b17731ab9cc6b75db36d4894c7f1b2361e7978e910c7a5e48e3bdb490a681ace1043d83460a65ab79ba4a2084b1478ea37f0eae8d25d0c40
7
- data.tar.gz: cd027929c19afd7fb18a042a9c097e5f052aaecf4567be972edd564158e56bd4d2e76feac6b68b4f89b27b6b2d70105e80c93bf659f5cba0862d30aa4dba877c
6
+ metadata.gz: c33bca90ab16de26ba7bc7d6c995314f2a5b93b99b1b60a8ffb3fae0eaac94edb275b3777e6bbdd86b34aafe11526aab946b857334013b40cb327620cb91a55c
7
+ data.tar.gz: 790593fb868bb3728ae91630ecbbd85e660fcc1d00a51400cdcfb1fc10f0ef3395eb46dc2624a5af1590cfe6f8a788ae37f3fac651db362bb4556d6fc4e8ee46
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.2.1] - 2025-12-24
4
+
5
+ ### Fixed
6
+ - Memoized `output_id` in `ResponsesToolResult` to ensure consistent IDs across multiple calls
7
+ - Memoized `message_output` and `reasoning_output` finders in `ResponsesResponse` for performance
8
+
9
+ ## [1.2.0] - 2025-12-24
10
+
11
+ ### Added
12
+ - **Responses API**: Full support for OpenRouter's Responses API Beta (`/api/v1/responses`)
13
+ - Simple string or structured array input
14
+ - Reasoning with configurable effort levels (`minimal`, `low`, `medium`, `high`)
15
+ - `ResponsesResponse` wrapper with convenient accessors
16
+ - **Responses API Tool Calling**: Complete function calling support for Responses API
17
+ - `ResponsesToolCall` and `ResponsesToolResult` classes
18
+ - `execute_tool_calls` for easy tool execution with blocks
19
+ - `build_follow_up_input` for multi-turn tool conversations
20
+ - `tool_choice` parameter (`auto`, `required`, `none`)
21
+ - Automatic format conversion from Chat Completions tool format
22
+ - **Shared Tool Call Infrastructure**: Extracted `ToolCallBase` and `ToolResultBase` modules
23
+ - DRY shared behavior for argument parsing and execution
24
+ - Consistent interface across Chat Completions and Responses APIs
25
+
26
+ ### Documentation
27
+ - New `docs/responses_api.md` with comprehensive Responses API guide
28
+ - Tool calling examples with Tool DSL and hash formats
29
+
3
30
  ## [1.1.0] - 2025-12-24
4
31
 
5
32
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- open_router_enhanced (1.1.0)
4
+ open_router_enhanced (1.2.1)
5
5
  activesupport (>= 6.0, < 9.0)
6
6
  dotenv (>= 2.0, < 4.0)
7
7
  faraday (>= 1.0, < 3.0)
@@ -11,33 +11,32 @@ PATH
11
11
  GEM
12
12
  remote: https://rubygems.org/
13
13
  specs:
14
- activesupport (8.0.3)
14
+ activesupport (8.1.1)
15
15
  base64
16
- benchmark (>= 0.3)
17
16
  bigdecimal
18
17
  concurrent-ruby (~> 1.0, >= 1.3.1)
19
18
  connection_pool (>= 2.2.5)
20
19
  drb
21
20
  i18n (>= 1.6, < 2)
21
+ json
22
22
  logger (>= 1.4.2)
23
23
  minitest (>= 5.1)
24
24
  securerandom (>= 0.3)
25
25
  tzinfo (~> 2.0, >= 2.0.5)
26
26
  uri (>= 0.13.1)
27
- addressable (2.8.7)
28
- public_suffix (>= 2.0.2, < 7.0)
27
+ addressable (2.8.8)
28
+ public_suffix (>= 2.0.2, < 8.0)
29
29
  ast (2.4.3)
30
30
  base64 (0.3.0)
31
- benchmark (0.4.1)
32
- bigdecimal (3.3.0)
31
+ bigdecimal (4.0.1)
33
32
  coderay (1.1.3)
34
- concurrent-ruby (1.3.5)
35
- connection_pool (2.5.4)
36
- crack (1.0.0)
33
+ concurrent-ruby (1.3.6)
34
+ connection_pool (3.0.2)
35
+ crack (1.0.1)
37
36
  bigdecimal
38
37
  rexml
39
38
  diff-lcs (1.6.2)
40
- dotenv (3.1.8)
39
+ dotenv (3.2.0)
41
40
  drb (2.2.3)
42
41
  faraday (2.14.0)
43
42
  faraday-net_http (>= 2.0, < 3.5)
@@ -45,50 +44,51 @@ GEM
45
44
  logger
46
45
  faraday-multipart (1.1.1)
47
46
  multipart-post (~> 2.0)
48
- faraday-net_http (3.4.1)
49
- net-http (>= 0.5.0)
47
+ faraday-net_http (3.4.2)
48
+ net-http (~> 0.5)
50
49
  hashdiff (1.2.1)
51
- i18n (1.14.7)
50
+ i18n (1.14.8)
52
51
  concurrent-ruby (~> 1.0)
53
- json (2.15.1)
52
+ json (2.18.0)
54
53
  json-schema (4.3.1)
55
54
  addressable (>= 2.8)
56
55
  language_server-protocol (3.17.0.5)
57
56
  lint_roller (1.1.0)
58
57
  logger (1.7.0)
59
58
  method_source (1.1.0)
60
- minitest (5.25.5)
59
+ minitest (6.0.0)
60
+ prism (~> 1.5)
61
61
  multipart-post (2.4.1)
62
- net-http (0.6.0)
63
- uri
62
+ net-http (0.9.1)
63
+ uri (>= 0.11.1)
64
64
  parallel (1.27.0)
65
- parser (3.3.9.0)
65
+ parser (3.3.10.0)
66
66
  ast (~> 2.4.1)
67
67
  racc
68
- prism (1.5.1)
68
+ prism (1.7.0)
69
69
  pry (0.15.2)
70
70
  coderay (~> 1.1)
71
71
  method_source (~> 1.0)
72
- public_suffix (6.0.2)
72
+ public_suffix (7.0.0)
73
73
  racc (1.8.1)
74
74
  rainbow (3.1.1)
75
- rake (13.3.0)
75
+ rake (13.3.1)
76
76
  regexp_parser (2.11.3)
77
77
  rexml (3.4.4)
78
- rspec (3.13.1)
78
+ rspec (3.13.2)
79
79
  rspec-core (~> 3.13.0)
80
80
  rspec-expectations (~> 3.13.0)
81
81
  rspec-mocks (~> 3.13.0)
82
- rspec-core (3.13.5)
82
+ rspec-core (3.13.6)
83
83
  rspec-support (~> 3.13.0)
84
84
  rspec-expectations (3.13.5)
85
85
  diff-lcs (>= 1.2.0, < 2.0)
86
86
  rspec-support (~> 3.13.0)
87
- rspec-mocks (3.13.5)
87
+ rspec-mocks (3.13.7)
88
88
  diff-lcs (>= 1.2.0, < 2.0)
89
89
  rspec-support (~> 3.13.0)
90
90
  rspec-support (3.13.6)
91
- rubocop (1.81.1)
91
+ rubocop (1.82.1)
92
92
  json (~> 2.3)
93
93
  language_server-protocol (~> 3.17.0.2)
94
94
  lint_roller (~> 1.1.0)
@@ -96,10 +96,10 @@ GEM
96
96
  parser (>= 3.3.0.2)
97
97
  rainbow (>= 2.2.2, < 4.0)
98
98
  regexp_parser (>= 2.9.3, < 3.0)
99
- rubocop-ast (>= 1.47.1, < 2.0)
99
+ rubocop-ast (>= 1.48.0, < 2.0)
100
100
  ruby-progressbar (~> 1.7)
101
101
  unicode-display_width (>= 2.4.0, < 4.0)
102
- rubocop-ast (1.47.1)
102
+ rubocop-ast (1.48.0)
103
103
  parser (>= 3.3.7.2)
104
104
  prism (~> 1.4)
105
105
  ruby-progressbar (1.13.0)
@@ -108,11 +108,10 @@ GEM
108
108
  concurrent-ruby (~> 1.0)
109
109
  unicode-display_width (3.2.0)
110
110
  unicode-emoji (~> 4.1)
111
- unicode-emoji (4.1.0)
112
- uri (1.0.4)
113
- vcr (6.3.1)
114
- base64
115
- webmock (3.25.1)
111
+ unicode-emoji (4.2.0)
112
+ uri (1.1.1)
113
+ vcr (6.4.0)
114
+ webmock (3.26.1)
116
115
  addressable (>= 2.8.0)
117
116
  crack (>= 0.3.2)
118
117
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -0,0 +1,298 @@
1
+ # Responses API (Beta)
2
+
3
+ The Responses API is an OpenAI-compatible stateless endpoint that provides access to multiple AI models with advanced reasoning capabilities.
4
+
5
+ > **Beta**: This API may have breaking changes. Use with caution in production.
6
+
7
+ ## Basic Usage
8
+
9
+ ```ruby
10
+ client = OpenRouter::Client.new
11
+
12
+ response = client.responses(
13
+ "What is the capital of France?",
14
+ model: "openai/gpt-4o-mini"
15
+ )
16
+
17
+ puts response.content # => "Paris"
18
+ ```
19
+
20
+ ## With Reasoning
21
+
22
+ The Responses API supports reasoning with configurable effort levels:
23
+
24
+ ```ruby
25
+ response = client.responses(
26
+ "What is 15% of 80? Show your work.",
27
+ model: "openai/o4-mini",
28
+ reasoning: { effort: "high" },
29
+ max_output_tokens: 500
30
+ )
31
+
32
+ # Access reasoning steps
33
+ if response.has_reasoning?
34
+ puts "Reasoning steps:"
35
+ response.reasoning_summary.each { |step| puts " - #{step}" }
36
+ end
37
+
38
+ puts "Answer: #{response.content}"
39
+ puts "Reasoning tokens used: #{response.reasoning_tokens}"
40
+ ```
41
+
42
+ ### Effort Levels
43
+
44
+ | Level | Description |
45
+ |-------|-------------|
46
+ | `minimal` | Basic reasoning with minimal computational effort |
47
+ | `low` | Light reasoning for simple problems |
48
+ | `medium` | Balanced reasoning for moderate complexity |
49
+ | `high` | Deep reasoning for complex problems |
50
+
51
+ ## Parameters
52
+
53
+ | Parameter | Type | Description |
54
+ |-----------|------|-------------|
55
+ | `input` | String or Array | The input text or structured message array (required) |
56
+ | `model` | String | Model identifier, e.g. `"openai/o4-mini"` (required) |
57
+ | `reasoning` | Hash | Reasoning config with `effort` key |
58
+ | `tools` | Array | Tool definitions for function calling |
59
+ | `tool_choice` | String/Hash | `"auto"`, `"none"`, `"required"`, or specific tool |
60
+ | `max_output_tokens` | Integer | Maximum tokens to generate |
61
+ | `temperature` | Float | Sampling temperature (0-2) |
62
+ | `top_p` | Float | Nucleus sampling parameter (0-1) |
63
+ | `extras` | Hash | Additional API parameters |
64
+
65
+ ## Structured Input
66
+
67
+ You can also use structured message arrays:
68
+
69
+ ```ruby
70
+ response = client.responses(
71
+ [
72
+ {
73
+ "type" => "message",
74
+ "role" => "user",
75
+ "content" => [
76
+ { "type" => "input_text", "text" => "Hello, world!" }
77
+ ]
78
+ }
79
+ ],
80
+ model: "openai/gpt-4o-mini"
81
+ )
82
+ ```
83
+
84
+ ## Response Object
85
+
86
+ The `ResponsesResponse` class provides convenient accessors:
87
+
88
+ ```ruby
89
+ response.id # Response ID
90
+ response.status # "completed", "failed", etc.
91
+ response.model # Model used
92
+ response.content # Assistant's text response
93
+ response.output # Raw output array
94
+
95
+ # Reasoning
96
+ response.has_reasoning? # Boolean
97
+ response.reasoning_summary # Array of reasoning steps
98
+
99
+ # Tool calls
100
+ response.has_tool_calls? # Boolean
101
+ response.tool_calls # Array of ResponsesToolCall objects
102
+ response.tool_calls_raw # Array of raw hash data
103
+
104
+ # Token usage
105
+ response.input_tokens # Input token count
106
+ response.output_tokens # Output token count
107
+ response.total_tokens # Total token count
108
+ response.reasoning_tokens # Tokens used for reasoning
109
+ ```
110
+
111
+ ## Tool/Function Calling
112
+
113
+ The Responses API supports function calling with a simplified format. Tool calls are wrapped in `ResponsesToolCall` objects for easy execution.
114
+
115
+ ### Defining Tools
116
+
117
+ You can use the same tool format as Chat Completions - the gem automatically converts it:
118
+
119
+ ```ruby
120
+ tools = [
121
+ {
122
+ type: "function",
123
+ function: {
124
+ name: "get_weather",
125
+ description: "Get current weather for a location",
126
+ parameters: {
127
+ type: "object",
128
+ properties: {
129
+ location: { type: "string", description: "City name" },
130
+ units: { type: "string", enum: ["celsius", "fahrenheit"] }
131
+ },
132
+ required: ["location"]
133
+ }
134
+ }
135
+ }
136
+ ]
137
+
138
+ response = client.responses(
139
+ "What's the weather in San Francisco?",
140
+ model: "openai/gpt-4o-mini",
141
+ tools: tools
142
+ )
143
+ ```
144
+
145
+ You can also use the `Tool` DSL:
146
+
147
+ ```ruby
148
+ weather_tool = OpenRouter::Tool.define do
149
+ name "get_weather"
150
+ description "Get current weather for a location"
151
+ parameters do
152
+ string :location, required: true, description: "City name"
153
+ string :units, enum: %w[celsius fahrenheit]
154
+ end
155
+ end
156
+
157
+ response = client.responses(
158
+ "What's the weather in Tokyo?",
159
+ model: "openai/gpt-4o-mini",
160
+ tools: [weather_tool]
161
+ )
162
+ ```
163
+
164
+ ### Tool Choice
165
+
166
+ Control when the model uses tools with `tool_choice`:
167
+
168
+ ```ruby
169
+ # Let model decide (default)
170
+ response = client.responses(input, model: model, tools: tools, tool_choice: "auto")
171
+
172
+ # Force tool use
173
+ response = client.responses(input, model: model, tools: tools, tool_choice: "required")
174
+
175
+ # Prevent tool use
176
+ response = client.responses(input, model: model, tools: tools, tool_choice: "none")
177
+ ```
178
+
179
+ ### Executing Tool Calls
180
+
181
+ ```ruby
182
+ if response.has_tool_calls?
183
+ # Execute each tool call with a block
184
+ results = response.execute_tool_calls do |name, arguments|
185
+ case name
186
+ when "get_weather"
187
+ fetch_weather(arguments["location"], arguments["units"])
188
+ when "search_web"
189
+ search(arguments["query"])
190
+ else
191
+ { error: "Unknown function: #{name}" }
192
+ end
193
+ end
194
+
195
+ # Results are ResponsesToolResult objects
196
+ results.each do |result|
197
+ if result.success?
198
+ puts "#{result.tool_call.name}: #{result.result}"
199
+ else
200
+ puts "Error: #{result.error}"
201
+ end
202
+ end
203
+ end
204
+ ```
205
+
206
+ ### Multi-turn Tool Conversations
207
+
208
+ Use `build_follow_up_input` to continue conversations after tool execution:
209
+
210
+ ```ruby
211
+ # First call - model requests tool use
212
+ original_input = "What's the weather in NYC and Paris?"
213
+ response = client.responses(original_input, model: "openai/gpt-4o-mini", tools: tools)
214
+
215
+ # Execute the tool calls
216
+ results = response.execute_tool_calls do |name, args|
217
+ fetch_weather(args["location"])
218
+ end
219
+
220
+ # Build follow-up input with tool results
221
+ next_input = response.build_follow_up_input(
222
+ original_input: original_input,
223
+ tool_results: results
224
+ )
225
+
226
+ # Continue the conversation - model will use the tool results
227
+ final_response = client.responses(next_input, model: "openai/gpt-4o-mini")
228
+ puts final_response.content
229
+ # => "In NYC it's 72°F and sunny. In Paris it's 18°C and cloudy."
230
+ ```
231
+
232
+ ### Adding Follow-up Messages
233
+
234
+ You can include a follow-up question when building the input:
235
+
236
+ ```ruby
237
+ next_input = response.build_follow_up_input(
238
+ original_input: original_input,
239
+ tool_results: results,
240
+ follow_up_message: "Which city has better weather for a picnic?"
241
+ )
242
+ ```
243
+
244
+ ### Tool Call Objects
245
+
246
+ `ResponsesToolCall` provides:
247
+
248
+ ```ruby
249
+ tool_call.id # Tool call ID
250
+ tool_call.call_id # Call ID for result matching
251
+ tool_call.name # Function name
252
+ tool_call.function_name # Alias for name
253
+ tool_call.arguments # Parsed arguments hash
254
+ tool_call.arguments_string # Raw JSON string
255
+ tool_call.to_input_item # Convert to input format
256
+ ```
257
+
258
+ `ResponsesToolResult` provides:
259
+
260
+ ```ruby
261
+ result.tool_call # Reference to the tool call
262
+ result.result # Execution result (if successful)
263
+ result.error # Error message (if failed)
264
+ result.success? # Boolean
265
+ result.failure? # Boolean
266
+ result.to_input_item # Convert to function_call_output format
267
+ ```
268
+
269
+ ## Comparison with Chat Completions
270
+
271
+ | Aspect | `complete()` | `responses()` |
272
+ |--------|--------------|---------------|
273
+ | Endpoint | `/chat/completions` | `/responses` |
274
+ | Input | `messages` array | `input` string or array |
275
+ | Output | `choices[].message` | `output[]` typed items |
276
+ | Reasoning | Not supported | `reasoning` parameter |
277
+ | Tool calling | Supported | Supported |
278
+ | Token limit | `max_tokens` | `max_output_tokens` |
279
+ | Streaming | Supported | Not yet supported |
280
+
281
+ ## When to Use
282
+
283
+ Use the Responses API when you need:
284
+ - Built-in reasoning with effort control
285
+ - OpenAI Responses API compatibility
286
+ - Simpler input format (string instead of messages)
287
+
288
+ Use Chat Completions when you need:
289
+ - Streaming responses
290
+ - Full callback system integration
291
+ - Usage tracking integration
292
+ - Response healing features
293
+
294
+ ## Future Enhancements
295
+
296
+ The following features are planned but not yet implemented:
297
+ - Streaming support
298
+ - Callbacks integration
@@ -9,6 +9,7 @@ require "pry"
9
9
  module OpenRouter
10
10
  class ServerError < StandardError; end
11
11
 
12
+ # rubocop:disable Metrics/ClassLength
12
13
  class Client
13
14
  include OpenRouter::HTTP
14
15
 
@@ -103,8 +104,10 @@ module OpenRouter
103
104
  # @param extras [Hash] Optional hash of model-specific parameters to send to the OpenRouter API
104
105
  # @param stream [Proc, nil] Optional callable object for streaming
105
106
  # @return [Response] The completion response wrapped in a Response object.
107
+ # rubocop:disable Metrics/ParameterLists
106
108
  def complete(messages, model: "openrouter/auto", providers: [], transforms: [], plugins: [], tools: [], tool_choice: nil,
107
109
  response_format: nil, force_structured_output: nil, prediction: nil, extras: {}, stream: nil)
110
+ # rubocop:enable Metrics/ParameterLists
108
111
  parameters = prepare_base_parameters(messages, model, providers, transforms, plugins, prediction, stream, extras)
109
112
  forced_extraction = configure_tools_and_structured_outputs!(parameters, model, tools, tool_choice,
110
113
  response_format, force_structured_output)
@@ -145,6 +148,48 @@ module OpenRouter
145
148
  response["data"]
146
149
  end
147
150
 
151
+ # Performs a request to the Responses API Beta (/api/v1/responses)
152
+ # This is an OpenAI-compatible stateless API with support for reasoning.
153
+ #
154
+ # @param input [String, Array] The input text or structured message array
155
+ # @param model [String] Model identifier (e.g., "openai/o4-mini")
156
+ # @param reasoning [Hash, nil] Optional reasoning config, e.g. {effort: "high"}
157
+ # Effort levels: "minimal", "low", "medium", "high"
158
+ # @param tools [Array<Tool, Hash>] Optional array of tool definitions
159
+ # @param tool_choice [String, Hash, nil] Optional: "auto", "none", "required", or specific tool
160
+ # @param max_output_tokens [Integer, nil] Maximum tokens to generate
161
+ # @param temperature [Float, nil] Sampling temperature (0-2)
162
+ # @param top_p [Float, nil] Nucleus sampling parameter (0-1)
163
+ # @param extras [Hash] Additional parameters to pass to the API
164
+ # @return [ResponsesResponse] The response wrapped in a ResponsesResponse object
165
+ #
166
+ # @example Basic usage
167
+ # response = client.responses("What is 2+2?", model: "openai/o4-mini")
168
+ # puts response.content
169
+ #
170
+ # @example With reasoning
171
+ # response = client.responses(
172
+ # "Solve this step by step: What is 15% of 80?",
173
+ # model: "openai/o4-mini",
174
+ # reasoning: { effort: "high" }
175
+ # )
176
+ # puts response.reasoning_summary
177
+ # puts response.content
178
+ def responses(input, model:, reasoning: nil, tools: [], tool_choice: nil,
179
+ max_output_tokens: nil, temperature: nil, top_p: nil, extras: {})
180
+ parameters = { model: model, input: input }
181
+ parameters[:reasoning] = reasoning if reasoning
182
+ parameters[:tools] = serialize_tools_for_responses(tools) if tools.any?
183
+ parameters[:tool_choice] = tool_choice if tool_choice
184
+ parameters[:max_output_tokens] = max_output_tokens if max_output_tokens
185
+ parameters[:temperature] = temperature if temperature
186
+ parameters[:top_p] = top_p if top_p
187
+ parameters.merge!(extras)
188
+
189
+ raw = post(path: "/responses", parameters: parameters)
190
+ ResponsesResponse.new(raw)
191
+ end
192
+
148
193
  # Create a new ModelSelector for intelligent model selection
149
194
  #
150
195
  # @return [ModelSelector] A new ModelSelector instance
@@ -510,7 +555,8 @@ module OpenRouter
510
555
  end
511
556
  end
512
557
 
513
- # Serialize tools to the format expected by OpenRouter API
558
+ # Serialize tools to the format expected by OpenRouter Chat Completions API
559
+ # Format: { type: "function", function: { name: ..., parameters: ... } }
514
560
  def serialize_tools(tools)
515
561
  tools.map do |tool|
516
562
  case tool
@@ -524,6 +570,34 @@ module OpenRouter
524
570
  end
525
571
  end
526
572
 
573
+ # Serialize tools to the flat format expected by Responses API
574
+ # Format: { type: "function", name: ..., parameters: ... }
575
+ def serialize_tools_for_responses(tools)
576
+ tools.map do |tool|
577
+ tool_hash = case tool
578
+ when Tool
579
+ tool.to_h
580
+ when Hash
581
+ tool.transform_keys(&:to_sym)
582
+ else
583
+ raise ArgumentError, "Tools must be Tool objects or hashes"
584
+ end
585
+
586
+ # Flatten the nested function structure if present
587
+ if tool_hash[:function]
588
+ {
589
+ type: "function",
590
+ name: tool_hash[:function][:name],
591
+ description: tool_hash[:function][:description],
592
+ parameters: tool_hash[:function][:parameters]
593
+ }.compact
594
+ else
595
+ # Already in flat format
596
+ tool_hash
597
+ end
598
+ end
599
+ end
600
+
527
601
  # Serialize response format to the format expected by OpenRouter API
528
602
  def serialize_response_format(response_format)
529
603
  case response_format
@@ -595,4 +669,5 @@ module OpenRouter
595
669
  INSTRUCTION
596
670
  end
597
671
  end
672
+ # rubocop:enable Metrics/ClassLength
598
673
  end
@@ -367,9 +367,9 @@ module OpenRouter
367
367
  total_size = Dir.glob(File.join(CACHE_DIR, "**/*"))
368
368
  .select { |f| File.file?(f) }
369
369
  .sum do |f|
370
- File.size(f)
371
- rescue StandardError
372
- 0
370
+ File.size(f)
371
+ rescue StandardError
372
+ 0
373
373
  end
374
374
  total_size / (1024.0 * 1024.0)
375
375
  end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenRouter
4
+ # Response wrapper for the Responses API Beta (/api/v1/responses)
5
+ # This API differs from chat completions in its response structure,
6
+ # using an `output` array with typed items instead of `choices`.
7
+ class ResponsesResponse
8
+ attr_reader :raw
9
+
10
+ def initialize(raw)
11
+ @raw = raw || {}
12
+ end
13
+
14
+ # Core accessors
15
+ def id
16
+ raw["id"]
17
+ end
18
+
19
+ def status
20
+ # Status can be at top level or derived from message output
21
+ raw["status"] || message_output&.dig("status")
22
+ end
23
+
24
+ def model
25
+ raw["model"]
26
+ end
27
+
28
+ def created_at
29
+ raw["created_at"]
30
+ end
31
+
32
+ def output
33
+ raw["output"] || []
34
+ end
35
+
36
+ def usage
37
+ raw["usage"] || {}
38
+ end
39
+
40
+ # Convenience method to get the assistant's text content
41
+ def content
42
+ message_output&.dig("content", 0, "text")
43
+ end
44
+
45
+ # Get reasoning summary steps (array of strings)
46
+ def reasoning_summary
47
+ reasoning_output&.dig("summary") || []
48
+ end
49
+
50
+ # Check if reasoning was included in the response
51
+ def reasoning?
52
+ !reasoning_output.nil?
53
+ end
54
+ alias has_reasoning? reasoning?
55
+
56
+ # Get tool/function calls from the response as ResponsesToolCall objects
57
+ #
58
+ # @return [Array<ResponsesToolCall>] Array of tool call objects
59
+ def tool_calls
60
+ @tool_calls ||= output
61
+ .select { |o| o["type"] == "function_call" }
62
+ .map { |tc| ResponsesToolCall.new(tc) }
63
+ end
64
+
65
+ # Get raw tool call data (hashes) from the response
66
+ #
67
+ # @return [Array<Hash>] Array of raw tool call hashes
68
+ def tool_calls_raw
69
+ output.select { |o| o["type"] == "function_call" }
70
+ end
71
+
72
+ def tool_calls?
73
+ tool_calls.any?
74
+ end
75
+ alias has_tool_calls? tool_calls?
76
+
77
+ # Execute all tool calls and return results
78
+ #
79
+ # @yield [name, arguments] Block to execute each tool
80
+ # @return [Array<ResponsesToolResult>] Results from all tool executions
81
+ #
82
+ # @example
83
+ # results = response.execute_tool_calls do |name, args|
84
+ # case name
85
+ # when "get_weather" then fetch_weather(args["location"])
86
+ # when "search" then search_web(args["query"])
87
+ # end
88
+ # end
89
+ def execute_tool_calls(&block)
90
+ tool_calls.map { |tc| tc.execute(&block) }
91
+ end
92
+
93
+ # Build a follow-up input array that includes tool results
94
+ # Use this to continue the conversation after executing tools
95
+ #
96
+ # @param original_input [String, Array] The original input sent to the API
97
+ # @param tool_results [Array<ResponsesToolResult>] Results from execute_tool_calls
98
+ # @param follow_up_message [String, nil] Optional follow-up user message
99
+ # @return [Array] Input array for the next API call
100
+ #
101
+ # @example
102
+ # # First call with tools
103
+ # response = client.responses("What's the weather?", model: "...", tools: [...])
104
+ #
105
+ # # Execute tools
106
+ # results = response.execute_tool_calls { |name, args| ... }
107
+ #
108
+ # # Build follow-up input
109
+ # next_input = response.build_follow_up_input(
110
+ # original_input: "What's the weather?",
111
+ # tool_results: results,
112
+ # follow_up_message: "Is that good for a picnic?"
113
+ # )
114
+ #
115
+ # # Continue conversation
116
+ # next_response = client.responses(next_input, model: "...")
117
+ def build_follow_up_input(original_input:, tool_results:, follow_up_message: nil)
118
+ input_items = []
119
+
120
+ # Add original user message
121
+ if original_input.is_a?(String)
122
+ input_items << {
123
+ "type" => "message",
124
+ "role" => "user",
125
+ "content" => [{ "type" => "input_text", "text" => original_input }]
126
+ }
127
+ elsif original_input.is_a?(Array)
128
+ input_items.concat(original_input)
129
+ end
130
+
131
+ # Add function calls from this response
132
+ tool_calls_raw.each do |tc|
133
+ input_items << tc
134
+ end
135
+
136
+ # Add function call outputs
137
+ tool_results.each do |result|
138
+ input_items << result.to_input_item
139
+ end
140
+
141
+ # Add assistant message if present
142
+ input_items << message_output if message_output
143
+
144
+ # Add follow-up user message if provided
145
+ if follow_up_message
146
+ input_items << {
147
+ "type" => "message",
148
+ "role" => "user",
149
+ "content" => [{ "type" => "input_text", "text" => follow_up_message }]
150
+ }
151
+ end
152
+
153
+ input_items
154
+ end
155
+
156
+ # Token counts
157
+ def input_tokens
158
+ usage["input_tokens"] || 0
159
+ end
160
+
161
+ def output_tokens
162
+ usage["output_tokens"] || 0
163
+ end
164
+
165
+ def total_tokens
166
+ usage["total_tokens"] || 0
167
+ end
168
+
169
+ def reasoning_tokens
170
+ usage.dig("output_tokens_details", "reasoning_tokens") || 0
171
+ end
172
+
173
+ # Hash-like access for raw data
174
+ def [](key)
175
+ raw[key]
176
+ end
177
+
178
+ def dig(*keys)
179
+ raw.dig(*keys)
180
+ end
181
+
182
+ private
183
+
184
+ def message_output
185
+ @message_output ||= output.find { |o| o["type"] == "message" }
186
+ end
187
+
188
+ def reasoning_output
189
+ @reasoning_output ||= output.find { |o| o["type"] == "reasoning" }
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module OpenRouter
6
+ # Represents a tool/function call from the Responses API.
7
+ # Format: type="function_call" with name/arguments at top level (not nested)
8
+ class ResponsesToolCall
9
+ include ToolCallBase
10
+
11
+ attr_reader :id, :call_id, :arguments_string
12
+
13
+ def initialize(tool_call_data)
14
+ @id = tool_call_data["id"]
15
+ @call_id = tool_call_data["call_id"]
16
+ @name = tool_call_data["name"]
17
+ @arguments_string = tool_call_data["arguments"] || "{}"
18
+ end
19
+
20
+ # Get the function name
21
+ attr_reader :name
22
+
23
+ # Alias for consistency with ToolCall
24
+ def function_name
25
+ @name
26
+ end
27
+
28
+ # Build result for execute method (required by ToolCallBase)
29
+ def build_result(result, error = nil)
30
+ ResponsesToolResult.new(self, result, error)
31
+ end
32
+
33
+ # Convert to the function_call format for conversation continuation
34
+ def to_input_item
35
+ {
36
+ "type" => "function_call",
37
+ "id" => @id,
38
+ "call_id" => @call_id,
39
+ "name" => @name,
40
+ "arguments" => @arguments_string
41
+ }
42
+ end
43
+
44
+ def to_h
45
+ to_input_item
46
+ end
47
+
48
+ def to_json(*args)
49
+ to_h.to_json(*args)
50
+ end
51
+ end
52
+
53
+ # Represents the result of executing a Responses API tool call
54
+ class ResponsesToolResult
55
+ include ToolResultBase
56
+
57
+ attr_reader :tool_call, :result, :error, :output_id
58
+
59
+ def initialize(tool_call, result = nil, error = nil)
60
+ @tool_call = tool_call
61
+ @result = result
62
+ @error = error
63
+ @output_id = "fc_output_#{SecureRandom.hex(8)}"
64
+ end
65
+
66
+ # Convert to function_call_output format for conversation continuation
67
+ #
68
+ # @return [Hash] The output item for the input array
69
+ def to_input_item
70
+ output_content = if @error
71
+ { error: @error }.to_json
72
+ elsif @result.is_a?(String)
73
+ @result
74
+ else
75
+ @result.to_json
76
+ end
77
+
78
+ {
79
+ "type" => "function_call_output",
80
+ "id" => @output_id,
81
+ "call_id" => @tool_call.call_id,
82
+ "output" => output_content
83
+ }
84
+ end
85
+
86
+ def to_h
87
+ to_input_item
88
+ end
89
+
90
+ def to_json(*args)
91
+ to_h.to_json(*args)
92
+ end
93
+ end
94
+ end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
-
5
3
  module OpenRouter
6
- class ToolCallError < Error; end
7
-
4
+ # Represents a tool/function call from the Chat Completions API.
5
+ # Format: tool_calls[].function.name/arguments (nested under function key)
8
6
  class ToolCall
7
+ include ToolCallBase
8
+
9
9
  attr_reader :id, :type, :function_name, :arguments_string
10
10
 
11
11
  def initialize(tool_call_data)
@@ -15,34 +15,17 @@ module OpenRouter
15
15
  raise ToolCallError, "Invalid tool call data: missing function" unless tool_call_data["function"]
16
16
 
17
17
  @function_name = tool_call_data["function"]["name"]
18
- @arguments_string = tool_call_data["function"]["arguments"]
19
- end
20
-
21
- # Parse the arguments JSON string into a Ruby hash
22
- def arguments
23
- @arguments ||= begin
24
- JSON.parse(@arguments_string)
25
- rescue JSON::ParserError => e
26
- raise ToolCallError, "Failed to parse tool call arguments: #{e.message}"
27
- end
18
+ @arguments_string = tool_call_data["function"]["arguments"] || "{}"
28
19
  end
29
20
 
30
- # Get the function name (alias for consistency)
21
+ # Get the function name
31
22
  def name
32
23
  @function_name
33
24
  end
34
25
 
35
- # Execute the tool call with a provided block
36
- # The block should accept (name, arguments) and return the result
37
- def execute(&block)
38
- raise ArgumentError, "Block required for tool execution" unless block_given?
39
-
40
- begin
41
- result = block.call(@function_name, arguments)
42
- ToolResult.new(self, result)
43
- rescue StandardError => e
44
- ToolResult.new(self, nil, e.message)
45
- end
26
+ # Build result for execute method (required by ToolCallBase)
27
+ def build_result(result, error = nil)
28
+ ToolResult.new(self, result, error)
46
29
  end
47
30
 
48
31
  # Convert this tool call to a message format for conversation continuation
@@ -50,16 +33,7 @@ module OpenRouter
50
33
  {
51
34
  role: "assistant",
52
35
  content: nil,
53
- tool_calls: [
54
- {
55
- id: @id,
56
- type: @type,
57
- function: {
58
- name: @function_name,
59
- arguments: @arguments_string
60
- }
61
- }
62
- ]
36
+ tool_calls: [to_h]
63
37
  }
64
38
  end
65
39
 
@@ -95,8 +69,6 @@ module OpenRouter
95
69
 
96
70
  # Validate against a provided array of tools (Tool instances or hashes)
97
71
  def valid?(tools:)
98
- # tools is now a required keyword argument
99
-
100
72
  schema = find_schema_for_call(tools)
101
73
  return true unless schema # No validation if tool not found
102
74
 
@@ -110,8 +82,6 @@ module OpenRouter
110
82
  end
111
83
 
112
84
  def validation_errors(tools:)
113
- # tools is now a required keyword argument
114
-
115
85
  schema = find_schema_for_call(tools)
116
86
  return [] unless schema # No errors if tool not found
117
87
 
@@ -144,8 +114,10 @@ module OpenRouter
144
114
  end
145
115
  end
146
116
 
147
- # Represents the result of executing a tool call
117
+ # Represents the result of executing a Chat Completions tool call
148
118
  class ToolResult
119
+ include ToolResultBase
120
+
149
121
  attr_reader :tool_call, :result, :error
150
122
 
151
123
  def initialize(tool_call, result = nil, error = nil)
@@ -154,27 +126,9 @@ module OpenRouter
154
126
  @error = error
155
127
  end
156
128
 
157
- def success?
158
- @error.nil?
159
- end
160
-
161
- def failure?
162
- !success?
163
- end
164
-
165
129
  # Convert to message format for conversation continuation
166
130
  def to_message
167
131
  @tool_call.to_result_message(@error || @result)
168
132
  end
169
-
170
- # Create a failed result
171
- def self.failure(tool_call, error)
172
- new(tool_call, nil, error)
173
- end
174
-
175
- # Create a successful result
176
- def self.success(tool_call, result)
177
- new(tool_call, result, nil)
178
- end
179
133
  end
180
134
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module OpenRouter
6
+ class ToolCallError < Error; end
7
+
8
+ # Shared behavior for tool call parsing across different API formats.
9
+ # Include this module and define `name` and `arguments_string` accessors.
10
+ module ToolCallBase
11
+ # Parse the arguments JSON string into a Ruby hash
12
+ def arguments
13
+ @arguments ||= begin
14
+ JSON.parse(arguments_string)
15
+ rescue JSON::ParserError => e
16
+ raise ToolCallError, "Failed to parse tool call arguments: #{e.message}"
17
+ end
18
+ end
19
+
20
+ # Execute the tool call with a provided block
21
+ # The block receives (name, arguments) and should return the result
22
+ #
23
+ # @yield [name, arguments] Block to execute the tool
24
+ # @return [ToolResultBase] The result of execution
25
+ def execute(&block)
26
+ raise ArgumentError, "Block required for tool execution" unless block_given?
27
+
28
+ begin
29
+ result = block.call(name, arguments)
30
+ build_result(result)
31
+ rescue StandardError => e
32
+ build_result(nil, e.message)
33
+ end
34
+ end
35
+
36
+ # Subclasses must implement this to return the appropriate result type
37
+ def build_result(_result, _error = nil)
38
+ raise NotImplementedError, "Subclasses must implement build_result"
39
+ end
40
+ end
41
+
42
+ # Shared behavior for tool execution results.
43
+ # Include this module and define `tool_call`, `result`, and `error` accessors.
44
+ module ToolResultBase
45
+ def success?
46
+ error.nil?
47
+ end
48
+
49
+ def failure?
50
+ !success?
51
+ end
52
+
53
+ # Factory methods for creating tool results
54
+ module ClassMethods
55
+ # Create a failed result
56
+ def failure(tool_call, error)
57
+ new(tool_call, nil, error)
58
+ end
59
+
60
+ # Create a successful result
61
+ def success(tool_call, result)
62
+ new(tool_call, result, nil)
63
+ end
64
+ end
65
+
66
+ def self.included(base)
67
+ base.extend(ClassMethods)
68
+ end
69
+ end
70
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenRouter
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.1"
5
5
  end
data/lib/open_router.rb CHANGED
@@ -18,10 +18,13 @@ end
18
18
 
19
19
  require_relative "open_router/http"
20
20
  require_relative "open_router/tool"
21
+ require_relative "open_router/tool_call_base"
21
22
  require_relative "open_router/tool_call"
22
23
  require_relative "open_router/schema"
23
24
  require_relative "open_router/json_healer"
24
25
  require_relative "open_router/response"
26
+ require_relative "open_router/responses_response"
27
+ require_relative "open_router/responses_tool_call"
25
28
  require_relative "open_router/model_registry"
26
29
  require_relative "open_router/model_selector"
27
30
  require_relative "open_router/prompt_template"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: open_router_enhanced
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Stiens
@@ -131,6 +131,7 @@ files:
131
131
  - docs/observability.md
132
132
  - docs/plugins.md
133
133
  - docs/prompt_templates.md
134
+ - docs/responses_api.md
134
135
  - docs/streaming.md
135
136
  - docs/structured_outputs.md
136
137
  - docs/tools.md
@@ -150,10 +151,13 @@ files:
150
151
  - lib/open_router/model_selector.rb
151
152
  - lib/open_router/prompt_template.rb
152
153
  - lib/open_router/response.rb
154
+ - lib/open_router/responses_response.rb
155
+ - lib/open_router/responses_tool_call.rb
153
156
  - lib/open_router/schema.rb
154
157
  - lib/open_router/streaming_client.rb
155
158
  - lib/open_router/tool.rb
156
159
  - lib/open_router/tool_call.rb
160
+ - lib/open_router/tool_call_base.rb
157
161
  - lib/open_router/usage_tracker.rb
158
162
  - lib/open_router/version.rb
159
163
  - sig/open_router.rbs