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 +4 -4
- data/CHANGELOG.md +21 -0
- data/Gemfile.lock +32 -33
- data/docs/responses_api.md +298 -0
- data/lib/open_router/client.rb +72 -1
- data/lib/open_router/responses_response.rb +192 -0
- data/lib/open_router/responses_tool_call.rb +95 -0
- data/lib/open_router/tool_call.rb +13 -59
- data/lib/open_router/tool_call_base.rb +69 -0
- data/lib/open_router/version.rb +1 -1
- data/lib/open_router.rb +3 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e39d5924a6388355a4ecae7a76933bf429546a3a5da1975d5dd020804735d24
|
|
4
|
+
data.tar.gz: 220e9504b58ad511e971a8c4a13939edef2c390f0ffeb031e21dfdb30ce78941
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
28
|
-
public_suffix (>= 2.0.2, <
|
|
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
|
-
|
|
32
|
-
bigdecimal (3.3.0)
|
|
31
|
+
bigdecimal (4.0.1)
|
|
33
32
|
coderay (1.1.3)
|
|
34
|
-
concurrent-ruby (1.3.
|
|
35
|
-
connection_pool (
|
|
36
|
-
crack (1.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.
|
|
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.
|
|
49
|
-
net-http (
|
|
47
|
+
faraday-net_http (3.4.2)
|
|
48
|
+
net-http (~> 0.5)
|
|
50
49
|
hashdiff (1.2.1)
|
|
51
|
-
i18n (1.14.
|
|
50
|
+
i18n (1.14.8)
|
|
52
51
|
concurrent-ruby (~> 1.0)
|
|
53
|
-
json (2.
|
|
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 (
|
|
59
|
+
minitest (6.0.0)
|
|
60
|
+
prism (~> 1.5)
|
|
61
61
|
multipart-post (2.4.1)
|
|
62
|
-
net-http (0.
|
|
63
|
-
uri
|
|
62
|
+
net-http (0.9.1)
|
|
63
|
+
uri (>= 0.11.1)
|
|
64
64
|
parallel (1.27.0)
|
|
65
|
-
parser (3.3.
|
|
65
|
+
parser (3.3.10.0)
|
|
66
66
|
ast (~> 2.4.1)
|
|
67
67
|
racc
|
|
68
|
-
prism (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 (
|
|
72
|
+
public_suffix (7.0.0)
|
|
73
73
|
racc (1.8.1)
|
|
74
74
|
rainbow (3.1.1)
|
|
75
|
-
rake (13.3.
|
|
75
|
+
rake (13.3.1)
|
|
76
76
|
regexp_parser (2.11.3)
|
|
77
77
|
rexml (3.4.4)
|
|
78
|
-
rspec (3.13.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
112
|
-
uri (1.
|
|
113
|
-
vcr (6.
|
|
114
|
-
|
|
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
|
data/lib/open_router/client.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
21
|
+
# Get the function name
|
|
31
22
|
def name
|
|
32
23
|
@function_name
|
|
33
24
|
end
|
|
34
25
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
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
|
data/lib/open_router/version.rb
CHANGED
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.
|
|
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
|