open_router_enhanced 1.0.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 +7 -0
- data/.env.example +1 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/.rubocop_todo.yml +130 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +41 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +384 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +138 -0
- data/LICENSE.txt +21 -0
- data/MIGRATION.md +556 -0
- data/README.md +1660 -0
- data/Rakefile +334 -0
- data/SECURITY.md +150 -0
- data/VCR_CONFIGURATION.md +80 -0
- data/docs/model_selection.md +637 -0
- data/docs/observability.md +430 -0
- data/docs/prompt_templates.md +422 -0
- data/docs/streaming.md +467 -0
- data/docs/structured_outputs.md +466 -0
- data/docs/tools.md +1016 -0
- data/examples/basic_completion.rb +122 -0
- data/examples/model_selection_example.rb +141 -0
- data/examples/observability_example.rb +199 -0
- data/examples/prompt_template_example.rb +184 -0
- data/examples/smart_completion_example.rb +89 -0
- data/examples/streaming_example.rb +176 -0
- data/examples/structured_outputs_example.rb +191 -0
- data/examples/tool_calling_example.rb +149 -0
- data/lib/open_router/client.rb +552 -0
- data/lib/open_router/http.rb +118 -0
- data/lib/open_router/json_healer.rb +263 -0
- data/lib/open_router/model_registry.rb +378 -0
- data/lib/open_router/model_selector.rb +462 -0
- data/lib/open_router/prompt_template.rb +290 -0
- data/lib/open_router/response.rb +371 -0
- data/lib/open_router/schema.rb +288 -0
- data/lib/open_router/streaming_client.rb +210 -0
- data/lib/open_router/tool.rb +221 -0
- data/lib/open_router/tool_call.rb +180 -0
- data/lib/open_router/usage_tracker.rb +277 -0
- data/lib/open_router/version.rb +5 -0
- data/lib/open_router.rb +123 -0
- data/sig/open_router.rbs +20 -0
- metadata +186 -0
data/docs/tools.md
ADDED
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
# Tool Calling
|
|
2
|
+
|
|
3
|
+
The OpenRouter gem provides comprehensive support for OpenRouter's function calling API with an intuitive Ruby DSL for defining and managing tools. This enables AI models to interact with external functions and APIs in a structured, type-safe manner.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Define a tool
|
|
9
|
+
weather_tool = OpenRouter::Tool.define do
|
|
10
|
+
name "get_weather"
|
|
11
|
+
description "Get current weather for a location"
|
|
12
|
+
|
|
13
|
+
parameters do
|
|
14
|
+
string :location, required: true, description: "City name or coordinates"
|
|
15
|
+
string :units, enum: ["celsius", "fahrenheit"], description: "Temperature units"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Use in completion
|
|
20
|
+
response = client.complete(
|
|
21
|
+
[{ role: "user", content: "What's the weather in London?" }],
|
|
22
|
+
model: "anthropic/claude-3.5-sonnet",
|
|
23
|
+
tools: [weather_tool],
|
|
24
|
+
tool_choice: "auto"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Handle tool calls
|
|
28
|
+
if response.has_tool_calls?
|
|
29
|
+
response.tool_calls.each do |tool_call|
|
|
30
|
+
result = fetch_weather(tool_call.arguments["location"])
|
|
31
|
+
puts result
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Tool Definition DSL
|
|
37
|
+
|
|
38
|
+
The gem provides a fluent DSL for defining tools with comprehensive parameter validation:
|
|
39
|
+
|
|
40
|
+
### Basic Tool Structure
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
tool = OpenRouter::Tool.define do
|
|
44
|
+
name "tool_name"
|
|
45
|
+
description "Clear description of what this tool does"
|
|
46
|
+
|
|
47
|
+
parameters do
|
|
48
|
+
# Parameter definitions go here
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Parameter Types
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
comprehensive_tool = OpenRouter::Tool.define do
|
|
57
|
+
name "comprehensive_example"
|
|
58
|
+
description "Example showing all parameter types"
|
|
59
|
+
|
|
60
|
+
parameters do
|
|
61
|
+
# String parameters
|
|
62
|
+
string :name, required: true, description: "Required string parameter"
|
|
63
|
+
string :category, enum: ["A", "B", "C"], description: "String with allowed values"
|
|
64
|
+
string :content, minLength: 10, maxLength: 1000, description: "String with length constraints"
|
|
65
|
+
string :pattern_field, pattern: "^[A-Z]{2,3}$", description: "String with regex pattern"
|
|
66
|
+
|
|
67
|
+
# Numeric parameters
|
|
68
|
+
integer :count, required: true, minimum: 1, maximum: 100, description: "Integer with range"
|
|
69
|
+
number :price, minimum: 0.01, description: "Floating point number"
|
|
70
|
+
|
|
71
|
+
# Boolean parameters
|
|
72
|
+
boolean :enabled, description: "Boolean flag"
|
|
73
|
+
|
|
74
|
+
# Array parameters
|
|
75
|
+
array :tags, description: "Array of strings", items: { type: "string" }
|
|
76
|
+
array :numbers, description: "Array of numbers", items: { type: "number", minimum: 0 }
|
|
77
|
+
|
|
78
|
+
# Object parameters (nested)
|
|
79
|
+
object :metadata do
|
|
80
|
+
string :key, required: true
|
|
81
|
+
string :value, required: true
|
|
82
|
+
integer :priority, minimum: 1, maximum: 10
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Complex Nested Objects
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
order_processing_tool = OpenRouter::Tool.define do
|
|
92
|
+
name "process_order"
|
|
93
|
+
description "Process a customer order"
|
|
94
|
+
|
|
95
|
+
parameters do
|
|
96
|
+
string :order_id, required: true, description: "Unique order identifier"
|
|
97
|
+
|
|
98
|
+
# Customer object
|
|
99
|
+
object :customer, required: true do
|
|
100
|
+
string :name, required: true, description: "Customer full name"
|
|
101
|
+
string :email, required: true, description: "Customer email address"
|
|
102
|
+
|
|
103
|
+
# Nested address object
|
|
104
|
+
object :address, required: true do
|
|
105
|
+
string :street, required: true
|
|
106
|
+
string :city, required: true
|
|
107
|
+
string :state, required: true
|
|
108
|
+
string :zip_code, required: true, pattern: "^\\d{5}(-\\d{4})?$"
|
|
109
|
+
string :country, default: "US", enum: ["US", "CA", "MX"]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Array of order items
|
|
114
|
+
array :items, required: true, description: "Order items" do
|
|
115
|
+
items do
|
|
116
|
+
object do
|
|
117
|
+
string :product_id, required: true
|
|
118
|
+
string :product_name, required: true
|
|
119
|
+
integer :quantity, required: true, minimum: 1
|
|
120
|
+
number :unit_price, required: true, minimum: 0.01
|
|
121
|
+
array :options, description: "Product options", items: { type: "string" }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Payment information
|
|
127
|
+
object :payment do
|
|
128
|
+
string :method, required: true, enum: ["credit_card", "debit_card", "paypal", "bank_transfer"]
|
|
129
|
+
number :amount, required: true, minimum: 0.01
|
|
130
|
+
string :currency, default: "USD", enum: ["USD", "EUR", "GBP", "CAD"]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Optional metadata
|
|
134
|
+
object :metadata do
|
|
135
|
+
string :source, enum: ["web", "mobile", "api"]
|
|
136
|
+
string :campaign_id
|
|
137
|
+
boolean :gift_order, default: false
|
|
138
|
+
string :special_instructions
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Array Parameters with Complex Items
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
data_analysis_tool = OpenRouter::Tool.define do
|
|
148
|
+
name "analyze_data"
|
|
149
|
+
description "Analyze datasets with various configurations"
|
|
150
|
+
|
|
151
|
+
parameters do
|
|
152
|
+
# Array of simple values
|
|
153
|
+
array :column_names, required: true, description: "Data column names", items: { type: "string" }
|
|
154
|
+
|
|
155
|
+
# Array of numbers with constraints
|
|
156
|
+
array :thresholds, description: "Analysis thresholds", items: {
|
|
157
|
+
type: "number",
|
|
158
|
+
minimum: 0,
|
|
159
|
+
maximum: 100
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Array of objects - use explicit items hash for complex objects
|
|
163
|
+
array :filters, description: "Data filters", items: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
column: { type: "string", description: "Column to filter" },
|
|
167
|
+
operator: { type: "string", enum: ["eq", "ne", "gt", "lt", "in", "not_in"] },
|
|
168
|
+
value_type: { type: "string", enum: ["string", "number", "array"] },
|
|
169
|
+
value: { type: "string", description: "Filter value (format depends on value_type)" }
|
|
170
|
+
},
|
|
171
|
+
required: ["column", "operator", "value_type"],
|
|
172
|
+
additionalProperties: false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Array with min/max items
|
|
176
|
+
array :metrics, required: true, description: "Metrics to calculate",
|
|
177
|
+
minItems: 1, maxItems: 10, items: {
|
|
178
|
+
type: "string",
|
|
179
|
+
enum: ["mean", "median", "mode", "std", "var", "min", "max"]
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Tool Definition from Hash
|
|
186
|
+
|
|
187
|
+
For complex tools or when migrating from existing OpenAPI/JSON schemas:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# Define from hash (useful for complex tools or API imports)
|
|
191
|
+
api_tool = OpenRouter::Tool.from_hash({
|
|
192
|
+
name: "api_request",
|
|
193
|
+
description: "Make HTTP requests to external APIs",
|
|
194
|
+
parameters: {
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
url: {
|
|
198
|
+
type: "string",
|
|
199
|
+
format: "uri",
|
|
200
|
+
description: "Target URL for the request"
|
|
201
|
+
},
|
|
202
|
+
method: {
|
|
203
|
+
type: "string",
|
|
204
|
+
enum: ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
205
|
+
default: "GET"
|
|
206
|
+
},
|
|
207
|
+
headers: {
|
|
208
|
+
type: "object",
|
|
209
|
+
patternProperties: {
|
|
210
|
+
"^[A-Za-z-]+$": { type: "string" }
|
|
211
|
+
},
|
|
212
|
+
description: "HTTP headers as key-value pairs"
|
|
213
|
+
},
|
|
214
|
+
body: {
|
|
215
|
+
type: "string",
|
|
216
|
+
description: "Request body (JSON string for POST/PUT)"
|
|
217
|
+
},
|
|
218
|
+
timeout: {
|
|
219
|
+
type: "integer",
|
|
220
|
+
minimum: 1,
|
|
221
|
+
maximum: 300,
|
|
222
|
+
default: 30,
|
|
223
|
+
description: "Request timeout in seconds"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
required: ["url"]
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
# Convert from OpenAPI 3.0 spec
|
|
231
|
+
def tool_from_openapi_operation(operation_spec)
|
|
232
|
+
OpenRouter::Tool.from_hash({
|
|
233
|
+
name: operation_spec[:operationId],
|
|
234
|
+
description: operation_spec[:summary] || operation_spec[:description],
|
|
235
|
+
parameters: operation_spec.dig(:requestBody, :content, :"application/json", :schema) || {
|
|
236
|
+
type: "object",
|
|
237
|
+
properties: {},
|
|
238
|
+
required: []
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Using Tools in Completions
|
|
245
|
+
|
|
246
|
+
### Basic Usage
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
tools = [weather_tool, calculator_tool, search_tool]
|
|
250
|
+
|
|
251
|
+
response = client.complete(
|
|
252
|
+
[{ role: "user", content: "What's the weather in Tokyo and what's 15 * 23?" }],
|
|
253
|
+
model: "anthropic/claude-3.5-sonnet",
|
|
254
|
+
tools: tools,
|
|
255
|
+
tool_choice: "auto" # Let the model decide which tools to use
|
|
256
|
+
)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Tool Choice Options
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# Auto - let model decide when to use tools
|
|
263
|
+
response = client.complete(messages, tools: tools, tool_choice: "auto")
|
|
264
|
+
|
|
265
|
+
# Required - force model to use a tool
|
|
266
|
+
response = client.complete(messages, tools: tools, tool_choice: "required")
|
|
267
|
+
|
|
268
|
+
# None - disable tool usage for this request
|
|
269
|
+
response = client.complete(messages, tools: tools, tool_choice: "none")
|
|
270
|
+
|
|
271
|
+
# Specific tool - force use of a particular tool
|
|
272
|
+
response = client.complete(messages, tools: tools, tool_choice: "get_weather")
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Handling Tool Calls
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
def handle_completion_with_tools(messages, tools)
|
|
279
|
+
response = client.complete(messages, tools: tools, tool_choice: "auto")
|
|
280
|
+
|
|
281
|
+
unless response.has_tool_calls?
|
|
282
|
+
return response.content # Regular text response
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Process each tool call
|
|
286
|
+
conversation = messages.dup
|
|
287
|
+
conversation << response.to_message
|
|
288
|
+
|
|
289
|
+
response.tool_calls.each do |tool_call|
|
|
290
|
+
puts "Executing tool: #{tool_call.name}"
|
|
291
|
+
puts "Arguments: #{tool_call.arguments.inspect}"
|
|
292
|
+
|
|
293
|
+
# Execute the tool
|
|
294
|
+
result = execute_tool(tool_call)
|
|
295
|
+
|
|
296
|
+
# Add result to conversation
|
|
297
|
+
conversation << tool_call.to_result_message(result)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Get final response
|
|
301
|
+
final_response = client.complete(conversation, tools: tools)
|
|
302
|
+
final_response.content
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def execute_tool(tool_call)
|
|
306
|
+
case tool_call.name
|
|
307
|
+
when "get_weather"
|
|
308
|
+
fetch_weather(tool_call.arguments["location"], tool_call.arguments["units"])
|
|
309
|
+
when "calculate"
|
|
310
|
+
perform_calculation(tool_call.arguments["expression"])
|
|
311
|
+
when "search_web"
|
|
312
|
+
search_web(tool_call.arguments["query"], tool_call.arguments["max_results"])
|
|
313
|
+
else
|
|
314
|
+
{ error: "Unknown tool: #{tool_call.name}" }
|
|
315
|
+
end
|
|
316
|
+
rescue => e
|
|
317
|
+
{ error: "Tool execution failed: #{e.message}" }
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## ToolCall Objects
|
|
322
|
+
|
|
323
|
+
When a model uses tools, you receive `ToolCall` objects with helpful methods:
|
|
324
|
+
|
|
325
|
+
### ToolCall Object
|
|
326
|
+
|
|
327
|
+
Properties and helpers you can rely on:
|
|
328
|
+
|
|
329
|
+
- `id`: String
|
|
330
|
+
- `type`: String (e.g., "function")
|
|
331
|
+
- `name`: tool function name
|
|
332
|
+
- `arguments_string`: raw JSON string
|
|
333
|
+
- `arguments`: parsed Hash (raises `ToolCallError` on invalid JSON)
|
|
334
|
+
- `to_message`: assistant message with the original `tool_calls` field
|
|
335
|
+
- `to_result_message(result)`: tool message payload with `tool_call_id` and JSON content
|
|
336
|
+
- `execute { |name, arguments| ... }`: returns an `OpenRouter::ToolResult` (success/failure), where:
|
|
337
|
+
- `success?`: boolean
|
|
338
|
+
- `to_message`: a tool message suitable for conversation continuation
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
response.tool_calls.each do |tool_call|
|
|
344
|
+
tool_result = tool_call.execute do |name, args|
|
|
345
|
+
case name
|
|
346
|
+
when "get_weather" then fetch_weather(args["location"], args["units"])
|
|
347
|
+
else { error: "unknown tool: #{name}" }
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
conversation << tool_result.to_message
|
|
352
|
+
end
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Advanced Usage Patterns
|
|
356
|
+
|
|
357
|
+
### Tool Validation and Error Handling
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
def safe_tool_execution(tool_call, tools)
|
|
361
|
+
# Validate arguments against tool schema
|
|
362
|
+
unless tool_call.valid?(tools: tools)
|
|
363
|
+
return {
|
|
364
|
+
error: "Invalid arguments",
|
|
365
|
+
details: tool_call.validation_errors(tools: tools)
|
|
366
|
+
}
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
begin
|
|
370
|
+
# Execute with timeout
|
|
371
|
+
Timeout::timeout(30) do
|
|
372
|
+
execute_tool_safely(tool_call)
|
|
373
|
+
end
|
|
374
|
+
rescue Timeout::Error
|
|
375
|
+
{ error: "Tool execution timed out" }
|
|
376
|
+
rescue => e
|
|
377
|
+
{ error: "Tool execution failed: #{e.message}" }
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def execute_tool_safely(tool_call)
|
|
382
|
+
case tool_call.name
|
|
383
|
+
when "file_operation"
|
|
384
|
+
# Validate file paths for security
|
|
385
|
+
path = tool_call.arguments["path"]
|
|
386
|
+
raise "Invalid path" unless safe_path?(path)
|
|
387
|
+
|
|
388
|
+
File.read(path)
|
|
389
|
+
when "api_request"
|
|
390
|
+
# Validate URLs for security
|
|
391
|
+
url = tool_call.arguments["url"]
|
|
392
|
+
raise "Invalid URL" unless safe_url?(url)
|
|
393
|
+
|
|
394
|
+
Net::HTTP.get(URI(url))
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def safe_path?(path)
|
|
399
|
+
# Prevent directory traversal
|
|
400
|
+
!path.include?("..") && path.start_with?("/safe/directory/")
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def safe_url?(url)
|
|
404
|
+
# Only allow specific domains
|
|
405
|
+
uri = URI.parse(url)
|
|
406
|
+
["api.example.com", "safe-api.com"].include?(uri.host)
|
|
407
|
+
end
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Streaming Tool Results
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
def streaming_tool_execution(tool_call)
|
|
414
|
+
case tool_call.name
|
|
415
|
+
when "large_data_processing"
|
|
416
|
+
# Stream results for long-running operations
|
|
417
|
+
results = []
|
|
418
|
+
|
|
419
|
+
process_large_dataset(tool_call.arguments) do |chunk|
|
|
420
|
+
results << chunk
|
|
421
|
+
yield({ status: "processing", progress: results.size, partial_data: chunk })
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
{ status: "complete", data: results }
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Usage with streaming
|
|
429
|
+
handle_tool_call_with_streaming(tool_call) do |partial_result|
|
|
430
|
+
puts "Progress: #{partial_result[:progress]} items processed"
|
|
431
|
+
end
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Tool Composition and Chaining
|
|
435
|
+
|
|
436
|
+
```ruby
|
|
437
|
+
class ToolChain
|
|
438
|
+
def initialize(client, tools)
|
|
439
|
+
@client = client
|
|
440
|
+
@tools = tools
|
|
441
|
+
@conversation = []
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def execute(initial_message, max_iterations: 10)
|
|
445
|
+
@conversation = [{ role: "user", content: initial_message }]
|
|
446
|
+
iterations = 0
|
|
447
|
+
|
|
448
|
+
while iterations < max_iterations
|
|
449
|
+
response = @client.complete(@conversation, tools: @tools, tool_choice: "auto")
|
|
450
|
+
@conversation << response.to_message
|
|
451
|
+
|
|
452
|
+
if response.has_tool_calls?
|
|
453
|
+
# Execute all tool calls
|
|
454
|
+
response.tool_calls.each do |tool_call|
|
|
455
|
+
result = execute_tool(tool_call)
|
|
456
|
+
@conversation << tool_call.to_result_message(result)
|
|
457
|
+
end
|
|
458
|
+
iterations += 1
|
|
459
|
+
else
|
|
460
|
+
# Final response without tool calls
|
|
461
|
+
return response.content
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
"Maximum iterations reached"
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
private
|
|
469
|
+
|
|
470
|
+
def execute_tool(tool_call)
|
|
471
|
+
# Tool execution logic here
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Usage
|
|
476
|
+
chain = ToolChain.new(client, [search_tool, calculator_tool, weather_tool])
|
|
477
|
+
result = chain.execute("Plan a trip to Tokyo: check weather, calculate budget for 5 days, and find flights")
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Tool Result Caching
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
class CachedToolExecutor
|
|
484
|
+
def initialize(cache_ttl: 3600) # 1 hour cache
|
|
485
|
+
@cache = {}
|
|
486
|
+
@cache_ttl = cache_ttl
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def execute(tool_call)
|
|
490
|
+
cache_key = generate_cache_key(tool_call)
|
|
491
|
+
cached_result = @cache[cache_key]
|
|
492
|
+
|
|
493
|
+
if cached_result && Time.now - cached_result[:timestamp] < @cache_ttl
|
|
494
|
+
return cached_result[:result]
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
result = perform_tool_execution(tool_call)
|
|
498
|
+
@cache[cache_key] = { result: result, timestamp: Time.now }
|
|
499
|
+
result
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
private
|
|
503
|
+
|
|
504
|
+
def generate_cache_key(tool_call)
|
|
505
|
+
"#{tool_call.name}:#{tool_call.arguments.to_json}"
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def perform_tool_execution(tool_call)
|
|
509
|
+
# Actual tool execution
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Tool Organization and Management
|
|
515
|
+
|
|
516
|
+
### Tool Registry
|
|
517
|
+
|
|
518
|
+
```ruby
|
|
519
|
+
class ToolRegistry
|
|
520
|
+
def initialize
|
|
521
|
+
@tools = {}
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def register(tool)
|
|
525
|
+
@tools[tool.name] = tool
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def get(name)
|
|
529
|
+
@tools[name]
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def all
|
|
533
|
+
@tools.values
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def for_capabilities(*capabilities)
|
|
537
|
+
# Return tools that provide specific capabilities
|
|
538
|
+
@tools.values.select do |tool|
|
|
539
|
+
capabilities.all? { |cap| tool_provides_capability?(tool, cap) }
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
private
|
|
544
|
+
|
|
545
|
+
def tool_provides_capability?(tool, capability)
|
|
546
|
+
# Define your capability mapping logic
|
|
547
|
+
case capability
|
|
548
|
+
when :web_search
|
|
549
|
+
["search_web", "google_search"].include?(tool.name)
|
|
550
|
+
when :calculations
|
|
551
|
+
["calculator", "math_eval"].include?(tool.name)
|
|
552
|
+
when :data_analysis
|
|
553
|
+
["analyze_data", "statistics"].include?(tool.name)
|
|
554
|
+
else
|
|
555
|
+
false
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Usage
|
|
561
|
+
registry = ToolRegistry.new
|
|
562
|
+
registry.register(search_tool)
|
|
563
|
+
registry.register(calculator_tool)
|
|
564
|
+
registry.register(weather_tool)
|
|
565
|
+
|
|
566
|
+
# Get tools for specific capabilities
|
|
567
|
+
analysis_tools = registry.for_capabilities(:calculations, :data_analysis)
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Tool Categories
|
|
571
|
+
|
|
572
|
+
```ruby
|
|
573
|
+
module Tools
|
|
574
|
+
module Web
|
|
575
|
+
SEARCH = OpenRouter::Tool.define do
|
|
576
|
+
name "web_search"
|
|
577
|
+
description "Search the web for information"
|
|
578
|
+
parameters do
|
|
579
|
+
string :query, required: true
|
|
580
|
+
integer :max_results, minimum: 1, maximum: 20, default: 10
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
SCRAPE = OpenRouter::Tool.define do
|
|
585
|
+
name "web_scrape"
|
|
586
|
+
description "Extract content from a web page"
|
|
587
|
+
parameters do
|
|
588
|
+
string :url, required: true
|
|
589
|
+
array :selectors, items: { type: "string" }
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
module Math
|
|
595
|
+
CALCULATOR = OpenRouter::Tool.define do
|
|
596
|
+
name "calculator"
|
|
597
|
+
description "Perform mathematical calculations"
|
|
598
|
+
parameters do
|
|
599
|
+
string :expression, required: true
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
STATISTICS = OpenRouter::Tool.define do
|
|
604
|
+
name "statistics"
|
|
605
|
+
description "Calculate statistical measures"
|
|
606
|
+
parameters do
|
|
607
|
+
array :data, required: true, items: { type: "number" }
|
|
608
|
+
array :measures, items: {
|
|
609
|
+
type: "string",
|
|
610
|
+
enum: ["mean", "median", "std", "var"]
|
|
611
|
+
}
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
module Data
|
|
617
|
+
FILE_READ = OpenRouter::Tool.define do
|
|
618
|
+
name "file_read"
|
|
619
|
+
description "Read file contents"
|
|
620
|
+
parameters do
|
|
621
|
+
string :path, required: true
|
|
622
|
+
string :encoding, default: "utf-8"
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
DATABASE_QUERY = OpenRouter::Tool.define do
|
|
627
|
+
name "database_query"
|
|
628
|
+
description "Query database"
|
|
629
|
+
parameters do
|
|
630
|
+
string :query, required: true
|
|
631
|
+
array :parameters, items: { type: "string" }
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Usage
|
|
638
|
+
web_tools = [Tools::Web::SEARCH, Tools::Web::SCRAPE]
|
|
639
|
+
math_tools = [Tools::Math::CALCULATOR, Tools::Math::STATISTICS]
|
|
640
|
+
all_tools = web_tools + math_tools + [Tools::Data::FILE_READ]
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
## Model Selection for Tool Calling
|
|
644
|
+
|
|
645
|
+
```ruby
|
|
646
|
+
# Select models that support function calling
|
|
647
|
+
model = OpenRouter::ModelSelector.new
|
|
648
|
+
.require(:function_calling)
|
|
649
|
+
.optimize_for(:cost)
|
|
650
|
+
.choose
|
|
651
|
+
|
|
652
|
+
# Use with tools
|
|
653
|
+
response = client.complete(
|
|
654
|
+
messages,
|
|
655
|
+
model: model,
|
|
656
|
+
tools: tools,
|
|
657
|
+
tool_choice: "auto"
|
|
658
|
+
)
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## Testing Tools
|
|
662
|
+
|
|
663
|
+
### Unit Testing Tool Definitions
|
|
664
|
+
|
|
665
|
+
```ruby
|
|
666
|
+
# spec/tools/weather_tool_spec.rb
|
|
667
|
+
RSpec.describe "Weather Tool" do
|
|
668
|
+
let(:tool) { Tools::Weather::CURRENT }
|
|
669
|
+
|
|
670
|
+
it "has correct name and description" do
|
|
671
|
+
expect(tool.name).to eq("get_current_weather")
|
|
672
|
+
expect(tool.description).to include("current weather")
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
it "defines required parameters" do
|
|
676
|
+
schema = tool.to_json_schema
|
|
677
|
+
expect(schema[:parameters][:required]).to include("location")
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
it "validates parameter constraints" do
|
|
681
|
+
schema = tool.to_json_schema
|
|
682
|
+
units_prop = schema[:parameters][:properties][:units]
|
|
683
|
+
expect(units_prop[:enum]).to include("celsius", "fahrenheit")
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### Integration Testing
|
|
689
|
+
|
|
690
|
+
```ruby
|
|
691
|
+
# spec/integration/tool_calling_spec.rb
|
|
692
|
+
RSpec.describe "Tool Calling Integration" do
|
|
693
|
+
let(:client) { OpenRouter::Client.new }
|
|
694
|
+
let(:tools) { [weather_tool, calculator_tool] }
|
|
695
|
+
|
|
696
|
+
it "handles tool calls correctly", :vcr do
|
|
697
|
+
response = client.complete(
|
|
698
|
+
[{ role: "user", content: "What's 2+2 and weather in Paris?" }],
|
|
699
|
+
model: "anthropic/claude-3.5-sonnet",
|
|
700
|
+
tools: tools,
|
|
701
|
+
tool_choice: "auto"
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
expect(response.has_tool_calls?).to be true
|
|
705
|
+
expect(response.tool_calls.map(&:name)).to include("calculator")
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
it "validates tool call arguments" do
|
|
709
|
+
# Mock response with invalid arguments
|
|
710
|
+
mock_response = build_mock_response_with_invalid_tool_call
|
|
711
|
+
|
|
712
|
+
tool_call = mock_response.tool_calls.first
|
|
713
|
+
expect(tool_call.valid?(tools: tools)).to be false
|
|
714
|
+
expect(tool_call.validation_errors(tools: tools)).to include(/required.*missing/)
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Mock Tool Execution
|
|
720
|
+
|
|
721
|
+
```ruby
|
|
722
|
+
class MockToolExecutor
|
|
723
|
+
def initialize(responses = {})
|
|
724
|
+
@responses = responses
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def execute(tool_call)
|
|
728
|
+
key = "#{tool_call.name}:#{tool_call.arguments.to_json}"
|
|
729
|
+
@responses[key] || @responses[tool_call.name] || { error: "No mock response defined" }
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Usage in tests
|
|
734
|
+
mock_executor = MockToolExecutor.new({
|
|
735
|
+
"get_weather" => { temperature: 22, condition: "sunny" },
|
|
736
|
+
"calculator" => { result: 42 }
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
result = mock_executor.execute(tool_call)
|
|
740
|
+
expect(result[:temperature]).to eq(22)
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
## Security Considerations
|
|
744
|
+
|
|
745
|
+
### Input Validation
|
|
746
|
+
|
|
747
|
+
```ruby
|
|
748
|
+
def validate_tool_arguments(tool_call)
|
|
749
|
+
case tool_call.name
|
|
750
|
+
when "file_operation"
|
|
751
|
+
path = tool_call.arguments["path"]
|
|
752
|
+
|
|
753
|
+
# Prevent directory traversal
|
|
754
|
+
raise "Invalid path" if path.include?("..")
|
|
755
|
+
|
|
756
|
+
# Restrict to allowed directories
|
|
757
|
+
allowed_dirs = ["/app/data", "/tmp/uploads"]
|
|
758
|
+
raise "Forbidden path" unless allowed_dirs.any? { |dir| path.start_with?(dir) }
|
|
759
|
+
|
|
760
|
+
when "api_request"
|
|
761
|
+
url = tool_call.arguments["url"]
|
|
762
|
+
uri = URI.parse(url)
|
|
763
|
+
|
|
764
|
+
# Whitelist allowed domains
|
|
765
|
+
allowed_hosts = ["api.example.com", "trusted-service.com"]
|
|
766
|
+
raise "Forbidden host" unless allowed_hosts.include?(uri.host)
|
|
767
|
+
|
|
768
|
+
# Prevent internal network access
|
|
769
|
+
raise "Internal network access forbidden" if internal_ip?(uri.host)
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
def internal_ip?(hostname)
|
|
774
|
+
ip = Resolv.getaddress(hostname)
|
|
775
|
+
IPAddr.new("10.0.0.0/8").include?(ip) ||
|
|
776
|
+
IPAddr.new("172.16.0.0/12").include?(ip) ||
|
|
777
|
+
IPAddr.new("192.168.0.0/16").include?(ip) ||
|
|
778
|
+
IPAddr.new("127.0.0.0/8").include?(ip)
|
|
779
|
+
rescue Resolv::ResolvError
|
|
780
|
+
false
|
|
781
|
+
end
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### Rate Limiting
|
|
785
|
+
|
|
786
|
+
```ruby
|
|
787
|
+
class RateLimitedToolExecutor
|
|
788
|
+
def initialize(limit: 10, period: 60) # 10 calls per minute
|
|
789
|
+
@limit = limit
|
|
790
|
+
@period = period
|
|
791
|
+
@call_times = []
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def execute(tool_call)
|
|
795
|
+
now = Time.now
|
|
796
|
+
|
|
797
|
+
# Remove old calls outside the time window
|
|
798
|
+
@call_times.reject! { |time| now - time > @period }
|
|
799
|
+
|
|
800
|
+
# Check rate limit
|
|
801
|
+
if @call_times.size >= @limit
|
|
802
|
+
raise "Rate limit exceeded: #{@limit} calls per #{@period} seconds"
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
@call_times << now
|
|
806
|
+
perform_tool_execution(tool_call)
|
|
807
|
+
end
|
|
808
|
+
end
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### Sandboxing
|
|
812
|
+
|
|
813
|
+
```ruby
|
|
814
|
+
require 'timeout'
|
|
815
|
+
|
|
816
|
+
def execute_tool_in_sandbox(tool_call)
|
|
817
|
+
# Set resource limits
|
|
818
|
+
original_rlimits = set_resource_limits
|
|
819
|
+
|
|
820
|
+
begin
|
|
821
|
+
# Execute with timeout
|
|
822
|
+
Timeout::timeout(30) do
|
|
823
|
+
case tool_call.name
|
|
824
|
+
when "code_execution"
|
|
825
|
+
execute_code_safely(tool_call.arguments["code"])
|
|
826
|
+
when "file_processing"
|
|
827
|
+
process_file_safely(tool_call.arguments["file_path"])
|
|
828
|
+
else
|
|
829
|
+
execute_tool_normally(tool_call)
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
ensure
|
|
833
|
+
# Restore original limits
|
|
834
|
+
restore_resource_limits(original_rlimits)
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
def set_resource_limits
|
|
839
|
+
original = {}
|
|
840
|
+
|
|
841
|
+
# Limit memory usage (100MB)
|
|
842
|
+
original[:memory] = Process.getrlimit(:AS)
|
|
843
|
+
Process.setrlimit(:AS, 100 * 1024 * 1024)
|
|
844
|
+
|
|
845
|
+
# Limit CPU time (10 seconds)
|
|
846
|
+
original[:cpu] = Process.getrlimit(:CPU)
|
|
847
|
+
Process.setrlimit(:CPU, 10)
|
|
848
|
+
|
|
849
|
+
original
|
|
850
|
+
end
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
## Best Practices
|
|
854
|
+
|
|
855
|
+
### 1. Clear Tool Descriptions
|
|
856
|
+
|
|
857
|
+
```ruby
|
|
858
|
+
# Good: Clear, specific description
|
|
859
|
+
search_tool = OpenRouter::Tool.define do
|
|
860
|
+
name "web_search"
|
|
861
|
+
description "Search the internet for current information about any topic. Returns relevant web pages with titles, URLs, and snippets. Use this when you need up-to-date information not in your training data."
|
|
862
|
+
|
|
863
|
+
parameters do
|
|
864
|
+
string :query, required: true,
|
|
865
|
+
description: "Search query - be specific and include relevant keywords. Example: 'weather forecast London UK' or 'latest iPhone 15 reviews'"
|
|
866
|
+
integer :max_results,
|
|
867
|
+
minimum: 1, maximum: 10, default: 5,
|
|
868
|
+
description: "Number of search results to return (1-10)"
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
# Bad: Vague description
|
|
873
|
+
bad_tool = OpenRouter::Tool.define do
|
|
874
|
+
name "search"
|
|
875
|
+
description "Search for stuff"
|
|
876
|
+
parameters do
|
|
877
|
+
string :q, required: true
|
|
878
|
+
end
|
|
879
|
+
end
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### 2. Parameter Validation
|
|
883
|
+
|
|
884
|
+
```ruby
|
|
885
|
+
# Use comprehensive validation
|
|
886
|
+
robust_tool = OpenRouter::Tool.define do
|
|
887
|
+
name "user_management"
|
|
888
|
+
description "Manage user accounts"
|
|
889
|
+
|
|
890
|
+
parameters do
|
|
891
|
+
string :action, required: true, enum: ["create", "update", "delete", "get"]
|
|
892
|
+
string :user_id, pattern: "^[a-zA-Z0-9_-]{1,50}$",
|
|
893
|
+
description: "Alphanumeric user ID (1-50 characters)"
|
|
894
|
+
string :email, pattern: "^[^@]+@[^@]+\\.[^@]+$",
|
|
895
|
+
description: "Valid email address"
|
|
896
|
+
integer :age, minimum: 13, maximum: 120,
|
|
897
|
+
description: "User age (13-120)"
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### 3. Error Handling
|
|
903
|
+
|
|
904
|
+
```ruby
|
|
905
|
+
def robust_tool_execution(tool_call, tools)
|
|
906
|
+
# Validate first
|
|
907
|
+
unless tool_call.valid?(tools: tools)
|
|
908
|
+
return {
|
|
909
|
+
success: false,
|
|
910
|
+
error: "Invalid arguments",
|
|
911
|
+
details: tool_call.validation_errors(tools: tools)
|
|
912
|
+
}
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
begin
|
|
916
|
+
result = execute_tool(tool_call)
|
|
917
|
+
{ success: true, data: result }
|
|
918
|
+
rescue => e
|
|
919
|
+
{
|
|
920
|
+
success: false,
|
|
921
|
+
error: "Execution failed: #{e.message}",
|
|
922
|
+
tool_name: tool_call.name,
|
|
923
|
+
timestamp: Time.now.iso8601
|
|
924
|
+
}
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
### 4. Tool Documentation
|
|
930
|
+
|
|
931
|
+
```ruby
|
|
932
|
+
# Document your tools
|
|
933
|
+
module ToolDocumentation
|
|
934
|
+
WEATHER = {
|
|
935
|
+
name: "get_weather",
|
|
936
|
+
description: "Retrieves current weather information",
|
|
937
|
+
usage_examples: [
|
|
938
|
+
"Get weather for a city: { location: 'London' }",
|
|
939
|
+
"Get weather with units: { location: 'Tokyo', units: 'celsius' }"
|
|
940
|
+
],
|
|
941
|
+
return_format: {
|
|
942
|
+
temperature: "number",
|
|
943
|
+
condition: "string",
|
|
944
|
+
humidity: "number (0-100)",
|
|
945
|
+
wind_speed: "number (km/h)"
|
|
946
|
+
},
|
|
947
|
+
error_cases: [
|
|
948
|
+
"Location not found: returns { error: 'Location not found' }",
|
|
949
|
+
"API unavailable: returns { error: 'Weather service unavailable' }"
|
|
950
|
+
]
|
|
951
|
+
}
|
|
952
|
+
end
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
## Common Tool Patterns
|
|
956
|
+
|
|
957
|
+
### Database Tools
|
|
958
|
+
|
|
959
|
+
```ruby
|
|
960
|
+
database_tool = OpenRouter::Tool.define do
|
|
961
|
+
name "database_query"
|
|
962
|
+
description "Execute read-only database queries"
|
|
963
|
+
|
|
964
|
+
parameters do
|
|
965
|
+
string :query, required: true,
|
|
966
|
+
description: "SQL SELECT query (read-only)"
|
|
967
|
+
array :parameters,
|
|
968
|
+
items: { type: "string" },
|
|
969
|
+
description: "Query parameters for prepared statements"
|
|
970
|
+
integer :limit, minimum: 1, maximum: 1000, default: 100,
|
|
971
|
+
description: "Maximum number of rows to return"
|
|
972
|
+
end
|
|
973
|
+
end
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
### File System Tools
|
|
977
|
+
|
|
978
|
+
```ruby
|
|
979
|
+
file_tool = OpenRouter::Tool.define do
|
|
980
|
+
name "file_operations"
|
|
981
|
+
description "Perform file system operations"
|
|
982
|
+
|
|
983
|
+
parameters do
|
|
984
|
+
string :operation, required: true,
|
|
985
|
+
enum: ["read", "write", "list", "exists", "size"]
|
|
986
|
+
string :path, required: true,
|
|
987
|
+
description: "File or directory path"
|
|
988
|
+
string :content,
|
|
989
|
+
description: "Content to write (required for write operation)"
|
|
990
|
+
string :encoding, default: "utf-8",
|
|
991
|
+
enum: ["utf-8", "ascii", "binary"]
|
|
992
|
+
end
|
|
993
|
+
end
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
### API Integration Tools
|
|
997
|
+
|
|
998
|
+
```ruby
|
|
999
|
+
api_tool = OpenRouter::Tool.define do
|
|
1000
|
+
name "external_api"
|
|
1001
|
+
description "Call external REST APIs"
|
|
1002
|
+
|
|
1003
|
+
parameters do
|
|
1004
|
+
string :endpoint, required: true,
|
|
1005
|
+
description: "API endpoint URL"
|
|
1006
|
+
string :method, default: "GET",
|
|
1007
|
+
enum: ["GET", "POST", "PUT", "DELETE"]
|
|
1008
|
+
object :headers,
|
|
1009
|
+
description: "HTTP headers"
|
|
1010
|
+
object :body,
|
|
1011
|
+
description: "Request body for POST/PUT"
|
|
1012
|
+
integer :timeout, minimum: 1, maximum: 300, default: 30,
|
|
1013
|
+
description: "Request timeout in seconds"
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
```
|