open_router_enhanced 1.2.2 → 1.2.3
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/.rubocop_todo.yml +33 -15
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +1 -1
- data/docs/tools.md +234 -1
- data/examples/dynamic_model_switching_example.rb +328 -0
- data/examples/real_world_schemas_example.rb +262 -0
- data/examples/responses_api_example.rb +324 -0
- data/examples/tool_loop_example.rb +317 -0
- data/lib/open_router/streaming_client.rb +8 -4
- data/lib/open_router/version.rb +1 -1
- metadata +6 -2
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Responses API Multi-Turn Tool Loop Example
|
|
5
|
+
# ===========================================
|
|
6
|
+
# This example demonstrates the Responses API, which provides a streamlined
|
|
7
|
+
# way to handle multi-turn tool conversations with automatic input building.
|
|
8
|
+
#
|
|
9
|
+
# The Responses API differs from Chat Completions:
|
|
10
|
+
# - Uses `output` array with typed items instead of `choices`
|
|
11
|
+
# - Provides `build_follow_up_input()` for easy conversation continuation
|
|
12
|
+
# - Tool calls have a flat structure (not nested under `function`)
|
|
13
|
+
# - Supports reasoning output as a first-class type
|
|
14
|
+
#
|
|
15
|
+
# Run with: ruby -I lib examples/responses_api_example.rb
|
|
16
|
+
|
|
17
|
+
require "open_router"
|
|
18
|
+
require "json"
|
|
19
|
+
|
|
20
|
+
# Configure the client
|
|
21
|
+
OpenRouter.configure do |config|
|
|
22
|
+
config.access_token = ENV.fetch("OPENROUTER_API_KEY") do
|
|
23
|
+
abort "Please set OPENROUTER_API_KEY environment variable"
|
|
24
|
+
end
|
|
25
|
+
config.site_name = "Responses API Examples"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
client = OpenRouter::Client.new
|
|
29
|
+
|
|
30
|
+
# Use a model that supports the Responses API
|
|
31
|
+
MODEL = "openai/gpt-4o-mini"
|
|
32
|
+
|
|
33
|
+
puts "=" * 60
|
|
34
|
+
puts "RESPONSES API TOOL LOOP EXAMPLE"
|
|
35
|
+
puts "=" * 60
|
|
36
|
+
|
|
37
|
+
# -----------------------------------------------------------------------------
|
|
38
|
+
# Define Our Tools
|
|
39
|
+
# -----------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
calculator_tool = OpenRouter::Tool.define do
|
|
42
|
+
name "calculate"
|
|
43
|
+
description "Perform mathematical calculations"
|
|
44
|
+
parameters do
|
|
45
|
+
string :expression, required: true, description: "Math expression to evaluate"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
weather_tool = OpenRouter::Tool.define do
|
|
50
|
+
name "get_weather"
|
|
51
|
+
description "Get current weather for a location"
|
|
52
|
+
parameters do
|
|
53
|
+
string :location, required: true, description: "City name"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
stock_tool = OpenRouter::Tool.define do
|
|
58
|
+
name "get_stock_price"
|
|
59
|
+
description "Get current stock price for a ticker symbol"
|
|
60
|
+
parameters do
|
|
61
|
+
string :symbol, required: true, description: "Stock ticker symbol (e.g., AAPL, GOOGL)"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
TOOLS = [calculator_tool, weather_tool, stock_tool].freeze
|
|
66
|
+
|
|
67
|
+
# -----------------------------------------------------------------------------
|
|
68
|
+
# Tool Execution Function
|
|
69
|
+
# -----------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def execute_tool(name, args)
|
|
72
|
+
case name
|
|
73
|
+
when "calculate"
|
|
74
|
+
expr = args["expression"]
|
|
75
|
+
# Handle percentage expressions like "15% of 378.90"
|
|
76
|
+
if expr =~ /(\d+(?:\.\d+)?)\s*%\s*of\s*(\d+(?:\.\d+)?)/i
|
|
77
|
+
result = (Regexp.last_match(1).to_f / 100) * Regexp.last_match(2).to_f
|
|
78
|
+
return { result: result.round(2), expression: expr }
|
|
79
|
+
end
|
|
80
|
+
# Standard math expression
|
|
81
|
+
expression = expr.gsub(%r{[^0-9+\-*/.()\s]}, "")
|
|
82
|
+
begin
|
|
83
|
+
{ result: eval(expression), expression: expr }
|
|
84
|
+
rescue StandardError
|
|
85
|
+
{ error: "Invalid expression" }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
when "get_weather"
|
|
89
|
+
location = args["location"]
|
|
90
|
+
temp = rand(10..35)
|
|
91
|
+
{
|
|
92
|
+
location: location,
|
|
93
|
+
temperature_celsius: temp,
|
|
94
|
+
conditions: %w[sunny cloudy rainy partly_cloudy].sample,
|
|
95
|
+
humidity: rand(30..90)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
when "get_stock_price"
|
|
99
|
+
symbol = args["symbol"].upcase
|
|
100
|
+
prices = { "AAPL" => 178.50, "GOOGL" => 141.20, "MSFT" => 378.90, "AMZN" => 185.30 }
|
|
101
|
+
price = prices[symbol] || (100 + rand * 200).round(2)
|
|
102
|
+
{
|
|
103
|
+
symbol: symbol,
|
|
104
|
+
price: price,
|
|
105
|
+
currency: "USD",
|
|
106
|
+
change: (rand * 10 - 5).round(2)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
else
|
|
110
|
+
{ error: "Unknown tool: #{name}" }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# -----------------------------------------------------------------------------
|
|
115
|
+
# Example 1: Basic Responses API Call
|
|
116
|
+
# -----------------------------------------------------------------------------
|
|
117
|
+
puts "\n1. BASIC RESPONSES API CALL"
|
|
118
|
+
puts "-" * 40
|
|
119
|
+
|
|
120
|
+
response = client.responses(
|
|
121
|
+
"What is 25 times 17?",
|
|
122
|
+
model: MODEL,
|
|
123
|
+
tools: TOOLS
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
puts "Response ID: #{response.id}"
|
|
127
|
+
puts "Model: #{response.model}"
|
|
128
|
+
puts "Status: #{response.status}"
|
|
129
|
+
|
|
130
|
+
if response.has_tool_calls?
|
|
131
|
+
puts "\nTool calls requested:"
|
|
132
|
+
response.tool_calls.each do |tc|
|
|
133
|
+
puts " - #{tc.name}: #{tc.arguments}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# -----------------------------------------------------------------------------
|
|
138
|
+
# Example 2: execute_tool_calls with Block
|
|
139
|
+
# -----------------------------------------------------------------------------
|
|
140
|
+
puts "\n\n2. EXECUTE_TOOL_CALLS WITH BLOCK"
|
|
141
|
+
puts "-" * 40
|
|
142
|
+
|
|
143
|
+
response = client.responses(
|
|
144
|
+
"What's the weather in Seattle?",
|
|
145
|
+
model: MODEL,
|
|
146
|
+
tools: TOOLS
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if response.has_tool_calls?
|
|
150
|
+
# Execute all tool calls with a single block
|
|
151
|
+
results = response.execute_tool_calls do |name, args|
|
|
152
|
+
puts " Executing: #{name}(#{args})"
|
|
153
|
+
execute_tool(name, args)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
puts "\nResults:"
|
|
157
|
+
results.each do |result|
|
|
158
|
+
if result.success?
|
|
159
|
+
puts " Success: #{result.result}"
|
|
160
|
+
else
|
|
161
|
+
puts " Error: #{result.error}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# -----------------------------------------------------------------------------
|
|
167
|
+
# Example 3: Multi-Turn with build_follow_up_input
|
|
168
|
+
# -----------------------------------------------------------------------------
|
|
169
|
+
puts "\n\n3. MULTI-TURN WITH BUILD_FOLLOW_UP_INPUT"
|
|
170
|
+
puts "-" * 40
|
|
171
|
+
|
|
172
|
+
original_query = "What's the weather in Tokyo and what's Apple stock at?"
|
|
173
|
+
puts "User: #{original_query}"
|
|
174
|
+
|
|
175
|
+
# First request
|
|
176
|
+
response = client.responses(
|
|
177
|
+
original_query,
|
|
178
|
+
model: MODEL,
|
|
179
|
+
tools: TOOLS
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if response.has_tool_calls?
|
|
183
|
+
puts "\nTool calls (Round 1):"
|
|
184
|
+
response.tool_calls.each do |tc|
|
|
185
|
+
puts " - #{tc.name}: #{tc.arguments}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Execute the tools
|
|
189
|
+
results = response.execute_tool_calls do |name, args|
|
|
190
|
+
execute_tool(name, args)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
puts "\nExecuted tools, building follow-up..."
|
|
194
|
+
|
|
195
|
+
# Build the follow-up input automatically
|
|
196
|
+
follow_up_input = response.build_follow_up_input(
|
|
197
|
+
original_input: original_query,
|
|
198
|
+
tool_results: results
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
puts "Follow-up input has #{follow_up_input.length} items"
|
|
202
|
+
|
|
203
|
+
# Continue the conversation
|
|
204
|
+
final_response = client.responses(
|
|
205
|
+
follow_up_input,
|
|
206
|
+
model: MODEL,
|
|
207
|
+
tools: TOOLS
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if final_response.has_tool_calls?
|
|
211
|
+
puts "\nMore tool calls requested (Round 2)..."
|
|
212
|
+
else
|
|
213
|
+
puts "\nAssistant: #{final_response.content}"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# -----------------------------------------------------------------------------
|
|
218
|
+
# Example 4: Complete Multi-Turn Loop Until Done
|
|
219
|
+
# -----------------------------------------------------------------------------
|
|
220
|
+
puts "\n\n4. COMPLETE MULTI-TURN LOOP"
|
|
221
|
+
puts "-" * 40
|
|
222
|
+
|
|
223
|
+
query = "I need to: 1) Check the weather in London, 2) Get Microsoft stock price, 3) Calculate 15% of $378.90"
|
|
224
|
+
puts "User: #{query}"
|
|
225
|
+
|
|
226
|
+
current_input = query
|
|
227
|
+
max_rounds = 5
|
|
228
|
+
round = 0
|
|
229
|
+
|
|
230
|
+
loop do
|
|
231
|
+
round += 1
|
|
232
|
+
puts "\n[Round #{round}]"
|
|
233
|
+
|
|
234
|
+
response = client.responses(
|
|
235
|
+
current_input,
|
|
236
|
+
model: MODEL,
|
|
237
|
+
tools: TOOLS
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if response.has_tool_calls?
|
|
241
|
+
puts "Tool calls:"
|
|
242
|
+
response.tool_calls.each { |tc| puts " - #{tc.name}(#{tc.arguments})" }
|
|
243
|
+
|
|
244
|
+
# Execute tools
|
|
245
|
+
results = response.execute_tool_calls do |name, args|
|
|
246
|
+
result = execute_tool(name, args)
|
|
247
|
+
puts " -> #{result.to_json[0..60]}..."
|
|
248
|
+
result
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Build next input
|
|
252
|
+
current_input = response.build_follow_up_input(
|
|
253
|
+
original_input: current_input.is_a?(String) ? current_input : current_input,
|
|
254
|
+
tool_results: results
|
|
255
|
+
)
|
|
256
|
+
else
|
|
257
|
+
# Final response - no more tool calls
|
|
258
|
+
puts "\nFinal Answer:"
|
|
259
|
+
puts response.content
|
|
260
|
+
break
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
if round >= max_rounds
|
|
264
|
+
puts "\n[Max rounds reached]"
|
|
265
|
+
break
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
puts "\nToken usage: #{response.total_tokens} total (#{response.input_tokens} in, #{response.output_tokens} out)"
|
|
270
|
+
|
|
271
|
+
# -----------------------------------------------------------------------------
|
|
272
|
+
# Example 5: Adding Follow-Up Questions
|
|
273
|
+
# -----------------------------------------------------------------------------
|
|
274
|
+
puts "\n\n5. FOLLOW-UP QUESTIONS"
|
|
275
|
+
puts "-" * 40
|
|
276
|
+
|
|
277
|
+
original = "What's the current price of Google stock?"
|
|
278
|
+
puts "User: #{original}"
|
|
279
|
+
|
|
280
|
+
response = client.responses(original, model: MODEL, tools: TOOLS)
|
|
281
|
+
|
|
282
|
+
if response.has_tool_calls?
|
|
283
|
+
results = response.execute_tool_calls { |name, args| execute_tool(name, args) }
|
|
284
|
+
|
|
285
|
+
# Build follow-up WITH an additional question
|
|
286
|
+
follow_up = response.build_follow_up_input(
|
|
287
|
+
original_input: original,
|
|
288
|
+
tool_results: results,
|
|
289
|
+
follow_up_message: "Is that higher or lower than last week?"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
next_response = client.responses(follow_up, model: MODEL, tools: TOOLS)
|
|
293
|
+
puts "\nAssistant: #{next_response.content}"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# -----------------------------------------------------------------------------
|
|
297
|
+
# Example 6: Comparing Responses API vs Chat Completions
|
|
298
|
+
# -----------------------------------------------------------------------------
|
|
299
|
+
puts "\n\n6. RESPONSES API vs CHAT COMPLETIONS COMPARISON"
|
|
300
|
+
puts "-" * 40
|
|
301
|
+
|
|
302
|
+
puts <<~COMPARISON
|
|
303
|
+
Key differences:
|
|
304
|
+
|
|
305
|
+
CHAT COMPLETIONS API:
|
|
306
|
+
- response["choices"][0]["message"]["content"]
|
|
307
|
+
- Tool calls under: choices[0].message.tool_calls[].function.{name, arguments}
|
|
308
|
+
- Manual message construction for continuation
|
|
309
|
+
- Use: response.to_message + tool_call.to_result_message(result)
|
|
310
|
+
|
|
311
|
+
RESPONSES API:
|
|
312
|
+
- response.content (or response["output"])
|
|
313
|
+
- Tool calls at: output[] where type="function_call"
|
|
314
|
+
- Automatic continuation with: response.build_follow_up_input()
|
|
315
|
+
- Use: response.execute_tool_calls { |name, args| ... }
|
|
316
|
+
|
|
317
|
+
When to use each:
|
|
318
|
+
- Chat Completions: Standard chat, fine-grained control, streaming
|
|
319
|
+
- Responses API: Multi-turn tool loops, reasoning output, simpler code
|
|
320
|
+
COMPARISON
|
|
321
|
+
|
|
322
|
+
puts "\n#{"=" * 60}"
|
|
323
|
+
puts "Examples complete!"
|
|
324
|
+
puts "=" * 60
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Tool Calling Loop Example (Chat Completions API)
|
|
5
|
+
# =================================================
|
|
6
|
+
# This example demonstrates a complete tool calling workflow using the
|
|
7
|
+
# Chat Completions API, including multi-tool calls and conversation continuation.
|
|
8
|
+
#
|
|
9
|
+
# Run with: ruby -I lib examples/tool_loop_example.rb
|
|
10
|
+
|
|
11
|
+
require "open_router"
|
|
12
|
+
require "json"
|
|
13
|
+
|
|
14
|
+
# Configure the client
|
|
15
|
+
OpenRouter.configure do |config|
|
|
16
|
+
config.access_token = ENV.fetch("OPENROUTER_API_KEY") do
|
|
17
|
+
abort "Please set OPENROUTER_API_KEY environment variable"
|
|
18
|
+
end
|
|
19
|
+
config.site_name = "Tool Loop Examples"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
client = OpenRouter::Client.new
|
|
23
|
+
|
|
24
|
+
# Use a model with function calling support
|
|
25
|
+
MODEL = "openai/gpt-4o-mini"
|
|
26
|
+
|
|
27
|
+
puts "=" * 60
|
|
28
|
+
puts "TOOL CALLING LOOP EXAMPLE"
|
|
29
|
+
puts "=" * 60
|
|
30
|
+
|
|
31
|
+
# -----------------------------------------------------------------------------
|
|
32
|
+
# Define Our Tools
|
|
33
|
+
# -----------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
# Calculator tool for math operations
|
|
36
|
+
calculator_tool = OpenRouter::Tool.define do
|
|
37
|
+
name "calculate"
|
|
38
|
+
description "Perform mathematical calculations. Use this for any math operations."
|
|
39
|
+
|
|
40
|
+
parameters do
|
|
41
|
+
string :expression, required: true, description: "Mathematical expression to evaluate (e.g., '2 + 2', '15 * 7')"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Weather lookup tool
|
|
46
|
+
weather_tool = OpenRouter::Tool.define do
|
|
47
|
+
name "get_weather"
|
|
48
|
+
description "Get current weather for a location"
|
|
49
|
+
|
|
50
|
+
parameters do
|
|
51
|
+
string :location, required: true, description: "City name (e.g., 'San Francisco', 'London')"
|
|
52
|
+
string :units, enum: %w[celsius fahrenheit], description: "Temperature units"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Search tool
|
|
57
|
+
search_tool = OpenRouter::Tool.define do
|
|
58
|
+
name "search"
|
|
59
|
+
description "Search for information on a topic"
|
|
60
|
+
|
|
61
|
+
parameters do
|
|
62
|
+
string :query, required: true, description: "Search query"
|
|
63
|
+
integer :max_results, description: "Maximum number of results (default: 3)"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
TOOLS = [calculator_tool, weather_tool, search_tool].freeze
|
|
68
|
+
|
|
69
|
+
# -----------------------------------------------------------------------------
|
|
70
|
+
# Tool Execution Functions (simulated)
|
|
71
|
+
# -----------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def execute_tool(name, args)
|
|
74
|
+
case name
|
|
75
|
+
when "calculate"
|
|
76
|
+
expr = args["expression"]
|
|
77
|
+
# Handle percentage expressions like "15% of 378.90"
|
|
78
|
+
if expr =~ /(\d+(?:\.\d+)?)\s*%\s*of\s*(\d+(?:\.\d+)?)/i
|
|
79
|
+
result = (Regexp.last_match(1).to_f / 100) * Regexp.last_match(2).to_f
|
|
80
|
+
return { result: result.round(2), expression: expr }
|
|
81
|
+
end
|
|
82
|
+
# Standard math expression - safe eval for basic math
|
|
83
|
+
expression = expr.gsub(%r{[^0-9+\-*/.()\s]}, "")
|
|
84
|
+
result = begin
|
|
85
|
+
eval(expression)
|
|
86
|
+
rescue StandardError
|
|
87
|
+
"Error: Invalid expression"
|
|
88
|
+
end
|
|
89
|
+
{ result: result, expression: expr }
|
|
90
|
+
|
|
91
|
+
when "get_weather"
|
|
92
|
+
# Simulated weather data
|
|
93
|
+
location = args["location"]
|
|
94
|
+
units = args["units"] || "celsius"
|
|
95
|
+
temp = rand(15..30)
|
|
96
|
+
temp = (temp * 9 / 5) + 32 if units == "fahrenheit"
|
|
97
|
+
{
|
|
98
|
+
location: location,
|
|
99
|
+
temperature: temp,
|
|
100
|
+
units: units,
|
|
101
|
+
conditions: %w[sunny cloudy partly_cloudy rainy].sample,
|
|
102
|
+
humidity: rand(40..80)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
when "search"
|
|
106
|
+
# Simulated search results
|
|
107
|
+
query = args["query"]
|
|
108
|
+
max_results = args["max_results"] || 3
|
|
109
|
+
results = max_results.times.map do |i|
|
|
110
|
+
{
|
|
111
|
+
title: "Result #{i + 1} for '#{query}'",
|
|
112
|
+
snippet: "This is a simulated search result about #{query}.",
|
|
113
|
+
url: "https://example.com/result-#{i + 1}"
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
{ query: query, results: results }
|
|
117
|
+
|
|
118
|
+
else
|
|
119
|
+
{ error: "Unknown tool: #{name}" }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# -----------------------------------------------------------------------------
|
|
124
|
+
# Example 1: Simple Single Tool Call
|
|
125
|
+
# -----------------------------------------------------------------------------
|
|
126
|
+
puts "\n1. SIMPLE TOOL CALL"
|
|
127
|
+
puts "-" * 40
|
|
128
|
+
|
|
129
|
+
messages = [
|
|
130
|
+
{ role: "user", content: "What is 15 multiplied by 23?" }
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
puts "User: #{messages.last[:content]}"
|
|
134
|
+
|
|
135
|
+
response = client.complete(messages, model: MODEL, tools: TOOLS)
|
|
136
|
+
|
|
137
|
+
if response.has_tool_calls?
|
|
138
|
+
puts "\nAssistant wants to call tools:"
|
|
139
|
+
response.tool_calls.each do |tc|
|
|
140
|
+
puts " - #{tc.name}(#{tc.arguments})"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Execute the tool calls and collect results
|
|
144
|
+
tool_results = response.tool_calls.map do |tc|
|
|
145
|
+
result = execute_tool(tc.name, tc.arguments)
|
|
146
|
+
puts " -> Result: #{result}"
|
|
147
|
+
tc.to_result_message(result)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Continue the conversation with tool results
|
|
151
|
+
messages << response.to_message
|
|
152
|
+
messages.concat(tool_results)
|
|
153
|
+
|
|
154
|
+
final_response = client.complete(messages, model: MODEL, tools: TOOLS)
|
|
155
|
+
puts "\nAssistant: #{final_response.content}"
|
|
156
|
+
else
|
|
157
|
+
puts "Assistant: #{response.content}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# -----------------------------------------------------------------------------
|
|
161
|
+
# Example 2: Multiple Tool Calls at Once
|
|
162
|
+
# -----------------------------------------------------------------------------
|
|
163
|
+
puts "\n\n2. MULTIPLE TOOL CALLS"
|
|
164
|
+
puts "-" * 40
|
|
165
|
+
|
|
166
|
+
messages = [
|
|
167
|
+
{ role: "user", content: "I'm planning a trip. What's the weather like in Tokyo and Paris? Also, calculate how many hours are in 2 weeks." }
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
puts "User: #{messages.last[:content]}"
|
|
171
|
+
|
|
172
|
+
response = client.complete(messages, model: MODEL, tools: TOOLS)
|
|
173
|
+
|
|
174
|
+
if response.has_tool_calls?
|
|
175
|
+
puts "\nAssistant wants to call #{response.tool_calls.length} tools:"
|
|
176
|
+
response.tool_calls.each do |tc|
|
|
177
|
+
puts " - #{tc.name}(#{tc.arguments})"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Execute all tool calls
|
|
181
|
+
tool_results = response.tool_calls.map do |tc|
|
|
182
|
+
result = execute_tool(tc.name, tc.arguments)
|
|
183
|
+
puts " -> #{tc.name} result: #{result.to_json[0..80]}..."
|
|
184
|
+
tc.to_result_message(result)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Continue conversation
|
|
188
|
+
messages << response.to_message
|
|
189
|
+
messages.concat(tool_results)
|
|
190
|
+
|
|
191
|
+
final_response = client.complete(messages, model: MODEL, tools: TOOLS)
|
|
192
|
+
puts "\nAssistant: #{final_response.content}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# -----------------------------------------------------------------------------
|
|
196
|
+
# Example 3: Multi-Turn Tool Loop
|
|
197
|
+
# -----------------------------------------------------------------------------
|
|
198
|
+
puts "\n\n3. MULTI-TURN TOOL LOOP"
|
|
199
|
+
puts "-" * 40
|
|
200
|
+
|
|
201
|
+
messages = [
|
|
202
|
+
{ role: "system", content: "You are a helpful assistant. Use tools when needed to provide accurate information." },
|
|
203
|
+
{ role: "user", content: "Search for 'Ruby programming' and tell me what you find." }
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
puts "User: #{messages.last[:content]}"
|
|
207
|
+
|
|
208
|
+
max_iterations = 5
|
|
209
|
+
iteration = 0
|
|
210
|
+
|
|
211
|
+
loop do
|
|
212
|
+
iteration += 1
|
|
213
|
+
puts "\n[Iteration #{iteration}]"
|
|
214
|
+
|
|
215
|
+
response = client.complete(messages, model: MODEL, tools: TOOLS)
|
|
216
|
+
|
|
217
|
+
if response.has_tool_calls?
|
|
218
|
+
# Process tool calls
|
|
219
|
+
response.tool_calls.each do |tc|
|
|
220
|
+
puts " Tool call: #{tc.name}(#{JSON.pretty_generate(tc.arguments)[0..50]}...)"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
tool_results = response.tool_calls.map do |tc|
|
|
224
|
+
result = execute_tool(tc.name, tc.arguments)
|
|
225
|
+
tc.to_result_message(result)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Add to conversation
|
|
229
|
+
messages << response.to_message
|
|
230
|
+
messages.concat(tool_results)
|
|
231
|
+
else
|
|
232
|
+
# No more tool calls - we have the final response
|
|
233
|
+
puts "\nAssistant: #{response.content}"
|
|
234
|
+
break
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
if iteration >= max_iterations
|
|
238
|
+
puts "\n[Max iterations reached]"
|
|
239
|
+
break
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# -----------------------------------------------------------------------------
|
|
244
|
+
# Example 4: Error Handling in Tools
|
|
245
|
+
# -----------------------------------------------------------------------------
|
|
246
|
+
puts "\n\n4. ERROR HANDLING"
|
|
247
|
+
puts "-" * 40
|
|
248
|
+
|
|
249
|
+
# Define a tool handler that includes error handling
|
|
250
|
+
def safe_execute_tool(name, args)
|
|
251
|
+
result = execute_tool(name, args)
|
|
252
|
+
{ success: true, data: result }
|
|
253
|
+
rescue StandardError => e
|
|
254
|
+
{ success: false, error: e.message }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
messages = [
|
|
258
|
+
{ role: "user", content: "Calculate the result of: 100 / (5 - 5)" }
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
puts "User: #{messages.last[:content]}"
|
|
262
|
+
|
|
263
|
+
response = client.complete(messages, model: MODEL, tools: TOOLS)
|
|
264
|
+
|
|
265
|
+
if response.has_tool_calls?
|
|
266
|
+
response.tool_calls.each do |tc|
|
|
267
|
+
puts " Tool call: #{tc.name}"
|
|
268
|
+
|
|
269
|
+
# Use the built-in execute method which handles errors
|
|
270
|
+
result = tc.execute do |name, args|
|
|
271
|
+
execute_tool(name, args)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
if result.success?
|
|
275
|
+
puts " -> Success: #{result.result}"
|
|
276
|
+
else
|
|
277
|
+
puts " -> Error: #{result.error}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Continue with the result either way
|
|
281
|
+
messages << response.to_message
|
|
282
|
+
messages << result.to_message
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
final_response = client.complete(messages, model: MODEL, tools: TOOLS)
|
|
286
|
+
puts "\nAssistant: #{final_response.content}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# -----------------------------------------------------------------------------
|
|
290
|
+
# Example 5: Tool Call Validation
|
|
291
|
+
# -----------------------------------------------------------------------------
|
|
292
|
+
puts "\n\n5. TOOL CALL VALIDATION"
|
|
293
|
+
puts "-" * 40
|
|
294
|
+
|
|
295
|
+
messages = [
|
|
296
|
+
{ role: "user", content: "What's the weather in Boston?" }
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
response = client.complete(messages, model: MODEL, tools: TOOLS)
|
|
300
|
+
|
|
301
|
+
if response.has_tool_calls?
|
|
302
|
+
response.tool_calls.each do |tc|
|
|
303
|
+
# Validate the tool call against our tool definitions
|
|
304
|
+
if tc.valid?(tools: TOOLS)
|
|
305
|
+
puts "Tool call '#{tc.name}' is valid"
|
|
306
|
+
puts " Arguments: #{tc.arguments}"
|
|
307
|
+
else
|
|
308
|
+
errors = tc.validation_errors(tools: TOOLS)
|
|
309
|
+
puts "Tool call '#{tc.name}' has validation errors:"
|
|
310
|
+
errors.each { |e| puts " - #{e}" }
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
puts "\n#{"=" * 60}"
|
|
316
|
+
puts "Examples complete!"
|
|
317
|
+
puts "=" * 60
|
|
@@ -36,12 +36,13 @@ module OpenRouter
|
|
|
36
36
|
# @param model [String|Array] Model identifier or array of models for fallback
|
|
37
37
|
# @param accumulate_response [Boolean] Whether to accumulate and return complete response
|
|
38
38
|
# @param extras [Hash] Additional parameters for the completion request
|
|
39
|
+
# @param block [Proc] Optional block to call for each chunk (in addition to registered callbacks)
|
|
39
40
|
# @return [Response, nil] Complete response if accumulate_response is true, nil otherwise
|
|
40
|
-
def stream_complete(messages, model: "openrouter/auto", accumulate_response: true, **extras)
|
|
41
|
+
def stream_complete(messages, model: "openrouter/auto", accumulate_response: true, **extras, &block)
|
|
41
42
|
response_accumulator = ResponseAccumulator.new if accumulate_response
|
|
42
43
|
|
|
43
|
-
# Set up streaming handler
|
|
44
|
-
stream_handler = build_stream_handler(response_accumulator)
|
|
44
|
+
# Set up streaming handler (pass optional per-call block)
|
|
45
|
+
stream_handler = build_stream_handler(response_accumulator, &block)
|
|
45
46
|
|
|
46
47
|
# Trigger start callback
|
|
47
48
|
trigger_streaming_callbacks(:on_start, { model: model, messages: messages })
|
|
@@ -92,11 +93,14 @@ module OpenRouter
|
|
|
92
93
|
|
|
93
94
|
private
|
|
94
95
|
|
|
95
|
-
def build_stream_handler(accumulator)
|
|
96
|
+
def build_stream_handler(accumulator, &per_call_block)
|
|
96
97
|
proc do |chunk|
|
|
97
98
|
# Trigger chunk callback
|
|
98
99
|
trigger_streaming_callbacks(:on_chunk, chunk)
|
|
99
100
|
|
|
101
|
+
# Call per-call block if provided (used by #stream method)
|
|
102
|
+
per_call_block&.call(chunk)
|
|
103
|
+
|
|
100
104
|
# Accumulate if needed
|
|
101
105
|
accumulator&.add_chunk(chunk)
|
|
102
106
|
|
data/lib/open_router/version.rb
CHANGED