open_router_enhanced 1.0.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 +36 -0
- data/Gemfile.lock +45 -47
- data/README.md +151 -1228
- data/docs/observability.md +3 -0
- data/docs/plugins.md +183 -0
- data/docs/responses_api.md +298 -0
- data/docs/streaming.md +18 -3
- data/docs/structured_outputs.md +466 -146
- data/lib/open_router/client.rb +122 -5
- 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 +9 -0
- metadata +7 -2
data/lib/open_router/client.rb
CHANGED
|
@@ -95,17 +95,20 @@ module OpenRouter
|
|
|
95
95
|
# @param model [String|Array] Model identifier, or array of model identifiers if you want to fallback to the next model in case of failure
|
|
96
96
|
# @param providers [Array<String>] Optional array of provider identifiers, ordered by priority
|
|
97
97
|
# @param transforms [Array<String>] Optional array of strings that tell OpenRouter to apply a series of transformations to the prompt before sending it to the model. Transformations are applied in-order
|
|
98
|
+
# @param plugins [Array<Hash>] Optional array of plugin hashes like [{id: "response-healing"}]. Available plugins: response-healing, web-search, pdf-inputs
|
|
98
99
|
# @param tools [Array<Tool>] Optional array of Tool objects or tool definition hashes for function calling
|
|
99
100
|
# @param tool_choice [String|Hash] Optional tool choice: "auto", "none", "required", or specific tool selection
|
|
100
101
|
# @param response_format [Hash] Optional response format for structured outputs
|
|
102
|
+
# @param prediction [Hash] Optional predicted output for latency reduction, e.g. {type: "content", content: "predicted text"}
|
|
101
103
|
# @param extras [Hash] Optional hash of model-specific parameters to send to the OpenRouter API
|
|
102
104
|
# @param stream [Proc, nil] Optional callable object for streaming
|
|
103
105
|
# @return [Response] The completion response wrapped in a Response object.
|
|
104
|
-
def complete(messages, model: "openrouter/auto", providers: [], transforms: [], tools: [], tool_choice: nil,
|
|
105
|
-
response_format: nil, force_structured_output: nil, extras: {}, stream: nil)
|
|
106
|
-
parameters = prepare_base_parameters(messages, model, providers, transforms, stream, extras)
|
|
106
|
+
def complete(messages, model: "openrouter/auto", providers: [], transforms: [], plugins: [], tools: [], tool_choice: nil,
|
|
107
|
+
response_format: nil, force_structured_output: nil, prediction: nil, extras: {}, stream: nil)
|
|
108
|
+
parameters = prepare_base_parameters(messages, model, providers, transforms, plugins, prediction, stream, extras)
|
|
107
109
|
forced_extraction = configure_tools_and_structured_outputs!(parameters, model, tools, tool_choice,
|
|
108
110
|
response_format, force_structured_output)
|
|
111
|
+
configure_plugins!(parameters, response_format, stream)
|
|
109
112
|
validate_vision_support(model, messages)
|
|
110
113
|
|
|
111
114
|
# Trigger before_request callbacks
|
|
@@ -142,6 +145,48 @@ module OpenRouter
|
|
|
142
145
|
response["data"]
|
|
143
146
|
end
|
|
144
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
|
+
|
|
145
190
|
# Create a new ModelSelector for intelligent model selection
|
|
146
191
|
#
|
|
147
192
|
# @return [ModelSelector] A new ModelSelector instance
|
|
@@ -267,12 +312,14 @@ module OpenRouter
|
|
|
267
312
|
private
|
|
268
313
|
|
|
269
314
|
# Prepare the base parameters for the API request
|
|
270
|
-
def prepare_base_parameters(messages, model, providers, transforms, stream, extras)
|
|
315
|
+
def prepare_base_parameters(messages, model, providers, transforms, plugins, prediction, stream, extras)
|
|
271
316
|
parameters = { messages: messages.dup }
|
|
272
317
|
|
|
273
318
|
configure_model_parameter!(parameters, model)
|
|
274
319
|
configure_provider_parameter!(parameters, providers)
|
|
275
320
|
configure_transforms_parameter!(parameters, transforms)
|
|
321
|
+
configure_plugins_parameter!(parameters, plugins)
|
|
322
|
+
configure_prediction_parameter!(parameters, prediction)
|
|
276
323
|
configure_stream_parameter!(parameters, stream)
|
|
277
324
|
|
|
278
325
|
parameters.merge!(extras)
|
|
@@ -299,11 +346,52 @@ module OpenRouter
|
|
|
299
346
|
parameters[:transforms] = transforms if transforms.any?
|
|
300
347
|
end
|
|
301
348
|
|
|
349
|
+
# Configure the plugins parameter if plugins are specified
|
|
350
|
+
def configure_plugins_parameter!(parameters, plugins)
|
|
351
|
+
parameters[:plugins] = plugins.dup if plugins.any?
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Configure the prediction parameter for latency optimization
|
|
355
|
+
def configure_prediction_parameter!(parameters, prediction)
|
|
356
|
+
parameters[:prediction] = prediction if prediction
|
|
357
|
+
end
|
|
358
|
+
|
|
302
359
|
# Configure the stream parameter if streaming is enabled
|
|
303
360
|
def configure_stream_parameter!(parameters, stream)
|
|
304
361
|
parameters[:stream] = stream if stream
|
|
305
362
|
end
|
|
306
363
|
|
|
364
|
+
# Auto-add response-healing plugin when using structured outputs (non-streaming only)
|
|
365
|
+
# This leverages OpenRouter's native JSON healing for better reliability
|
|
366
|
+
def configure_plugins!(parameters, response_format, stream)
|
|
367
|
+
return unless should_auto_add_healing?(response_format, stream)
|
|
368
|
+
|
|
369
|
+
parameters[:plugins] ||= []
|
|
370
|
+
|
|
371
|
+
# Don't duplicate if user already specified response-healing
|
|
372
|
+
return if parameters[:plugins].any? { |p| p[:id] == "response-healing" || p["id"] == "response-healing" }
|
|
373
|
+
|
|
374
|
+
parameters[:plugins] << { id: "response-healing" }
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Determine if we should auto-add the response-healing plugin
|
|
378
|
+
def should_auto_add_healing?(response_format, stream)
|
|
379
|
+
return false unless configuration.auto_native_healing
|
|
380
|
+
return false if stream # Response healing doesn't work with streaming
|
|
381
|
+
return false unless response_format
|
|
382
|
+
|
|
383
|
+
# Check if response_format is a structured output type
|
|
384
|
+
case response_format
|
|
385
|
+
when OpenRouter::Schema
|
|
386
|
+
true
|
|
387
|
+
when Hash
|
|
388
|
+
type = response_format[:type] || response_format["type"]
|
|
389
|
+
%w[json_schema json_object].include?(type.to_s)
|
|
390
|
+
else
|
|
391
|
+
false
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
307
395
|
# Configure tools and structured outputs, returning forced_extraction flag
|
|
308
396
|
def configure_tools_and_structured_outputs!(parameters, model, tools, tool_choice, response_format,
|
|
309
397
|
force_structured_output)
|
|
@@ -464,7 +552,8 @@ module OpenRouter
|
|
|
464
552
|
end
|
|
465
553
|
end
|
|
466
554
|
|
|
467
|
-
# 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: ... } }
|
|
468
557
|
def serialize_tools(tools)
|
|
469
558
|
tools.map do |tool|
|
|
470
559
|
case tool
|
|
@@ -478,6 +567,34 @@ module OpenRouter
|
|
|
478
567
|
end
|
|
479
568
|
end
|
|
480
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
|
+
|
|
481
598
|
# Serialize response format to the format expected by OpenRouter API
|
|
482
599
|
def serialize_response_format(response_format)
|
|
483
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