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.
@@ -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
- warn "[OpenRouter] Streaming callback error for #{event}: #{e.message}"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenRouter
4
- VERSION = "2.0.1"
4
+ VERSION = "2.2.0"
5
5
  end
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.1
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-04-16 00:00:00.000000000 Z
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