open_router_enhanced 1.2.5 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 006c0d9fe0bb456345df1e75db430cf54943d85f34c55a931cf3490aa7e8ad45
4
- data.tar.gz: 15953a9890fa76b4d039ff40357e11407bbd03fe44d731926327e65a909e7b94
3
+ metadata.gz: 925d55d16a222d6a954c449307c480ab85bf3092138b172cd8a2561f40aeeba4
4
+ data.tar.gz: 7f427cb96bdd2a98c355096db04babb861a8bb2a13ab3f7f940c5397a9d4f651
5
5
  SHA512:
6
- metadata.gz: 7048aec5f2abd698c35300fea40135c149ea90345c4e9897d14ac1114ba88a85b1b5ddcef77b3dfc1f01993c5dcb1dd922e011a3fb0162dfcdf1ab1b518de933
7
- data.tar.gz: 859c1f7a15f59b11a530eb7eb460e36499782dda46eb76affdf74e63f474f9e40b56d24b4bea65fb6cb08b5438e0186f04b73aed99cf2dd6de2eeebc5a83691d
6
+ metadata.gz: c4cea4727bfa5df23b123e695cd95fe6106f6a57a5184e8b3af9f7c043e3198702ea433fc7d553220c3def45d1adc899b4e53ff4b7fa3ff4b005cf76a580c681
7
+ data.tar.gz: 9544496653c0716f7c704d10f7376e21e20bc4f98fe784114822640ffc957142e3fed056bcdc6d135e333010e2489910c44f7a445aa50ac3dc18eb9fe52bb4ee
data/CHANGELOG.md CHANGED
@@ -1,5 +1,254 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.0.0] - 2025-12-28
4
+
5
+ ### Overview
6
+
7
+ Version 2.0.0 introduces the `CompletionOptions` class - a structured, self-documenting way to configure API requests. This replaces the previous pattern of 11+ keyword arguments with a clean, reusable options object.
8
+
9
+ **This is a semver major release, but existing code will continue to work without modification.** The new patterns are opt-in and recommended for new code.
10
+
11
+ ---
12
+
13
+ ### Breaking Changes
14
+
15
+ Method signatures now accept an optional `CompletionOptions` object as the second parameter:
16
+
17
+ | Method | Old Signature | New Signature |
18
+ |--------|---------------|---------------|
19
+ | `complete` | `(messages, model:, tools:, ...)` | `(messages, options = nil, stream:, **kwargs)` |
20
+ | `stream_complete` | `(messages, model:, ...)` | `(messages, options = nil, accumulate_response:, **kwargs, &block)` |
21
+ | `stream` | `(messages, model:, ...)` | `(messages, options = nil, **kwargs, &block)` |
22
+ | `responses` | `(input, model:, ...)` | `(input, options = nil, **kwargs)` |
23
+
24
+ **Important**: The `options` parameter accepts `CompletionOptions`, `Hash`, or `nil`. All existing keyword argument patterns continue to work.
25
+
26
+ ---
27
+
28
+ ### Migration Guide
29
+
30
+ #### No Changes Required (Backward Compatible)
31
+
32
+ All existing code continues to work:
33
+
34
+ ```ruby
35
+ # ✅ These all work exactly as before:
36
+ client.complete(messages, model: "openai/gpt-4o")
37
+ client.complete(messages, model: "openai/gpt-4o", temperature: 0.7)
38
+ client.stream(messages, model: "openai/gpt-4o") { |chunk| print chunk }
39
+ client.responses("Hello", model: "openai/gpt-4o", reasoning: { effort: "high" })
40
+ ```
41
+
42
+ #### Recommended: Use CompletionOptions for New Code
43
+
44
+ For new code, we recommend using `CompletionOptions` for better IDE support, documentation, and reusability:
45
+
46
+ ```ruby
47
+ # Create reusable configuration
48
+ opts = OpenRouter::CompletionOptions.new(
49
+ model: "openai/gpt-4o",
50
+ temperature: 0.7,
51
+ max_tokens: 1000
52
+ )
53
+
54
+ # Use across multiple calls
55
+ response1 = client.complete(messages1, opts)
56
+ response2 = client.complete(messages2, opts)
57
+
58
+ # Override specific values without mutating original
59
+ creative_opts = opts.merge(temperature: 1.2)
60
+ response3 = client.complete(messages3, creative_opts)
61
+ ```
62
+
63
+ #### Migrating Complex Configurations
64
+
65
+ **Before (v1.x)**:
66
+ ```ruby
67
+ client.complete(
68
+ messages,
69
+ model: "openai/gpt-4o",
70
+ tools: my_tools,
71
+ tool_choice: "auto",
72
+ temperature: 0.7,
73
+ max_tokens: 2000,
74
+ providers: ["openai", "azure"],
75
+ response_format: { type: "json_object" },
76
+ extras: { custom_param: "value" }
77
+ )
78
+ ```
79
+
80
+ **After (v2.0 - recommended)**:
81
+ ```ruby
82
+ opts = OpenRouter::CompletionOptions.new(
83
+ model: "openai/gpt-4o",
84
+ tools: my_tools,
85
+ tool_choice: "auto",
86
+ temperature: 0.7,
87
+ max_tokens: 2000,
88
+ providers: ["openai", "azure"],
89
+ response_format: { type: "json_object" },
90
+ extras: { custom_param: "value" }
91
+ )
92
+
93
+ client.complete(messages, opts)
94
+ ```
95
+
96
+ #### Migrating Streaming Code
97
+
98
+ **Before (v1.x)**:
99
+ ```ruby
100
+ client.stream_complete(
101
+ messages,
102
+ model: "openai/gpt-4o",
103
+ accumulate_response: true
104
+ ) do |chunk|
105
+ print chunk.dig("choices", 0, "delta", "content")
106
+ end
107
+ ```
108
+
109
+ **After (v2.0 - recommended)**:
110
+ ```ruby
111
+ opts = OpenRouter::CompletionOptions.new(model: "openai/gpt-4o")
112
+
113
+ client.stream_complete(messages, opts, accumulate_response: true) do |chunk|
114
+ print chunk.dig("choices", 0, "delta", "content")
115
+ end
116
+
117
+ # Or use the simpler stream method:
118
+ client.stream(messages, opts) { |content| print content }
119
+ ```
120
+
121
+ #### Pattern: Base Options with Per-Request Overrides
122
+
123
+ ```ruby
124
+ # Define base configuration once
125
+ BASE_OPTS = OpenRouter::CompletionOptions.new(
126
+ model: "openai/gpt-4o",
127
+ max_tokens: 1000,
128
+ providers: ["openai"]
129
+ )
130
+
131
+ # Override for specific use cases
132
+ def generate_creative_content(prompt)
133
+ messages = [{ role: "user", content: prompt }]
134
+ client.complete(messages, BASE_OPTS, temperature: 1.2)
135
+ end
136
+
137
+ def generate_factual_content(prompt)
138
+ messages = [{ role: "user", content: prompt }]
139
+ client.complete(messages, BASE_OPTS, temperature: 0.1)
140
+ end
141
+ ```
142
+
143
+ ---
144
+
145
+ ### Added
146
+
147
+ #### `OpenRouter::CompletionOptions` Class
148
+
149
+ A structured configuration object supporting **30+ parameters** organized by category:
150
+
151
+ **Core Parameters**:
152
+ - `model` - Model ID string or array for fallback routing
153
+ - `tools` - Tool/function definitions
154
+ - `tool_choice` - `"auto"`, `"none"`, `"required"`, or specific tool
155
+ - `extras` - Hash for pass-through of any additional/future parameters
156
+
157
+ **Sampling Parameters** (control response randomness):
158
+ - `temperature` - 0.0-2.0, controls randomness (default varies by model)
159
+ - `top_p` - 0.0-1.0, nucleus sampling threshold
160
+ - `top_k` - Integer, limits token selection to top K
161
+ - `frequency_penalty` - -2.0 to 2.0, penalize frequent tokens
162
+ - `presence_penalty` - -2.0 to 2.0, penalize tokens already present
163
+ - `repetition_penalty` - 0.0-2.0, general repetition penalty
164
+ - `min_p` - 0.0-1.0, minimum probability threshold
165
+ - `top_a` - 0.0-1.0, dynamic token filtering
166
+ - `seed` - Integer for reproducible outputs
167
+
168
+ **Output Control**:
169
+ - `max_tokens` - Maximum tokens to generate (legacy)
170
+ - `max_completion_tokens` - Maximum tokens (preferred, newer API)
171
+ - `stop` - String or array of stop sequences
172
+ - `logprobs` - Boolean, return log probabilities
173
+ - `top_logprobs` - 0-20, number of top logprobs per token
174
+ - `logit_bias` - Hash mapping token IDs to bias values (-100 to 100)
175
+ - `response_format` - Structured output schema configuration
176
+ - `parallel_tool_calls` - Boolean, allow parallel function calls
177
+ - `verbosity` - `:low`, `:medium`, `:high`
178
+
179
+ **OpenRouter Routing**:
180
+ - `providers` - Array of provider names (becomes `provider.order`)
181
+ - `provider` - Full provider config hash (overrides `providers`)
182
+ - `transforms` - Array of transform identifiers
183
+ - `plugins` - Array of plugin configs (`web-search`, `response-healing`, etc.)
184
+ - `prediction` - Predicted output for latency optimization
185
+ - `route` - `"fallback"` or `"sort"`
186
+ - `metadata` - Custom key-value metadata
187
+ - `user` - End-user identifier for tracking
188
+ - `session_id` - Session grouping identifier (max 128 chars)
189
+
190
+ **Responses API**:
191
+ - `reasoning` - Hash with `effort:` key (`"minimal"`, `"low"`, `"medium"`, `"high"`)
192
+
193
+ **Client-Side Options** (not sent to API):
194
+ - `force_structured_output` - Override forced extraction mode behavior
195
+
196
+ #### Helper Methods
197
+
198
+ ```ruby
199
+ opts = CompletionOptions.new(model: "gpt-4", tools: [...])
200
+
201
+ opts.has_tools? # => true if tools are defined
202
+ opts.has_response_format? # => true if response_format is set
203
+ opts.fallback_models? # => true if model is an array
204
+
205
+ opts.to_h # => Hash of all non-nil, non-empty values
206
+ opts.to_api_params # => Hash for API request (excludes client-side params)
207
+ opts.merge(temp: 0.5) # => New CompletionOptions with override
208
+ ```
209
+
210
+ ---
211
+
212
+ ### Backward Compatibility
213
+
214
+ **All existing patterns continue to work**:
215
+
216
+ ```ruby
217
+ # Direct kwargs (unchanged from v1.x)
218
+ client.complete(messages, model: "gpt-4")
219
+
220
+ # Hash as second argument
221
+ client.complete(messages, { model: "gpt-4", temperature: 0.7 })
222
+
223
+ # CompletionOptions object (new in v2.0)
224
+ opts = CompletionOptions.new(model: "gpt-4")
225
+ client.complete(messages, opts)
226
+
227
+ # Options with kwargs overrides (new in v2.0)
228
+ client.complete(messages, opts, temperature: 0.9)
229
+ ```
230
+
231
+ The `normalize_options` helper transparently handles all input styles.
232
+
233
+ ---
234
+
235
+ ### Internal Improvements
236
+
237
+ - New `normalize_options` private helper for flexible input handling
238
+ - Refactored `prepare_base_parameters` to accept `CompletionOptions`
239
+ - Refactored `configure_tools_and_structured_outputs!` to use `CompletionOptions`
240
+ - Added focused parameter helpers:
241
+ - `configure_sampling_parameters!`
242
+ - `configure_output_parameters!`
243
+ - `configure_routing_parameters!`
244
+ - Improved separation of concerns in request building
245
+
246
+ ---
247
+
248
+ ### Bug Fixes
249
+
250
+ - Fixed issue where `extras` hash contents were nested incorrectly in API requests. Parameters like `max_tokens` passed via `extras` now correctly appear at the top level of the request body.
251
+
3
252
  ## [1.2.2] - 2025-12-25
