open_router_enhanced 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a260aa7386fa0f23bc13f963953db3b35f560b18bae0d39c93e4aa8c28562837
4
- data.tar.gz: af442633daa38c99141fbda7b27d5708f101338c98375863fafd56425b1bdf21
3
+ metadata.gz: 1e39d5924a6388355a4ecae7a76933bf429546a3a5da1975d5dd020804735d24
4
+ data.tar.gz: 220e9504b58ad511e971a8c4a13939edef2c390f0ffeb031e21dfdb30ce78941
5
5
  SHA512:
6
- metadata.gz: 2eef8670903a1b78b17731ab9cc6b75db36d4894c7f1b2361e7978e910c7a5e48e3bdb490a681ace1043d83460a65ab79ba4a2084b1478ea37f0eae8d25d0c40
7
- data.tar.gz: cd027929c19afd7fb18a042a9c097e5f052aaecf4567be972edd564158e56bd4d2e76feac6b68b4f89b27b6b2d70105e80c93bf659f5cba0862d30aa4dba877c
6
+ metadata.gz: 61f753aedc0057758a7cb8310ba06b5751da07ca17ed6bd433473e8ce36184286a37060cd32b2595ac9a8b5e9c798cd2db078ae5c7ddf2232c4b6c271d44623a
7
+ data.tar.gz: bb9f8c9122ddc7fc1c850b34a76ee9e209ca14ff95ff4fe2dbd68b8fccdf10021f78111cd994c0e0f3a163ee61512434b39410227ec818cbf52a1cf22aac5ff6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.2.0] - 2025-12-24
4
+
5
+ ### Added
6
+ - **Responses API**: Full support for OpenRouter's Responses API Beta (`/api/v1/responses`)
7
+ - Simple string or structured array input
8
+ - Reasoning with configurable effort levels (`minimal`, `low`, `medium`, `high`)
9
+ - `ResponsesResponse` wrapper with convenient accessors
10
+ - **Responses API Tool Calling**: Complete function calling support for Responses API
11
+ - `ResponsesToolCall` and `ResponsesToolResult` classes
12
+ - `execute_tool_calls` for easy tool execution with blocks
13
+ - `build_follow_up_input` for multi-turn tool conversations
14
+ - `tool_choice` parameter (`auto`, `required`, `none`)
15
+ - Automatic format conversion from Chat Completions tool format
16
+ - **Shared Tool Call Infrastructure**: Extracted `ToolCallBase` and `ToolResultBase` modules
17
+ - DRY shared behavior for argument parsing and execution
18
+ - Consistent interface across Chat Completions and Responses APIs
19
+
20
+ ### Documentation
21
+ - New `docs/responses_api.md` with comprehensive Responses API guide
22
+ - Tool calling examples with Tool DSL and hash formats
23
+
3
24
  ## [1.1.0] - 2025-12-24
4
25
 
5
26
  ### 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.0)
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
@@ -145,6 +145,48 @@ module OpenRouter
145
145
  response["data"]
146
146
  end
147
147
 
148
+ # Performs a request to the Responses API Beta (/api/v1/responses)
149
+ # This is an OpenAI-compatible stateless API with support for reasoning.
150
+ #
151
+ # @param input [String, Array] The input text or structured message array
152
+ # @param model [String] Model identifier (e.g., "openai/o4-mini")
153
+ # @param reasoning [Hash, nil] Optional reasoning config, e.g. {effort: "high"}
154
+ # Effort levels: "minimal", "low", "medium", "high"
155
+ # @param tools [Array<Tool, Hash>] Optional array of tool definitions
156
+ # @param tool_choice [String, Hash, nil] Optional: "auto", "none", "required", or specific tool
157
+ # @param max_output_tokens [Integer, nil] Maximum tokens to generate
158
+ # @param temperature [Float, nil] Sampling temperature (0-2)
159
+ # @param top_p [Float, nil] Nucleus sampling parameter (0-1)
160
+ # @param extras [Hash] Additional parameters to pass to the API
161
+ # @return [ResponsesResponse] The response wrapped in a ResponsesResponse object
162
+ #
163
+ # @example Basic usage
164
+ # response = client.responses("What is 2+2?", model: "openai/o4-mini")
165
+ # puts response.content
166
+ #
167
+ # @example With reasoning
168
+ # response = client.responses(
169
+ # "Solve this step by step: What is 15% of 80?",
170
+ # model: "openai/o4-mini",
171
+ # reasoning: { effort: "high" }
172
+ # )
173
+ # puts response.reasoning_summary
174
+ # puts response.content
175
+ def responses(input, model:, reasoning: nil, tools: [], tool_choice: nil,
176
+ max_output_tokens: nil, temperature: nil, top_p: nil, extras: {})
177
+ parameters = { model: model, input: input }
178
+ parameters[:reasoning] = reasoning if reasoning
179
+ parameters[:tools] = serialize_tools_for_responses(tools) if tools.any?
180
+ parameters[:tool_choice] = tool_choice if tool_choice
181
+ parameters[:max_output_tokens] = max_output_tokens if max_output_tokens
182
+ parameters[:temperature] = temperature if temperature
183
+ parameters[:top_p] = top_p if top_p
184
+ parameters.merge!(extras)
185
+
186
+ raw = post(path: "/responses", parameters: parameters)
187
+ ResponsesResponse.new(raw)
188
+ end
189
+
148
190
  # Create a new ModelSelector for intelligent model selection
