open_router_enhanced 1.2.5 → 2.0.1

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.
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
@@ -13,15 +12,19 @@ module OpenRouter
13
12
  class Client
14
13
  include OpenRouter::HTTP
15
14
 
16
- attr_reader :callbacks, :usage_tracker
15
+ attr_reader :callbacks, :usage_tracker, :configuration
17
16
 
18
17
  # Initializes the client with optional configurations.
19
18
  def initialize(access_token: nil, request_timeout: nil, uri_base: nil, extra_headers: {}, track_usage: true)
20
- OpenRouter.configuration.access_token = access_token if access_token
21
- OpenRouter.configuration.request_timeout = request_timeout if request_timeout
22
- OpenRouter.configuration.uri_base = uri_base if uri_base
23
- OpenRouter.configuration.extra_headers = extra_headers if extra_headers.any?
24
- yield(OpenRouter.configuration) if block_given?
19
+ # Build a per-instance configuration to avoid mutating the global singleton,
20
+ # which would cause credential leakage across Client instances in concurrent use.
21
+ @configuration = OpenRouter.configuration.dup
22
+ @configuration.extra_headers = OpenRouter.configuration.extra_headers.dup
23
+ @configuration.access_token = access_token if access_token
24
+ @configuration.request_timeout = request_timeout if request_timeout
25
+ @configuration.uri_base = uri_base if uri_base
26
+ @configuration.extra_headers = @configuration.extra_headers.merge(extra_headers) if extra_headers.any?
27
+ yield(@configuration) if block_given?
25
28
 
26
29
  # Instance-level tracking of capability warnings to avoid memory leaks
27
30
  @capability_warnings_shown = Set.new
@@ -41,10 +44,6 @@ module OpenRouter
41
44
  @usage_tracker = UsageTracker.new if @track_usage
42
45
  end
43
46
 
44
- def configuration
45
- OpenRouter.configuration
46
- end
47
-
48
47
  # Register a callback for a specific event
49
48
  #
50
49
  # @param event [Symbol] The event to register for (:before_request, :after_response, :on_tool_call, :on_error, :on_stream_chunk, :on_healing)
@@ -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.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.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.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 tools?
248
+ tools.is_a?(Array) && !tools.empty?
249
+ end
250
+
251
+ # Check if response format is configured
252
+ #
253
+ # @return [Boolean]
254
+ def 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