4
253
 
5
254
  ### Fixed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- open_router_enhanced (1.2.5)
4
+ open_router_enhanced (2.0.0)
5
5
  activesupport (>= 6.0, < 9.0)
6
6
  dotenv (>= 2.0, < 4.0)
7
7
  faraday (>= 1.0, < 3.0)
data/README.md CHANGED
@@ -198,6 +198,89 @@ end
198
198
 
199
199
  **[Full configuration documentation](docs/configuration.md)**
200
200
 
201
+ ## CompletionOptions
202
+
203
+ For complex requests with many parameters, use `CompletionOptions` to organize your configuration:
204
+
205
+ ```ruby
206
+ # Create reusable options
207
+ opts = OpenRouter::CompletionOptions.new(
208
+ model: "anthropic/claude-3.5-sonnet",
209
+ temperature: 0.7,
210
+ max_tokens: 1000,
211
+ tools: [weather_tool],
212
+ providers: ["anthropic"]
213
+ )
214
+
215
+ # Use with complete()
216
+ response = client.complete(messages, opts)
217
+
218
+ # Override specific values
219
+ response = client.complete(messages, opts, temperature: 0.9)
220
+
221
+ # All these styles work (backward compatible):
222
+ client.complete(messages, model: "gpt-4") # kwargs
223
+ client.complete(messages, { model: "gpt-4" }) # hash
224
+ client.complete(messages, opts) # options object
225
+ client.complete(messages, opts, temperature: 0.5) # options + override
226
+ ```
227
+
228
+ ### Available Parameters
229
+
230
+ ```ruby
231
+ OpenRouter::CompletionOptions.new(
232
+ # Model selection
233
+ model: "gpt-4", # Model ID or array for fallback
234
+ route: "fallback", # Routing strategy
235
+
236
+ # Sampling parameters
237
+ temperature: 0.7, # 0.0-2.0
238
+ top_p: 0.9, # Nucleus sampling
239
+ top_k: 50, # Top-K sampling
240
+ frequency_penalty: 0.5, # -2.0 to 2.0
241
+ presence_penalty: 0.3, # -2.0 to 2.0
242
+ repetition_penalty: 1.1, # 0.0-2.0
243
+ min_p: 0.05, # Minimum probability
244
+ top_a: 0.1, # Dynamic filtering
245
+ seed: 42, # Reproducibility
246
+
247
+ # Output control
248
+ max_tokens: 1000, # Max completion tokens
249
+ max_completion_tokens: 1000, # Preferred over max_tokens
250
+ stop: ["\n", "END"], # Stop sequences
251
+ logprobs: true, # Return log probabilities
252
+ top_logprobs: 5, # Number of top logprobs
253
+ logit_bias: { "50256" => -100 }, # Token biasing
254
+ response_format: schema, # Structured output
255
+ parallel_tool_calls: true, # Allow parallel calls
256
+
257
+ # Tools
258
+ tools: [weather_tool], # Tool definitions
259
+ tool_choice: "auto", # auto, none, required, or specific
260
+
261
+ # OpenRouter routing
262
+ providers: ["anthropic", "openai"], # Simple provider ordering
263
+ provider: { # Full provider config
264
+ order: ["anthropic"],
265
+ quantizations: ["fp16"],
266
+ allow_fallbacks: true
267
+ },
268
+ transforms: ["middle-out"], # Message transforms
269
+ plugins: [{ id: "web-search" }], # OpenRouter plugins
270
+ prediction: { type: "content", content: "..." }, # Latency optimization
271
+ metadata: { request_id: "abc123" }, # Custom metadata
272
+ user: "user_123", # User identifier
273
+ session_id: "session_456", # Session grouping
274
+
275
+ # Responses API
276
+ reasoning: { effort: "high" }, # For reasoning models
277
+
278
+ # Client-side
279
+ force_structured_output: true, # Force schema injection
280
+ extras: { custom_param: "value" } # Pass-through params
281
+ )
282
+ ```
283
+
201
284
  ## Features