149
191
  #
150
192
  # @return [ModelSelector] A new ModelSelector instance
@@ -510,7 +552,8 @@ module OpenRouter
510
552
  end
511
553
  end
512
554
 
513
- # Serialize tools to the format expected by OpenRouter API
555
+ # Serialize tools to the format expected by OpenRouter Chat Completions API
556
+ # Format: { type: "function", function: { name: ..., parameters: ... } }
514
557
  def serialize_tools(tools)
515
558
  tools.map do |tool|
516
559
  case tool
@@ -524,6 +567,34 @@ module OpenRouter
524
567
  end
525
568
  end
526
569
 
570
+ # Serialize tools to the flat format expected by Responses API
571
+ # Format: { type: "function", name: ..., parameters: ... }
572
+ def serialize_tools_for_responses(tools)
573
+ tools.map do |tool|
574
+ tool_hash = case tool
575
+ when Tool
576
+ tool.to_h
577
+ when Hash
578
+ tool.transform_keys(&:to_sym)
579
+ else
580
+ raise ArgumentError, "Tools must be Tool objects or hashes"
581
+ end
582
+
583
+ # Flatten the nested function structure if present
584
+ if tool_hash[:function]
585
+ {
586
+ type: "function",
587
+ name: tool_hash[:function][:name],
588
+ description: tool_hash[:function][:description],
589
+ parameters: tool_hash[:function][:parameters]
590
+ }.compact
591
+ else
592
+ # Already in flat format
593
+ tool_hash
594
+ end
595
+ end
596
+ end
597
+
527
598
  # Serialize response format to the format expected by OpenRouter API
528
599
  def serialize_response_format(response_format)
529
600
  case response_format
