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 +4 -4
- data/CHANGELOG.md +249 -0
- data/Gemfile.lock +1 -1
- data/README.md +83 -0
- data/docs/model_selection.md +30 -0
- data/docs/plugins.md +34 -0
- data/docs/responses_api.md +34 -0
- data/docs/streaming.md +32 -0
- data/docs/structured_outputs.md +31 -0
- data/docs/tools.md +32 -1
- data/lib/open_router/client.rb +173 -70
- data/lib/open_router/completion_options.rb +265 -0
- data/lib/open_router/streaming_client.rb +27 -13
- data/lib/open_router/version.rb +1 -1
- data/lib/open_router.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 925d55d16a222d6a954c449307c480ab85bf3092138b172cd8a2561f40aeeba4
|
|
4
|
+
data.tar.gz: 7f427cb96bdd2a98c355096db04babb861a8bb2a13ab3f7f940c5397a9d4f651
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
data/docs/model_selection.md
CHANGED
|
@@ -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:
|
data/docs/responses_api.md
CHANGED
|
@@ -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.
|
data/docs/structured_outputs.md
CHANGED
|
@@ -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?
|
data/lib/open_router/client.rb
CHANGED
|
@@ -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
|
-
#
|
|
96
|
-
# @param
|
|
97
|
-
# @param
|
|
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
|
-
# @
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
|
156
|
-
# @param
|
|
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
|
-
#
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
416
|
-
|
|
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
|
-
|
|
521
|
+
force_extraction = determine_forced_extraction_mode(opts.model, opts.force_structured_output)
|
|
419
522
|
|
|
420
|
-
if
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
options,
|
|
87
101
|
accumulate_response: false,
|
|
88
|
-
**
|
|
102
|
+
**kwargs
|
|
89
103
|
) do |chunk|
|
|
90
104
|
content = extract_content_from_chunk(chunk)
|
|
91
105
|
block.call(content) if content
|
data/lib/open_router/version.rb
CHANGED
data/lib/open_router.rb
CHANGED
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:
|
|
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-
|
|
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
|