202
285
 
203
286
  ### Tool Calling
@@ -15,6 +15,36 @@ model = OpenRouter::ModelSelector.new
15
15
  response = client.complete(messages, model: model, tools: tools)
16
16
  ```
17
17
 
18
+ ## Using CompletionOptions with Model Selection (v2.0+)
19
+
20
+ Combine model selection with `CompletionOptions` for powerful, reusable configurations:
21
+
22
+ ```ruby
23
+ # Select model dynamically
24
+ model = OpenRouter::ModelSelector.new
25
+ .require(:function_calling, :structured_outputs)
26
+ .optimize_for(:cost)
27
+ .choose
28
+
29
+ # Create reusable options with the selected model
30
+ opts = OpenRouter::CompletionOptions.new(
31
+ model: model,
32
+ tools: my_tools,
33
+ max_tokens: 1000
34
+ )
35
+
36
+ # Use across multiple calls
37
+ response = client.complete(messages, opts)
38
+
39
+ # Or use fallback model arrays directly in options
40
+ fallback_opts = OpenRouter::CompletionOptions.new(
41
+ model: ["anthropic/claude-3.5-sonnet", "openai/gpt-4o", "google/gemini-pro"],
42
+ route: "fallback"
43
+ )
44
+ ```
45
+
46
+ All keyword argument patterns continue to work for backward compatibility.
47
+
18
48
  ## ModelSelector API
19
49
 
20
50
  The `ModelSelector` class provides a fluent interface for building complex model selection criteria.
data/docs/plugins.md CHANGED
@@ -31,6 +31,40 @@ response = client.complete(
31
31
  )
32
32
  ```
33
33
 
34
+ ## Using CompletionOptions with Plugins (v2.0+)
35
+
36
+ For cleaner, reusable configurations, use the `CompletionOptions` class:
37
+
38
+ ```ruby
39
+ # Create reusable plugin configuration
40
+ web_search_opts = OpenRouter::CompletionOptions.new(
41
+ model: "openai/gpt-4o-mini",
42
+ plugins: [{ id: "web-search" }]
43
+ )
44
+
45
+ # Use across multiple calls
46
+ response = client.complete(messages, web_search_opts)
47
+
48
+ # Combine multiple plugins with other options
49
+ research_opts = OpenRouter::CompletionOptions.new(
50
+ model: "openai/gpt-4o",
51
+ plugins: [
52
+ { id: "web-search" },
53
+ { id: "pdf-inputs" }
54
+ ],
55
+ max_tokens: 2000,
56
+ temperature: 0.3
57
+ )
58
+
59
+ # Add prediction for latency optimization
60
+ fast_opts = OpenRouter::CompletionOptions.new(
61
+ model: "openai/gpt-4o",
62
+ prediction: { type: "content", content: "The answer is..." }
63
+ )
64
+ ```
65
+
66
+ All keyword argument patterns continue to work for backward compatibility.
67
+
34
68
  ## Response Healing Plugin
35
69
 
36
70
  The response-healing plugin fixes common JSON formatting issues server-side:
@@ -17,6 +17,40 @@ response = client.responses(
17
17
  puts response.content # => "Paris"
18
18
  ```
19
19
 
20
+ ## Using CompletionOptions (v2.0+)
21
+
22
+ For cleaner, reusable configurations, use the `CompletionOptions` class:
23
+
24
+ ```ruby
25
+ # Create reusable reasoning configuration
26
+ reasoning_opts = OpenRouter::CompletionOptions.new(
27
+ model: "openai/o4-mini",
28
+ reasoning: { effort: "high" },
29
+ max_tokens: 1000
30
+ )
31
+
32
+ # Use with responses
33
+ response = client.responses("Explain quantum entanglement", reasoning_opts)
34
+
35
+ # Override specific values per-request
36
+ response = client.responses(
37
+ "What is 15% of 80?",
38
+ reasoning_opts,
39
+ reasoning: { effort: "low" } # Simpler problem, less reasoning needed
40
+ )
41
+
42
+ # Create tool-enabled responses configuration
43
+ tool_opts = OpenRouter::CompletionOptions.new(
44
+ model: "openai/gpt-4o-mini",
45
+ tools: [weather_tool, calculator_tool],
46
+ tool_choice: "auto"
47
+ )
48
+
49
+ response = client.responses("What's the weather in Tokyo?", tool_opts)
50
+ ```
51
+
52
+ All keyword argument patterns continue to work for backward compatibility.
53
+
20
54
  ## With Reasoning
21
55
 
22
56
  The Responses API supports reasoning with configurable effort levels:
data/docs/streaming.md CHANGED
@@ -22,6 +22,38 @@ response = streaming_client.stream_complete(
22
22
  puts response.content # Complete response after streaming
23
23
  ```
24
24
 
25
+ ## Using CompletionOptions (v2.0+)
26
+
27
+ For cleaner, reusable configurations, use the `CompletionOptions` class:
28
+
29
+ ```ruby
30
+ # Create reusable streaming configuration
31
+ stream_opts = OpenRouter::CompletionOptions.new(
32
+ model: "openai/gpt-4o-mini",
33
+ temperature: 0.8,
34
+ max_tokens: 2000
35
+ )
36
+
37
+ # Use with stream_complete
38
+ response = streaming_client.stream_complete(
39
+ messages,
40
+ stream_opts,
41
+ accumulate_response: true
42
+ )
43
+
44
+ # Use with simple stream method
45
+ streaming_client.stream(messages, stream_opts) do |chunk|
46
+ print chunk
47
+ end
48
+
49
+ # Override specific values per-request
50
+ streaming_client.stream(messages, stream_opts, temperature: 0.2) do |chunk|
51
+ print chunk
52
+ end
53
+ ```
54
+
55
+ All keyword argument patterns continue to work for backward compatibility.
56
+
25
57
  ## Streaming with Callbacks
26
58
 
27
59
  The streaming client supports extensive callback events for monitoring and custom processing.
@@ -35,6 +35,37 @@ puts user["age"] # => 30
35
35
  puts user["email"] # => "john@example.com"
36
36
  ```
37
37
 
38
+ ## Using CompletionOptions (v2.0+)
39
+
40
+ For cleaner, reusable configurations, use the `CompletionOptions` class:
41
+
42
+ ```ruby
43
+ # Create reusable structured output configuration
44
+ struct_opts = OpenRouter::CompletionOptions.new(
45
+ model: "openai/gpt-4o",
46
+ response_format: user_schema,
47
+ max_tokens: 500,
48
+ temperature: 0.1 # Lower temperature for more deterministic outputs
49
+ )
50
+
51
+ # Use across multiple calls
52
+ response1 = client.complete(messages1, struct_opts)
53
+ response2 = client.complete(messages2, struct_opts)
54
+
55
+ # Override for specific requests
56
+ creative_opts = struct_opts.merge(temperature: 0.8)
57
+ response3 = client.complete(messages3, creative_opts)
58
+
59
+ # Control forced structured output behavior
60
+ opts_with_force = OpenRouter::CompletionOptions.new(
61
+ model: "some-model",
62
+ response_format: schema,
63
+ force_structured_output: true # Override auto-detection
64
+ )
65
+ ```
66
+
67
+ All keyword argument patterns continue to work for backward compatibility.
68
+
38
69
  ## Key Concepts: JSON Content vs Structured Outputs
39
70
 
40
71
  **Important: These are fundamentally different features.**
data/docs/tools.md CHANGED
@@ -9,7 +9,7 @@ The OpenRouter gem provides comprehensive support for OpenRouter's function call
9
9
  weather_tool = OpenRouter::Tool.define do
10
10
  name "get_weather"
11
11
  description "Get current weather for a location"
12
-
12
+
13
13
  parameters do
14
14
  string :location, required: true, description: "City name or coordinates"
15
15
  string :units, enum: ["celsius", "fahrenheit"], description: "Temperature units"
@@ -23,6 +23,37 @@ response = client.complete(
23
23
  tools: [weather_tool],
24
24
  tool_choice: "auto"
25
25
  )
26
+ ```
27
+
28
+ ## Using CompletionOptions (v2.0+)
29
+
30
+ For cleaner, reusable configurations, use the `CompletionOptions` class:
31
+
32
+ ```ruby
33
+ # Create reusable tool configuration
34
+ tool_opts = OpenRouter::CompletionOptions.new(
35
+ model: "anthropic/claude-3.5-sonnet",
36
+ tools: [weather_tool, calculator_tool],
37
+ tool_choice: "auto",
38
+ max_tokens: 1000
39
+ )
40
+
41
+ # Use with complete
42
+ response = client.complete(messages, tool_opts)
43
+
44
+ # Override tool_choice per request
45
+ response = client.complete(messages, tool_opts, tool_choice: "required")
46
+
47
+ # Configure parallel tool calling
48
+ parallel_opts = OpenRouter::CompletionOptions.new(
49
+ model: "openai/gpt-4o",
50
+ tools: tools,
51
+ tool_choice: "auto",
52
+ parallel_tool_calls: true # Allow multiple tool calls simultaneously
53
+ )
54
+ ```
55
+
56
+ All keyword argument patterns continue to work for backward compatibility.
26
57
 
27
58
  # Handle tool calls
28
59
  if response.has_tool_calls?
@@ -4,7 +4,6 @@ require "active_support/core_ext/object/blank"
4
4
  require "active_support/core_ext/hash/indifferent_access"
5
5
 
6
6
  require_relative "http"
7
- require "pry"
8
7
 
9
8
  module OpenRouter
10
9
  class ServerError < StandardError; end
@@ -92,27 +91,31 @@ module OpenRouter
92
91
  end
93
92
 
94
93
  # Performs a chat completion request to the OpenRouter API.
95
- # @param messages [Array<Hash>] Array of message hashes with role and content, like [{role: "user", content: "What is the meaning of life?"}]
96
- # @param model [String|Array] Model identifier, or array of model identifiers if you want to fallback to the next model in case of failure
97
- # @param providers [Array<String>] Optional array of provider identifiers, ordered by priority
98
- # @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
99
- # @param plugins [Array<Hash>] Optional array of plugin hashes like [{id: "response-healing"}]. Available plugins: response-healing, web-search, pdf-inputs
100
- # @param tools [Array<Tool>] Optional array of Tool objects or tool definition hashes for function calling
101
- # @param tool_choice [String|Hash] Optional tool choice: "auto", "none", "required", or specific tool selection
102
- # @param response_format [Hash] Optional response format for structured outputs
103
- # @param prediction [Hash] Optional predicted output for latency reduction, e.g. {type: "content", content: "predicted text"}
104
- # @param extras [Hash] Optional hash of model-specific parameters to send to the OpenRouter API
94
+ #
95
+ # @param messages [Array<Hash>] Array of message hashes with role and content
96
+ # @param options [CompletionOptions, Hash, nil] Options object or hash with configuration
105
97
  # @param stream [Proc, nil] Optional callable object for streaming
106
- # @return [Response] The completion response wrapped in a Response object.
107
- # rubocop:disable Metrics/ParameterLists
108
- def complete(messages, model: "openrouter/auto", providers: [], transforms: [], plugins: [], tools: [], tool_choice: nil,
109
- response_format: nil, force_structured_output: nil, prediction: nil, extras: {}, stream: nil)
110
- # rubocop:enable Metrics/ParameterLists
111
- parameters = prepare_base_parameters(messages, model, providers, transforms, plugins, prediction, stream, extras)
112
- forced_extraction = configure_tools_and_structured_outputs!(parameters, model, tools, tool_choice,
113
- response_format, force_structured_output)
114
- configure_plugins!(parameters, response_format, stream)
115
- validate_vision_support(model, messages)
98
+ # @param kwargs [Hash] Additional options (merged with options parameter)
99
+ # @return [Response] The completion response wrapped in a Response object
100
+ #
101
+ # @example Simple usage (unchanged)
102
+ # client.complete(messages, model: "gpt-4")
103
+ #
104
+ # @example With CompletionOptions
105
+ # opts = CompletionOptions.new(model: "gpt-4", temperature: 0.7, tools: my_tools)
106
+ # client.complete(messages, opts)
107
+ #
108
+ # @example Hash options
109
+ # client.complete(messages, { model: "gpt-4", temperature: 0.7 })
110
+ #
111
+ # @example Options with override
112
+ # client.complete(messages, base_opts, temperature: 0.9)
113
+ def complete(messages, options = nil, stream: nil, **kwargs)
114
+ opts = normalize_options(options, kwargs)
115
+ parameters = prepare_base_parameters(messages, opts, stream)
116
+ forced_extraction = configure_tools_and_structured_outputs!(parameters, opts)
117
+ configure_plugins!(parameters, opts.response_format, stream)
118
+ validate_vision_support(opts.model, messages)
116
119
 
117
120
  # Trigger before_request callbacks
118
121
  trigger_callbacks(:before_request, parameters)
@@ -120,10 +123,11 @@ module OpenRouter
120
123
  raw_response = execute_request(parameters)
121
124
  validate_response!(raw_response, stream)
122
125
 
123
- response = build_response(raw_response, response_format, forced_extraction)
126
+ response = build_response(raw_response, opts.response_format, forced_extraction)
124
127
 
125
128
  # Track usage if enabled
126
- @usage_tracker&.track(response, model: model.is_a?(String) ? model : model.first)
129
+ model_for_tracking = opts.model.is_a?(String) ? opts.model : opts.model.first
130
+ @usage_tracker&.track(response, model: model_for_tracking)
127
131
 
128
132
  # Trigger after_response callbacks
129
133
  trigger_callbacks(:after_response, response)
@@ -152,39 +156,42 @@ module OpenRouter
152
156
  # This is an OpenAI-compatible stateless API with support for reasoning.
153
157
  #
154
158
  # @param input [String, Array] The input text or structured message array
155
- # @param model [String] Model identifier (e.g., "openai/o4-mini")
156
- # @param reasoning [Hash, nil] Optional reasoning config, e.g. {effort: "high"}
157
- # Effort levels: "minimal", "low", "medium", "high"
158
- # @param tools [Array<Tool, Hash>] Optional array of tool definitions
159
- # @param tool_choice [String, Hash, nil] Optional: "auto", "none", "required", or specific tool
160
- # @param max_output_tokens [Integer, nil] Maximum tokens to generate
161
- # @param temperature [Float, nil] Sampling temperature (0-2)
162
- # @param top_p [Float, nil] Nucleus sampling parameter (0-1)
163
- # @param extras [Hash] Additional parameters to pass to the API
159
+ # @param options [CompletionOptions, Hash, nil] Options object or hash with configuration
160
+ # @param kwargs [Hash] Additional options (merged with options parameter)
164
161
  # @return [ResponsesResponse] The response wrapped in a ResponsesResponse object
165
162
  #
166
163
  # @example Basic usage
167
164
  # response = client.responses("What is 2+2?", model: "openai/o4-mini")
168
165
  # puts response.content
169
166
  #
170
- # @example With reasoning
171
- # response = client.responses(
172
- # "Solve this step by step: What is 15% of 80?",
167
+ # @example With reasoning using CompletionOptions
168
+ # opts = CompletionOptions.new(
173
169
  # model: "openai/o4-mini",
174
170
  # reasoning: { effort: "high" }
175
171
  # )
172
+ # response = client.responses("Solve this step by step: What is 15% of 80?", opts)
176
173
  # puts response.reasoning_summary
177
174
  # puts response.content
178
- def responses(input, model:, reasoning: nil, tools: [], tool_choice: nil,
179
- max_output_tokens: nil, temperature: nil, top_p: nil, extras: {})
180
- parameters = { model: model, input: input }
181
- parameters[:reasoning] = reasoning if reasoning
182
- parameters[:tools] = serialize_tools_for_responses(tools) if tools.any?
183
- parameters[:tool_choice] = tool_choice if tool_choice
184
- parameters[:max_output_tokens] = max_output_tokens if max_output_tokens
185
- parameters[:temperature] = temperature if temperature
186
- parameters[:top_p] = top_p if top_p
187
- parameters.merge!(extras)
175
+ #
176
+ # @example With kwargs (still works)
177
+ # response = client.responses("Question", model: "openai/o4-mini", reasoning: { effort: "high" })
178
+ def responses(input, options = nil, **kwargs)
179
+ opts = normalize_options(options, kwargs)
180
+
181
+ # Model is required for Responses API
182
+ if opts.model == "openrouter/auto"
183
+ raise ArgumentError, "model is required for responses API (cannot use default 'openrouter/auto')"
184
+ end
185
+
186
+ parameters = { model: opts.model, input: input }
187
+ parameters[:reasoning] = opts.reasoning if opts.reasoning
188
+ parameters[:tools] = serialize_tools_for_responses(opts.tools) if opts.has_tools?
189
+ parameters[:tool_choice] = opts.tool_choice if opts.tool_choice
190
+ # Prefer max_completion_tokens over max_tokens (consistent with complete() method)
191
+ parameters[:max_output_tokens] = opts.max_completion_tokens || opts.max_tokens if opts.max_completion_tokens || opts.max_tokens
192
+ parameters[:temperature] = opts.temperature if opts.temperature
193
+ parameters[:top_p] = opts.top_p if opts.top_p
194
+ parameters.merge!(opts.extras || {})
188
195
 
189
196
  raw = post(path: "/responses", parameters: parameters)
190
197
  ResponsesResponse.new(raw)
@@ -314,18 +321,47 @@ module OpenRouter
314
321
 
315
322
  private
316
323
 
324
+ # Normalize options from various input formats into CompletionOptions
325
+ #
326
+ # @param options [CompletionOptions, Hash, nil] Options object or hash
327
+ # @param kwargs [Hash] Additional keyword arguments
328
+ # @return [CompletionOptions] Normalized options object
329
+ def normalize_options(options, kwargs)
330
+ case options
331
+ when CompletionOptions
332
+ kwargs.empty? ? options : options.merge(**kwargs)
333
+ when Hash
334
+ # Symbolize keys to handle both string and symbol key hashes
335
+ symbolized = options.transform_keys(&:to_sym)
336
+ CompletionOptions.new(**symbolized.merge(kwargs))
337
+ when nil
338
+ CompletionOptions.new(**kwargs)
339
+ else
340
+ raise ArgumentError, "options must be CompletionOptions, Hash, or nil"
341
+ end
342
+ end
343
+
317
344
  # Prepare the base parameters for the API request
318
- def prepare_base_parameters(messages, model, providers, transforms, plugins, prediction, stream, extras)
345
+ #
346
+ # @param messages [Array<Hash>] Array of message hashes
347
+ # @param opts [CompletionOptions] Normalized options object
348
+ # @param stream [Proc, nil] Optional streaming handler
349
+ # @return [Hash] Parameters hash for the API request
350
+ def prepare_base_parameters(messages, opts, stream)
319
351
  parameters = { messages: messages.dup }
320
352
 
321
- configure_model_parameter!(parameters, model)
322
- configure_provider_parameter!(parameters, providers)
323
- configure_transforms_parameter!(parameters, transforms)
324
- configure_plugins_parameter!(parameters, plugins)
325
- configure_prediction_parameter!(parameters, prediction)
353
+ configure_model_parameter!(parameters, opts.model)
354
+ configure_provider_parameter!(parameters, opts)
355
+ configure_transforms_parameter!(parameters, opts.transforms)
356
+ configure_plugins_parameter!(parameters, opts.plugins)
357
+ configure_prediction_parameter!(parameters, opts.prediction)
326
358
  configure_stream_parameter!(parameters, stream)
359
+ configure_sampling_parameters!(parameters, opts)
360
+ configure_output_parameters!(parameters, opts)
361
+ configure_routing_parameters!(parameters, opts)
327
362
 
328
- parameters.merge!(extras)
363
+ # Merge any extras last (allows overriding anything)
364
+ parameters.merge!(opts.extras || {})
329
365
  parameters
330
366
  end
331
367
 
@@ -339,9 +375,66 @@ module OpenRouter
339
375
  end
340
376
  end
341
377
 
342
- # Configure the provider parameter if providers are specified
343
- def configure_provider_parameter!(parameters, providers)
344
- parameters[:provider] = { order: providers } if providers.any?
378
+ # Configure the provider parameter from options
379
+ #
380
+ # @param parameters [Hash] Request parameters hash
381
+ # @param opts [CompletionOptions] Options object
382
+ def configure_provider_parameter!(parameters, opts)
383
+ # Full provider config takes precedence over simple providers array
384
+ if opts.provider && !opts.provider.empty?
385
+ parameters[:provider] = opts.provider
386
+ elsif opts.providers.any?
387
+ parameters[:provider] = { order: opts.providers }
388
+ end
389
+
390
+ # Route parameter for fallback models
391
+ parameters[:route] = opts.route if opts.route
392
+ end
393
+
394
+ # Configure sampling parameters (temperature, top_p, etc.)
395
+ #
396
+ # @param parameters [Hash] Request parameters hash
397
+ # @param opts [CompletionOptions] Options object
398
+ def configure_sampling_parameters!(parameters, opts)
399
+ parameters[:temperature] = opts.temperature if opts.temperature
400
+ parameters[:top_p] = opts.top_p if opts.top_p
401
+ parameters[:top_k] = opts.top_k if opts.top_k
402
+ parameters[:frequency_penalty] = opts.frequency_penalty if opts.frequency_penalty
403
+ parameters[:presence_penalty] = opts.presence_penalty if opts.presence_penalty
404
+ parameters[:repetition_penalty] = opts.repetition_penalty if opts.repetition_penalty
405
+ parameters[:min_p] = opts.min_p if opts.min_p
406
+ parameters[:top_a] = opts.top_a if opts.top_a
407
+ parameters[:seed] = opts.seed if opts.seed
408
+ end
409
+
410
+ # Configure output control parameters
411
+ #
412
+ # @param parameters [Hash] Request parameters hash
413
+ # @param opts [CompletionOptions] Options object
414
+ def configure_output_parameters!(parameters, opts)
415
+ # Prefer max_completion_tokens over max_tokens if both are set
416
+ if opts.max_completion_tokens
417
+ parameters[:max_completion_tokens] = opts.max_completion_tokens
418
+ elsif opts.max_tokens
419
+ parameters[:max_tokens] = opts.max_tokens
420
+ end
421
+
422
+ parameters[:stop] = opts.stop if opts.stop
423
+ parameters[:logprobs] = opts.logprobs unless opts.logprobs.nil?
424
+ parameters[:top_logprobs] = opts.top_logprobs if opts.top_logprobs
425
+ parameters[:logit_bias] = opts.logit_bias if opts.logit_bias && !opts.logit_bias.empty?
426
+ parameters[:parallel_tool_calls] = opts.parallel_tool_calls unless opts.parallel_tool_calls.nil?
427
+ parameters[:verbosity] = opts.verbosity if opts.verbosity
428
+ end
429
+
430
+ # Configure OpenRouter-specific routing parameters
431
+ #
432
+ # @param parameters [Hash] Request parameters hash
433
+ # @param opts [CompletionOptions] Options object
434
+ def configure_routing_parameters!(parameters, opts)
435
+ parameters[:metadata] = opts.metadata if opts.metadata && !opts.metadata.empty?
436
+ parameters[:user] = opts.user if opts.user
437
+ parameters[:session_id] = opts.session_id if opts.session_id
345
438
  end
346
439
 
347
440
  # Configure the transforms parameter if transforms are specified
@@ -396,32 +489,42 @@ module OpenRouter
396
489
  end
397
490
 
398
491
  # Configure tools and structured outputs, returning forced_extraction flag
399
- def configure_tools_and_structured_outputs!(parameters, model, tools, tool_choice, response_format,
400
- force_structured_output)
401
- configure_tool_calling!(parameters, model, tools, tool_choice)
402
- configure_structured_outputs!(parameters, model, response_format, force_structured_output)
492
+ #
493
+ # @param parameters [Hash] Request parameters hash
494
+ # @param opts [CompletionOptions] Options object
495
+ # @return [Boolean] Whether forced extraction mode is being used
496
+ def configure_tools_and_structured_outputs!(parameters, opts)
497
+ configure_tool_calling!(parameters, opts)
498
+ configure_structured_outputs!(parameters, opts)
403
499
  end
404
500
 
405
501
  # Configure tool calling support
406
- def configure_tool_calling!(parameters, model, tools, tool_choice)
407
- return unless tools.any?
502
+ #
503
+ # @param parameters [Hash] Request parameters hash
504
+ # @param opts [CompletionOptions] Options object
505
+ def configure_tool_calling!(parameters, opts)
506
+ return unless opts.has_tools?
408
507
 
409
- warn_if_unsupported(model, :function_calling, "tool calling")
410
- parameters[:tools] = serialize_tools(tools)
411
- parameters[:tool_choice] = tool_choice if tool_choice
508
+ warn_if_unsupported(opts.model, :function_calling, "tool calling")
509
+ parameters[:tools] = serialize_tools(opts.tools)
510
+ parameters[:tool_choice] = opts.tool_choice if opts.tool_choice
412
511
  end
413
512
 
414
513
  # Configure structured output support and return forced_extraction flag
415
- def configure_structured_outputs!(parameters, model, response_format, force_structured_output)
416
- return false unless response_format
514
+ #
515
+ # @param parameters [Hash] Request parameters hash
516
+ # @param opts [CompletionOptions] Options object
517
+ # @return [Boolean] Whether forced extraction mode is being used
518
+ def configure_structured_outputs!(parameters, opts)
519
+ return false unless opts.has_response_format?
417
520
 
418
- force_structured_output = determine_forced_extraction_mode(model, force_structured_output)
521
+ force_extraction = determine_forced_extraction_mode(opts.model, opts.force_structured_output)
419
522
 
420
- if force_structured_output
421
- handle_forced_structured_output!(parameters, model, response_format)
523
+ if force_extraction
524
+ handle_forced_structured_output!(parameters, opts.model, opts.response_format)
422
525
  true
423
526
  else
424
- handle_native_structured_output!(parameters, model, response_format)
527
+ handle_native_structured_output!(parameters, opts.model, opts.response_format)
425
528
  false
426
529
  end
427
530
  end
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenRouter
4
+ # CompletionOptions provides a structured way to configure API requests.
5
+ #
6
+ # Supports all OpenRouter API parameters plus client-side options.
7
+ # Can be used with complete(), stream_complete(), and responses() methods.
8
+ #
9
+ # @example Simple usage with kwargs (unchanged)
10
+ # client.complete(messages, model: "gpt-4")
11
+ #
12
+ # @example Using CompletionOptions for complex requests
13
+ # options = OpenRouter::CompletionOptions.new(
14
+ # model: "anthropic/claude-3.5-sonnet",
15
+ # temperature: 0.7,
16
+ # tools: [weather_tool],
17
+ # providers: ["anthropic"]
18
+ # )
19
+ # client.complete(messages, options)
20
+ #
21
+ # @example Merging options with overrides
22
+ # base_opts = CompletionOptions.new(model: "gpt-4", temperature: 0.5)
23
+ # client.complete(messages, base_opts, temperature: 0.9) # overrides temperature
24
+ #
25
+ class CompletionOptions
26
+ # ═══════════════════════════════════════════════════════════════════════════
27
+ # Common params (used by both Complete and Responses APIs)
28
+ # ═══════════════════════════════════════════════════════════════════════════
29
+
30
+ # @return [String, Array<String>] Model ID or array for fallback routing
31
+ attr_accessor :model
32
+
33
+ # @return [Array<Tool, Hash>] Tool/function definitions for function calling
34
+ attr_accessor :tools
35
+
36
+ # @return [String, Hash, nil] Tool selection: "auto", "none", "required", or specific
37
+ attr_accessor :tool_choice
38
+
39
+ # @return [Hash] Pass-through for any additional/future API params
40
+ attr_accessor :extras
41
+
42
+ # ═══════════════════════════════════════════════════════════════════════════
43
+ # Sampling parameters (OpenRouter passes these to underlying models)
44
+ # ═══════════════════════════════════════════════════════════════════════════
45
+
46
+ # @return [Float, nil] Sampling temperature (0.0-2.0, default 1.0)
47
+ attr_accessor :temperature
48
+
49
+ # @return [Float, nil] Nucleus sampling (0.0-1.0)
50
+ attr_accessor :top_p
51
+
52
+ # @return [Integer, nil] Limits token selection to top K options
53
+ attr_accessor :top_k
54
+
55
+ # @return [Float, nil] Frequency penalty (-2.0 to 2.0)
56
+ attr_accessor :frequency_penalty
57
+
58
+ # @return [Float, nil] Presence penalty (-2.0 to 2.0)
59
+ attr_accessor :presence_penalty
60
+
61
+ # @return [Float, nil] Repetition penalty (0.0-2.0)
62
+ attr_accessor :repetition_penalty
63
+
64
+ # @return [Float, nil] Minimum probability threshold (0.0-1.0)
65
+ attr_accessor :min_p
66
+
67
+ # @return [Float, nil] Dynamic filtering based on confidence (0.0-1.0)
68
+ attr_accessor :top_a
69
+
70
+ # @return [Integer, nil] Random seed for reproducibility
71
+ attr_accessor :seed
72
+
73
+ # ═══════════════════════════════════════════════════════════════════════════
74
+ # Output control
75
+ # ═══════════════════════════════════════════════════════════════════════════
76
+
77
+ # @return [Integer, nil] Legacy max tokens limit
78
+ attr_accessor :max_tokens
79
+
80
+ # @return [Integer, nil] Preferred max completion tokens limit
81
+ attr_accessor :max_completion_tokens
82
+
83
+ # @return [String, Array<String>, nil] Stop sequences
84
+ attr_accessor :stop
85
+
86
+ # @return [Boolean, nil] Return log probabilities of output tokens
87
+ attr_accessor :logprobs
88
+
89
+ # @return [Integer, nil] Number of top logprobs to return (0-20)
90
+ attr_accessor :top_logprobs
91
+
92
+ # @return [Hash, nil] Token ID to bias mapping (-100 to 100)
93
+ attr_accessor :logit_bias
94
+
95
+ # @return [Hash, Schema, nil] Structured output schema/format
96
+ attr_accessor :response_format
97
+
98
+ # @return [Boolean, nil] Allow parallel tool calls
99
+ attr_accessor :parallel_tool_calls
100
+
101
+ # @return [Symbol, String, nil] Output verbosity (:low, :medium, :high)
102
+ attr_accessor :verbosity
103
+
104
+ # ═══════════════════════════════════════════════════════════════════════════
105
+ # OpenRouter-specific routing & features
106
+ # ═══════════════════════════════════════════════════════════════════════════
107
+
108
+ # @return [Array<String>] Simple provider ordering (becomes provider.order)
109
+ attr_accessor :providers
110
+
111
+ # @return [Hash, nil] Full provider config (overrides :providers if set)
112
+ # Supports: order, only, ignore, allow_fallbacks, require_parameters,
113
+ # data_collection, zdr, quantizations, sort, max_price, etc.
114
+ attr_accessor :provider
115
+
116
+ # @return [Array<String>] Transform identifiers (e.g., ["middle-out"])
117
+ attr_accessor :transforms
118
+
119
+ # @return [Array<Hash>] Plugin configurations
120
+ attr_accessor :plugins
121
+
122
+ # @return [Hash, nil] Predicted output for latency reduction
123
+ # Format: { type: "content", content: "predicted text" }
124
+ attr_accessor :prediction
125
+
126
+ # @return [String, nil] Routing strategy: "fallback" or "sort"
127
+ attr_accessor :route
128
+
129
+ # @return [Hash, nil] Custom key-value metadata
130
+ attr_accessor :metadata
131
+
132
+ # @return [String, nil] End-user identifier for tracking
133
+ attr_accessor :user
134
+
135
+ # @return [String, nil] Session grouping identifier (max 128 chars)
136
+ attr_accessor :session_id
137
+
138
+ # ═══════════════════════════════════════════════════════════════════════════
139
+ # Responses API specific
140
+ # ═══════════════════════════════════════════════════════════════════════════
141
+
142
+ # @return [Hash, nil] Reasoning configuration for Responses API
143
+ # Format: { effort: "minimal"|"low"|"medium"|"high" }
144
+ attr_accessor :reasoning
145
+
146
+ # ═══════════════════════════════════════════════════════════════════════════
147
+ # Client-side options (not sent to API)
148
+ # ═══════════════════════════════════════════════════════════════════════════
149
+
150
+ # @return [Boolean, nil] Override forced extraction mode for structured outputs
151
+ # true: Force extraction via system message injection
152
+ # false: Use native structured output
153
+ # nil: Auto-determine based on model capability
154
+ attr_accessor :force_structured_output
155
+
156
+ # All supported parameters with their defaults
157
+ DEFAULTS = {
158
+ # Common
159
+ model: "openrouter/auto",
160
+ tools: [],
161
+ tool_choice: nil,
162
+ extras: {},
163
+ # Sampling
164
+ temperature: nil,
165
+ top_p: nil,
166
+ top_k: nil,
167
+ frequency_penalty: nil,
168
+ presence_penalty: nil,
169
+ repetition_penalty: nil,
170
+ min_p: nil,
171
+ top_a: nil,
172
+ seed: nil,
173
+ # Output
174
+ max_tokens: nil,
175
+ max_completion_tokens: nil,
176
+ stop: nil,
177
+ logprobs: nil,
178
+ top_logprobs: nil,
179
+ logit_bias: nil,
180
+ response_format: nil,
181
+ parallel_tool_calls: nil,
182
+ verbosity: nil,
183
+ # OpenRouter routing
184
+ providers: [],
185
+ provider: nil,
186
+ transforms: [],
187
+ plugins: [],
188
+ prediction: nil,
189
+ route: nil,
190
+ metadata: nil,
191
+ user: nil,
192
+ session_id: nil,
193
+ # Responses API
194
+ reasoning: nil,
195
+ # Client-side
196
+ force_structured_output: nil
197
+ }.freeze
198
+
199
+ # Parameters that are client-side only (not sent to API)
200
+ CLIENT_SIDE_PARAMS = %i[force_structured_output extras].freeze
201
+
202
+ # Initialize with keyword arguments
203
+ #
204
+ # @param attrs [Hash] Parameter values (see DEFAULTS for available keys)
205
+ def initialize(**attrs)
206
+ DEFAULTS.each do |key, default|
207
+ value = attrs.key?(key) ? attrs[key] : default
208
+ # Deep dup arrays/hashes to prevent mutation of shared defaults
209
+ value = value.dup if value.is_a?(Array) || value.is_a?(Hash)
210
+ instance_variable_set(:"@#{key}", value)
211
+ end
212
+ end
213
+
214
+ # Convert to hash, excluding nil values and empty collections
215
+ #
216
+ # @return [Hash] Non-empty parameter values
217
+ def to_h
218
+ DEFAULTS.keys.each_with_object({}) do |key, hash|
219
+ value = instance_variable_get(:"@#{key}")
220
+ next if value.nil?
221
+ next if value.respond_to?(:empty?) && value.empty?
222
+
223
+ hash[key] = value
224
+ end
225
+ end
226
+
227
+ # Create a new CompletionOptions with merged overrides
228
+ #
229
+ # @param overrides [Hash] Values to override
230
+ # @return [CompletionOptions] New instance with merged values
231
+ def merge(**overrides)
232
+ self.class.new(**to_h.merge(overrides))
233
+ end
234
+
235
+ # Build API request parameters hash
236
+ # Excludes client-side-only options and merges extras
237
+ #
238
+ # @return [Hash] Parameters ready for API request
239
+ def to_api_params
240
+ api_params = to_h.reject { |key, _| CLIENT_SIDE_PARAMS.include?(key) }
241
+ api_params.merge(extras || {})
242
+ end
243
+
244
+ # Check if this options object has any tools defined
245
+ #
246
+ # @return [Boolean]
247
+ def has_tools?
248
+ tools.is_a?(Array) && !tools.empty?
249
+ end
250
+
251
+ # Check if response format is configured
252
+ #
253
+ # @return [Boolean]
254
+ def has_response_format?
255
+ !response_format.nil?
256
+ end
257
+
258
+ # Check if using model fallback (array of models)
259
+ #
260
+ # @return [Boolean]
261
+ def fallback_models?
262
+ model.is_a?(Array)
263
+ end
264
+ end
265
+ end
@@ -33,24 +33,34 @@ module OpenRouter
33
33
  # Enhanced streaming completion with better event handling and response reconstruction
34
34
  #
35
35
  # @param messages [Array<Hash>] Array of message hashes
36
- # @param model [String|Array] Model identifier or array of models for fallback
36
+ # @param options [CompletionOptions, Hash, nil] Options object or hash with configuration
37
37
  # @param accumulate_response [Boolean] Whether to accumulate and return complete response
38
- # @param extras [Hash] Additional parameters for the completion request
38
+ # @param kwargs [Hash] Additional options (merged with options parameter)
39
39
  # @param block [Proc] Optional block to call for each chunk (in addition to registered callbacks)
40
40
  # @return [Response, nil] Complete response if accumulate_response is true, nil otherwise
41
- def stream_complete(messages, model: "openrouter/auto", accumulate_response: true, **extras, &block)
41
+ #
42
+ # @example Simple usage (unchanged)
43
+ # client.stream_complete(messages, model: "gpt-4")
44
+ #
45
+ # @example With CompletionOptions
46
+ # opts = CompletionOptions.new(model: "gpt-4", temperature: 0.7)
47
+ # client.stream_complete(messages, opts)
48
+ #
49
+ # @example Options with overrides
50
+ # client.stream_complete(messages, base_opts, temperature: 0.9)
51
+ def stream_complete(messages, options = nil, accumulate_response: true, **kwargs, &block)
52
+ opts = normalize_options(options, kwargs)
42
53
  response_accumulator = ResponseAccumulator.new if accumulate_response
43
54
 
44
55
  # Set up streaming handler (pass optional per-call block)
45
56
  stream_handler = build_stream_handler(response_accumulator, &block)
46
57
 
47
58
  # Trigger start callback
48
- trigger_streaming_callbacks(:on_start, { model: model, messages: messages })
59
+ trigger_streaming_callbacks(:on_start, { model: opts.model, messages: messages })
49
60
 
50
61
  begin
51
- # Execute the streaming request
52
- # Note: extras must be passed as a hash, not splatted, to match complete()'s signature
53
- complete(messages, model: model, stream: stream_handler, extras: extras)
62
+ # Execute the streaming request using parent's complete method
63
+ complete(messages, opts, stream: stream_handler)
54
64
 
55
65
  # Return accumulated response if requested
56
66
  if accumulate_response && response_accumulator
@@ -70,22 +80,26 @@ module OpenRouter
70
80
  # Stream with a simple block interface
71
81
  #
72
82
  # @param messages [Array<Hash>] Array of message hashes
73
- # @param model [String|Array] Model identifier
83
+ # @param options [CompletionOptions, Hash, nil] Options object or hash with configuration
84
+ # @param kwargs [Hash] Additional options (merged with options parameter)
74
85
  # @param block [Proc] Block to call for each content chunk
75
- # @param extras [Hash] Additional parameters
76
86
  #
77
- # @example
87
+ # @example Simple usage (unchanged)
78
88
  # client.stream(messages, model: "openai/gpt-4o-mini") do |chunk|
79
89
  # print chunk
80
90
  # end
81
- def stream(messages, model: "openrouter/auto", **extras, &block)
91
+ #
92
+ # @example With CompletionOptions
93
+ # opts = CompletionOptions.new(model: "gpt-4", temperature: 0.7)
94
+ # client.stream(messages, opts) { |chunk| print chunk }
95
+ def stream(messages, options = nil, **kwargs, &block)
82
96
  raise ArgumentError, "Block required for streaming" unless block_given?
83
97
 
84
98
  stream_complete(
85
99
  messages,
86
- model: model,
100
+ options,
87
101
  accumulate_response: false,
88
- **extras
102
+ **kwargs
89
103
  ) do |chunk|
90
104
  content = extract_content_from_chunk(chunk)
91
105
  block.call(content) if content
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenRouter
4
- VERSION = "1.2.5"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/open_router.rb CHANGED
@@ -17,6 +17,7 @@ module OpenRouter
17
17
  end
18
18
 
19
19
  require_relative "open_router/http"
20
+ require_relative "open_router/completion_options"
20
21
  require_relative "open_router/tool"
21
22
  require_relative "open_router/tool_call_base"
22
23
  require_relative "open_router/tool_call"
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: 1.2.5
4
+ version: 2.0.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: 2025-12-27 00:00:00.000000000 Z
11
+ date: 2025-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -149,6 +149,7 @@ files:
149
149
  - examples/tool_loop_example.rb
150
150
  - lib/open_router.rb
151
151
  - lib/open_router/client.rb
152
+ - lib/open_router/completion_options.rb
152
153
  - lib/open_router/http.rb
153
154
  - lib/open_router/json_healer.rb
154
155
  - lib/open_router/model_registry.rb