@@ -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 has_reasoning?
52
+ !reasoning_output.nil?
53
+ end
54
+
55
+ # Get tool/function calls from the response as ResponsesToolCall objects
56
+ #
57
+ # @return [Array<ResponsesToolCall>] Array of tool call objects
58
+ def tool_calls
59
+ @tool_calls ||= output
60
+ .select { |o| o["type"] == "function_call" }
61
+ .map { |tc| ResponsesToolCall.new(tc) }
62
+ end
63
+
64
+ # Get raw tool call data (hashes) from the response
65
+ #
66
+ # @return [Array<Hash>] Array of raw tool call hashes
67
+ def tool_calls_raw
68
+ output.select { |o| o["type"] == "function_call" }
69
+ end
70
+
71
+ def has_tool_calls?
72
+ tool_calls.any?
73
+ end
74
+
75
+ # Execute all tool calls and return results
76
+ #
77
+ # @yield [name, arguments] Block to execute each tool
78
+ # @return [Array<ResponsesToolResult>] Results from all tool executions
79
+ #
80
+ # @example
81
+ # results = response.execute_tool_calls do |name, args|
82
+ # case name
83
+ # when "get_weather" then fetch_weather(args["location"])
84
+ # when "search" then search_web(args["query"])
85
+ # end
86
+ # end
87
+ def execute_tool_calls(&block)
88
+ tool_calls.map { |tc| tc.execute(&block) }
89
+ end
90
+
91
+ # Build a follow-up input array that includes tool results
92
+ # Use this to continue the conversation after executing tools
93
+ #
94
+ # @param original_input [String, Array] The original input sent to the API
95
+ # @param tool_results [Array<ResponsesToolResult>] Results from execute_tool_calls
96
+ # @param follow_up_message [String, nil] Optional follow-up user message
97
+ # @return [Array] Input array for the next API call
98
+ #
99
+ # @example
100
+ # # First call with tools
101
+ # response = client.responses("What's the weather?", model: "...", tools: [...])
102
+ #
103
+ # # Execute tools
104
+ # results = response.execute_tool_calls { |name, args| ... }
105
+ #
106
+ # # Build follow-up input
107
+ # next_input = response.build_follow_up_input(
108
+ # original_input: "What's the weather?",
109
+ # tool_results: results,
110
+ # follow_up_message: "Is that good for a picnic?"
111
+ # )
112
+ #
113
+ # # Continue conversation
114
+ # next_response = client.responses(next_input, model: "...")
115
+ def build_follow_up_input(original_input:, tool_results:, follow_up_message: nil)
116
+ input_items = []
117
+
118
+ # Add original user message
119
+ if original_input.is_a?(String)
120
+ input_items << {
121
+ "type" => "message",
122
+ "role" => "user",
123
+ "content" => [{ "type" => "input_text", "text" => original_input }]
124
+ }
125
+ elsif original_input.is_a?(Array)
126
+ input_items.concat(original_input)
127
+ end
128
+
129
+ # Add function calls from this response
130
+ tool_calls_raw.each do |tc|
131
+ input_items << tc
132
+ end
133
+
134
+ # Add function call outputs
135
+ tool_results.each do |result|
136
+ input_items << result.to_input_item
137
+ end
138
+
139
+ # Add assistant message if present
140
+ if message_output
141
+ input_items << message_output
142
+ end
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.dig("input_tokens") || 0
159
+ end
160
+
161
+ def output_tokens
162
+ usage.dig("output_tokens") || 0
163
+ end
164
+
165
+ def total_tokens
166
+ usage.dig("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
+ output.find { |o| o["type"] == "message" }
186
+ end
187
+
188
+ def reasoning_output
189
+ output.find { |o| o["type"] == "reasoning" }
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,95 @@
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
+ def name
22
+ @name
23
+ end
24
+
25
+ # Alias for consistency with ToolCall
26
+ def function_name
27
+ @name
28
+ end
29
+
30
+ # Build result for execute method (required by ToolCallBase)
31
+ def build_result(result, error = nil)
32
+ ResponsesToolResult.new(self, result, error)
33
+ end
34
+
35
+ # Convert to the function_call format for conversation continuation
36
+ def to_input_item
37
+ {
38
+ "type" => "function_call",
39
+ "id" => @id,
40
+ "call_id" => @call_id,
41
+ "name" => @name,
42
+ "arguments" => @arguments_string
43
+ }
44
+ end
45
+
46
+ def to_h
47
+ to_input_item
48
+ end
49
+
50
+ def to_json(*args)
51
+ to_h.to_json(*args)
52
+ end
53
+ end
54
+
55
+ # Represents the result of executing a Responses API tool call
56
+ class ResponsesToolResult
57
+ include ToolResultBase
58
+
59
+ attr_reader :tool_call, :result, :error
60
+
61
+ def initialize(tool_call, result = nil, error = nil)
62
+ @tool_call = tool_call
63
+ @result = result
64
+ @error = error
65
+ end
66
+
67
+ # Convert to function_call_output format for conversation continuation
68
+ #
69
+ # @return [Hash] The output item for the input array
70
+ def to_input_item
71
+ output_content = if @error
72
+ { error: @error }.to_json
73
+ elsif @result.is_a?(String)
74
+ @result
75
+ else
76
+ @result.to_json
77
+ end
78
+
79
+ {
80
+ "type" => "function_call_output",
81
+ "id" => "fc_output_#{SecureRandom.hex(8)}",
82
+ "call_id" => @tool_call.call_id,
83
+ "output" => output_content
84
+ }
85
+ end
86
+
87
+ def to_h
88
+ to_input_item
89
+ end
90
+
91
+ def to_json(*args)
92
+ to_h.to_json(*args)
93
+ end
94
+ end
95
+ 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,69 @@
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
+ module ClassMethods
54
+ # Create a failed result
55
+ def failure(tool_call, error)
56
+ new(tool_call, nil, error)
57
+ end
58
+
59
+ # Create a successful result
60
+ def success(tool_call, result)
61
+ new(tool_call, result, nil)
62
+ end
63
+ end
64
+
65
+ def self.included(base)
66
+ base.extend(ClassMethods)
67
+ end
68
+ end
69
+ 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.0"
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.0
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