open_router_enhanced 2.0.1 → 2.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 +18 -0
- data/Gemfile.lock +1 -1
- data/README.md +90 -0
- data/Rakefile +24 -14
- data/docs/superpowers/plans/2026-06-27-openrouter-routing-features.md +913 -0
- data/docs/superpowers/specs/2026-06-27-openrouter-routing-features-design.md +179 -0
- data/examples/dynamic_model_switching_example.rb +0 -0
- data/examples/model_selection_example.rb +0 -0
- data/examples/prompt_template_example.rb +0 -0
- data/examples/real_world_schemas_example.rb +0 -0
- data/examples/responses_api_example.rb +0 -0
- data/examples/smart_completion_example.rb +0 -0
- data/examples/structured_outputs_example.rb +0 -0
- data/examples/tool_calling_example.rb +0 -0
- data/examples/tool_loop_example.rb +0 -0
- data/lib/open_router/callbacks.rb +50 -0
- data/lib/open_router/client.rb +12 -576
- data/lib/open_router/json_healer.rb +1 -1
- data/lib/open_router/model_registry.rb +24 -6
- data/lib/open_router/model_selector.rb +7 -7
- data/lib/open_router/parameter_builder.rb +120 -0
- data/lib/open_router/request_handler.rb +98 -0
- data/lib/open_router/response.rb +13 -120
- data/lib/open_router/response_parsing.rb +107 -0
- data/lib/open_router/routing.rb +80 -0
- data/lib/open_router/streaming_client.rb +1 -1
- data/lib/open_router/subagent_tool.rb +51 -0
- data/lib/open_router/tool_serializer.rb +164 -0
- data/lib/open_router/version.rb +1 -1
- data/lib/open_router.rb +14 -0
- metadata +11 -2
|
@@ -135,7 +135,7 @@ module OpenRouter
|
|
|
135
135
|
@streaming_callbacks[event].each do |callback|
|
|
136
136
|
callback.call(data)
|
|
137
137
|
rescue StandardError => e
|
|
138
|
-
|
|
138
|
+
OpenRouter.log_warning("[OpenRouter] Streaming callback error for #{event}: #{e.message}")
|
|
139
139
|
end
|
|
140
140
|
end
|
|
141
141
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "tool"
|
|
4
|
+
|
|
5
|
+
module OpenRouter
|
|
6
|
+
# Represents the `openrouter:subagent` server tool, which lets an orchestrator
|
|
7
|
+
# model delegate self-contained subtasks to a cheaper worker model mid-generation.
|
|
8
|
+
#
|
|
9
|
+
# Unlike a function Tool, it serializes to the server-tool shape:
|
|
10
|
+
# { type: "openrouter:subagent", parameters: { model:, instructions:, ... } }
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# sub = OpenRouter::SubagentTool.new(model: "z-ai/glm-5.2", instructions: "Be concise.")
|
|
14
|
+
# client.complete(messages, model: "anthropic/claude-3.5-sonnet", tools: [sub])
|
|
15
|
+
class SubagentTool < Tool
|
|
16
|
+
SERVER_TOOL_TYPE = "openrouter:subagent"
|
|
17
|
+
|
|
18
|
+
# We deliberately do not call super: Tool#initialize expects a function
|
|
19
|
+
# definition with a name/description and validates it, neither of which a
|
|
20
|
+
# server tool has. The server-tool shape is built directly here instead.
|
|
21
|
+
def initialize(model:, instructions: nil, max_completion_tokens: nil, # rubocop:disable Lint/MissingSuper
|
|
22
|
+
temperature: nil, reasoning: nil)
|
|
23
|
+
raise ArgumentError, "model is required for SubagentTool" if model.nil? || model.to_s.strip.empty?
|
|
24
|
+
|
|
25
|
+
@type = SERVER_TOOL_TYPE
|
|
26
|
+
@parameters_config = {
|
|
27
|
+
model: model,
|
|
28
|
+
instructions: instructions,
|
|
29
|
+
max_completion_tokens: max_completion_tokens,
|
|
30
|
+
temperature: temperature,
|
|
31
|
+
reasoning: reasoning
|
|
32
|
+
}.compact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
{ type: @type, parameters: @parameters_config }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def name
|
|
40
|
+
@type
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def description
|
|
44
|
+
"OpenRouter subagent server tool (worker: #{@parameters_config[:model]})"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parameters
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenRouter
|
|
4
|
+
# Mixin providing tool calling and structured output configuration for Client.
|
|
5
|
+
# rubocop:disable Metrics/ModuleLength
|
|
6
|
+
module ToolSerializer
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# Configure tools and structured outputs, returning forced_extraction flag
|
|
10
|
+
def configure_tools_and_structured_outputs!(parameters, opts)
|
|
11
|
+
configure_tool_calling!(parameters, opts)
|
|
12
|
+
configure_structured_outputs!(parameters, opts)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def configure_tool_calling!(parameters, opts)
|
|
16
|
+
return unless opts.tools?
|
|
17
|
+
|
|
18
|
+
warn_if_unsupported(opts.model, :function_calling, "tool calling")
|
|
19
|
+
parameters[:tools] = serialize_tools(opts.tools)
|
|
20
|
+
parameters[:tool_choice] = opts.tool_choice if opts.tool_choice
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns forced_extraction boolean
|
|
24
|
+
def configure_structured_outputs!(parameters, opts)
|
|
25
|
+
return false unless opts.response_format?
|
|
26
|
+
|
|
27
|
+
force_extraction = determine_forced_extraction_mode(opts.model, opts.force_structured_output)
|
|
28
|
+
|
|
29
|
+
if force_extraction
|
|
30
|
+
handle_forced_structured_output!(parameters, opts.model, opts.response_format)
|
|
31
|
+
true
|
|
32
|
+
else
|
|
33
|
+
handle_native_structured_output!(parameters, opts.model, opts.response_format)
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def determine_forced_extraction_mode(model, force_structured_output)
|
|
39
|
+
return force_structured_output unless force_structured_output.nil?
|
|
40
|
+
|
|
41
|
+
if model.is_a?(String) &&
|
|
42
|
+
!model.start_with?("openrouter/") &&
|
|
43
|
+
!ModelRegistry.has_capability?(model, :structured_outputs) &&
|
|
44
|
+
configuration.auto_force_on_unsupported_models
|
|
45
|
+
OpenRouter.log_warning("[OpenRouter] Model '#{model}' doesn't support native structured outputs. Automatically using forced extraction mode.")
|
|
46
|
+
true
|
|
47
|
+
else
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def handle_forced_structured_output!(parameters, model, response_format)
|
|
53
|
+
warn_if_unsupported(model, :structured_outputs, "structured outputs") if configuration.strict_mode
|
|
54
|
+
inject_schema_instructions!(parameters[:messages], response_format)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def handle_native_structured_output!(parameters, model, response_format)
|
|
58
|
+
warn_if_unsupported(model, :structured_outputs, "structured outputs")
|
|
59
|
+
parameters[:response_format] = serialize_response_format(response_format)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Serialize tools to Chat Completions API format: { type: "function", function: { name:, parameters: } }
|
|
63
|
+
def serialize_tools(tools)
|
|
64
|
+
tools.map do |tool|
|
|
65
|
+
case tool
|
|
66
|
+
when Tool
|
|
67
|
+
tool.to_h
|
|
68
|
+
when Hash
|
|
69
|
+
tool
|
|
70
|
+
else
|
|
71
|
+
raise ArgumentError, "Tools must be Tool objects or hashes"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Serialize tools to Responses API flat format: { type: "function", name:, parameters: }
|
|
77
|
+
def serialize_tools_for_responses(tools)
|
|
78
|
+
tools.map do |tool|
|
|
79
|
+
tool_hash = case tool
|
|
80
|
+
when Tool
|
|
81
|
+
tool.to_h
|
|
82
|
+
when Hash
|
|
83
|
+
tool.transform_keys(&:to_sym)
|
|
84
|
+
else
|
|
85
|
+
raise ArgumentError, "Tools must be Tool objects or hashes"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if tool_hash[:function]
|
|
89
|
+
{
|
|
90
|
+
type: "function",
|
|
91
|
+
name: tool_hash[:function][:name],
|
|
92
|
+
description: tool_hash[:function][:description],
|
|
93
|
+
parameters: tool_hash[:function][:parameters]
|
|
94
|
+
}.compact
|
|
95
|
+
else
|
|
96
|
+
tool_hash
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def serialize_response_format(response_format)
|
|
102
|
+
case response_format
|
|
103
|
+
when Hash
|
|
104
|
+
if response_format[:json_schema].is_a?(Schema)
|
|
105
|
+
response_format.merge(json_schema: response_format[:json_schema].to_h)
|
|
106
|
+
else
|
|
107
|
+
response_format
|
|
108
|
+
end
|
|
109
|
+
when Schema
|
|
110
|
+
{ type: "json_schema", json_schema: response_format.to_h }
|
|
111
|
+
else
|
|
112
|
+
response_format
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def inject_schema_instructions!(messages, response_format)
|
|
117
|
+
schema = extract_schema(response_format)
|
|
118
|
+
return unless schema
|
|
119
|
+
|
|
120
|
+
instruction_content = if schema.respond_to?(:get_format_instructions)
|
|
121
|
+
schema.get_format_instructions
|
|
122
|
+
else
|
|
123
|
+
build_schema_instruction(schema)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
messages << { role: "system", content: instruction_content }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def extract_schema(response_format)
|
|
130
|
+
case response_format
|
|
131
|
+
when Schema
|
|
132
|
+
response_format
|
|
133
|
+
when Hash
|
|
134
|
+
if response_format[:json_schema].is_a?(Schema)
|
|
135
|
+
response_format[:json_schema]
|
|
136
|
+
elsif response_format[:json_schema].is_a?(Hash)
|
|
137
|
+
response_format[:json_schema]
|
|
138
|
+
else
|
|
139
|
+
response_format
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def build_schema_instruction(schema)
|
|
145
|
+
schema_json = schema.respond_to?(:to_h) ? schema.to_h.to_json : schema.to_json
|
|
146
|
+
|
|
147
|
+
<<~INSTRUCTION
|
|
148
|
+
You must respond with valid JSON matching this exact schema:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
#{schema_json}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Rules:
|
|
155
|
+
- Return ONLY the JSON object, no other text
|
|
156
|
+
- Ensure all required fields are present
|
|
157
|
+
- Match the exact data types specified
|
|
158
|
+
- Follow any format constraints (email, date, etc.)
|
|
159
|
+
- Do not include trailing commas or comments
|
|
160
|
+
INSTRUCTION
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
# rubocop:enable Metrics/ModuleLength
|
|
164
|
+
end
|
data/lib/open_router/version.rb
CHANGED
data/lib/open_router.rb
CHANGED
|
@@ -19,6 +19,7 @@ end
|
|
|
19
19
|
require_relative "open_router/http"
|
|
20
20
|
require_relative "open_router/completion_options"
|
|
21
21
|
require_relative "open_router/tool"
|
|
22
|
+
require_relative "open_router/subagent_tool"
|
|
22
23
|
require_relative "open_router/tool_call_base"
|
|
23
24
|
require_relative "open_router/tool_call"
|
|
24
25
|
require_relative "open_router/schema"
|
|
@@ -30,6 +31,7 @@ require_relative "open_router/model_registry"
|
|
|
30
31
|
require_relative "open_router/model_selector"
|
|
31
32
|
require_relative "open_router/prompt_template"
|
|
32
33
|
require_relative "open_router/usage_tracker"
|
|
34
|
+
require_relative "open_router/routing"
|
|
33
35
|
require_relative "open_router/client"
|
|
34
36
|
require_relative "open_router/streaming_client"
|
|
35
37
|
require_relative "open_router/version"
|
|
@@ -60,6 +62,10 @@ module OpenRouter
|
|
|
60
62
|
# Default structured output mode configuration
|
|
61
63
|
attr_accessor :default_structured_output_mode
|
|
62
64
|
|
|
65
|
+
# Optional logger. When set, gem warnings are routed through it (e.g. Rails.logger).
|
|
66
|
+
# When nil (default), warnings go to Kernel.warn → $stderr.
|
|
67
|
+
attr_accessor :logger
|
|
68
|
+
|
|
63
69
|
DEFAULT_API_VERSION = "v1"
|
|
64
70
|
DEFAULT_REQUEST_TIMEOUT = 120
|
|
65
71
|
DEFAULT_URI_BASE = "https://openrouter.ai/api"
|
|
@@ -130,4 +136,12 @@ module OpenRouter
|
|
|
130
136
|
def self.configure
|
|
131
137
|
yield(configuration)
|
|
132
138
|
end
|
|
139
|
+
|
|
140
|
+
def self.log_warning(message)
|
|
141
|
+
if configuration.logger
|
|
142
|
+
configuration.logger.warn(message)
|
|
143
|
+
else
|
|
144
|
+
Kernel.warn(message)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
133
147
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: open_router_enhanced
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Stiens
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -134,6 +134,8 @@ files:
|
|
|
134
134
|
- docs/responses_api.md
|
|
135
135
|
- docs/streaming.md
|
|
136
136
|
- docs/structured_outputs.md
|
|
137
|
+
- docs/superpowers/plans/2026-06-27-openrouter-routing-features.md
|
|
138
|
+
- docs/superpowers/specs/2026-06-27-openrouter-routing-features-design.md
|
|
137
139
|
- docs/tools.md
|
|
138
140
|
- examples/basic_completion.rb
|
|
139
141
|
- examples/dynamic_model_switching_example.rb
|
|
@@ -148,21 +150,28 @@ files:
|
|
|
148
150
|
- examples/tool_calling_example.rb
|
|
149
151
|
- examples/tool_loop_example.rb
|
|
150
152
|
- lib/open_router.rb
|
|
153
|
+
- lib/open_router/callbacks.rb
|
|
151
154
|
- lib/open_router/client.rb
|
|
152
155
|
- lib/open_router/completion_options.rb
|
|
153
156
|
- lib/open_router/http.rb
|
|
154
157
|
- lib/open_router/json_healer.rb
|
|
155
158
|
- lib/open_router/model_registry.rb
|
|
156
159
|
- lib/open_router/model_selector.rb
|
|
160
|
+
- lib/open_router/parameter_builder.rb
|
|
157
161
|
- lib/open_router/prompt_template.rb
|
|
162
|
+
- lib/open_router/request_handler.rb
|
|
158
163
|
- lib/open_router/response.rb
|
|
164
|
+
- lib/open_router/response_parsing.rb
|
|
159
165
|
- lib/open_router/responses_response.rb
|
|
160
166
|
- lib/open_router/responses_tool_call.rb
|
|
167
|
+
- lib/open_router/routing.rb
|
|
161
168
|
- lib/open_router/schema.rb
|
|
162
169
|
- lib/open_router/streaming_client.rb
|
|
170
|
+
- lib/open_router/subagent_tool.rb
|
|
163
171
|
- lib/open_router/tool.rb
|
|
164
172
|
- lib/open_router/tool_call.rb
|
|
165
173
|
- lib/open_router/tool_call_base.rb
|
|
174
|
+
- lib/open_router/tool_serializer.rb
|
|
166
175
|
- lib/open_router/usage_tracker.rb
|
|
167
176
|
- lib/open_router/version.rb
|
|
168
177
|
- sig/open_router.rbs
|