open_router_enhanced 1.2.2 → 1.2.3

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.
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Responses API Multi-Turn Tool Loop Example
5
+ # ===========================================
6
+ # This example demonstrates the Responses API, which provides a streamlined
7
+ # way to handle multi-turn tool conversations with automatic input building.
8
+ #
9
+ # The Responses API differs from Chat Completions:
10
+ # - Uses `output` array with typed items instead of `choices`
11
+ # - Provides `build_follow_up_input()` for easy conversation continuation
12
+ # - Tool calls have a flat structure (not nested under `function`)
13
+ # - Supports reasoning output as a first-class type
14
+ #
15
+ # Run with: ruby -I lib examples/responses_api_example.rb
16
+
17
+ require "open_router"
18
+ require "json"
19
+
20
+ # Configure the client
21
+ OpenRouter.configure do |config|
22
+ config.access_token = ENV.fetch("OPENROUTER_API_KEY") do
23
+ abort "Please set OPENROUTER_API_KEY environment variable"
24
+ end
25
+ config.site_name = "Responses API Examples"
26
+ end
27
+
28
+ client = OpenRouter::Client.new
29
+
30
+ # Use a model that supports the Responses API
31
+ MODEL = "openai/gpt-4o-mini"
32
+
33
+ puts "=" * 60
34
+ puts "RESPONSES API TOOL LOOP EXAMPLE"
35
+ puts "=" * 60
36
+
37
+ # -----------------------------------------------------------------------------
38
+ # Define Our Tools
39
+ # -----------------------------------------------------------------------------
40
+
41
+ calculator_tool = OpenRouter::Tool.define do
42
+ name "calculate"
43
+ description "Perform mathematical calculations"
44
+ parameters do
45
+ string :expression, required: true, description: "Math expression to evaluate"
46
+ end
47
+ end
48
+
49
+ weather_tool = OpenRouter::Tool.define do
50
+ name "get_weather"
51
+ description "Get current weather for a location"
52
+ parameters do
53
+ string :location, required: true, description: "City name"
54
+ end
55
+ end
56
+
57
+ stock_tool = OpenRouter::Tool.define do
58
+ name "get_stock_price"
59
+ description "Get current stock price for a ticker symbol"
60
+ parameters do
61
+ string :symbol, required: true, description: "Stock ticker symbol (e.g., AAPL, GOOGL)"
62
+ end
63
+ end
64
+
65
+ TOOLS = [calculator_tool, weather_tool, stock_tool].freeze
66
+
67
+ # -----------------------------------------------------------------------------
68
+ # Tool Execution Function
69
+ # -----------------------------------------------------------------------------
70
+
71
+ def execute_tool(name, args)
72
+ case name
73
+ when "calculate"
74
+ expr = args["expression"]
75
+ # Handle percentage expressions like "15% of 378.90"
76
+ if expr =~ /(\d+(?:\.\d+)?)\s*%\s*of\s*(\d+(?:\.\d+)?)/i
77
+ result = (Regexp.last_match(1).to_f / 100) * Regexp.last_match(2).to_f
78
+ return { result: result.round(2), expression: expr }
79
+ end
80
+ # Standard math expression
81
+ expression = expr.gsub(%r{[^0-9+\-*/.()\s]}, "")
82
+ begin
83
+ { result: eval(expression), expression: expr }
84
+ rescue StandardError
85
+ { error: "Invalid expression" }
86
+ end
87
+
88
+ when "get_weather"
89
+ location = args["location"]
90
+ temp = rand(10..35)
91
+ {
92
+ location: location,
93
+ temperature_celsius: temp,
94
+ conditions: %w[sunny cloudy rainy partly_cloudy].sample,
95
+ humidity: rand(30..90)
96
+ }
97
+
98
+ when "get_stock_price"
99
+ symbol = args["symbol"].upcase
100
+ prices = { "AAPL" => 178.50, "GOOGL" => 141.20, "MSFT" => 378.90, "AMZN" => 185.30 }
101
+ price = prices[symbol] || (100 + rand * 200).round(2)
102
+ {
103
+ symbol: symbol,
104
+ price: price,
105
+ currency: "USD",
106
+ change: (rand * 10 - 5).round(2)
107
+ }
108
+
109
+ else
110
+ { error: "Unknown tool: #{name}" }
111
+ end
112
+ end
113
+
114
+ # -----------------------------------------------------------------------------
115
+ # Example 1: Basic Responses API Call
116
+ # -----------------------------------------------------------------------------
117
+ puts "\n1. BASIC RESPONSES API CALL"
118
+ puts "-" * 40
119
+
120
+ response = client.responses(
121
+ "What is 25 times 17?",
122
+ model: MODEL,
123
+ tools: TOOLS
124
+ )
125
+
126
+ puts "Response ID: #{response.id}"
127
+ puts "Model: #{response.model}"
128
+ puts "Status: #{response.status}"
129
+
130
+ if response.has_tool_calls?
131
+ puts "\nTool calls requested:"
132
+ response.tool_calls.each do |tc|
133
+ puts " - #{tc.name}: #{tc.arguments}"
134
+ end
135
+ end
136
+
137
+ # -----------------------------------------------------------------------------
138
+ # Example 2: execute_tool_calls with Block
139
+ # -----------------------------------------------------------------------------
140
+ puts "\n\n2. EXECUTE_TOOL_CALLS WITH BLOCK"
141
+ puts "-" * 40
142
+
143
+ response = client.responses(
144
+ "What's the weather in Seattle?",
145
+ model: MODEL,
146
+ tools: TOOLS
147
+ )
148
+
149
+ if response.has_tool_calls?
150
+ # Execute all tool calls with a single block
151
+ results = response.execute_tool_calls do |name, args|
152
+ puts " Executing: #{name}(#{args})"
153
+ execute_tool(name, args)
154
+ end
155
+
156
+ puts "\nResults:"
157
+ results.each do |result|
158
+ if result.success?
159
+ puts " Success: #{result.result}"
160
+ else
161
+ puts " Error: #{result.error}"
162
+ end
163
+ end
164
+ end
165
+
166
+ # -----------------------------------------------------------------------------
167
+ # Example 3: Multi-Turn with build_follow_up_input
168
+ # -----------------------------------------------------------------------------
169
+ puts "\n\n3. MULTI-TURN WITH BUILD_FOLLOW_UP_INPUT"
170
+ puts "-" * 40
171
+
172
+ original_query = "What's the weather in Tokyo and what's Apple stock at?"
173
+ puts "User: #{original_query}"
174
+
175
+ # First request
176
+ response = client.responses(
177
+ original_query,
178
+ model: MODEL,
179
+ tools: TOOLS
180
+ )
181
+
182
+ if response.has_tool_calls?
183
+ puts "\nTool calls (Round 1):"
184
+ response.tool_calls.each do |tc|
185
+ puts " - #{tc.name}: #{tc.arguments}"
186
+ end
187
+
188
+ # Execute the tools
189
+ results = response.execute_tool_calls do |name, args|
190
+ execute_tool(name, args)
191
+ end
192
+
193
+ puts "\nExecuted tools, building follow-up..."
194
+
195
+ # Build the follow-up input automatically
196
+ follow_up_input = response.build_follow_up_input(
197
+ original_input: original_query,
198
+ tool_results: results
199
+ )
200
+
201
+ puts "Follow-up input has #{follow_up_input.length} items"
202
+
203
+ # Continue the conversation
204
+ final_response = client.responses(
205
+ follow_up_input,
206
+ model: MODEL,
207
+ tools: TOOLS
208
+ )
209
+
210
+ if final_response.has_tool_calls?
211
+ puts "\nMore tool calls requested (Round 2)..."
212
+ else
213
+ puts "\nAssistant: #{final_response.content}"
214
+ end
215
+ end
216
+
217
+ # -----------------------------------------------------------------------------
218
+ # Example 4: Complete Multi-Turn Loop Until Done
219
+ # -----------------------------------------------------------------------------
220
+ puts "\n\n4. COMPLETE MULTI-TURN LOOP"
221
+ puts "-" * 40
222
+
223
+ query = "I need to: 1) Check the weather in London, 2) Get Microsoft stock price, 3) Calculate 15% of $378.90"
224
+ puts "User: #{query}"
225
+
226
+ current_input = query
227
+ max_rounds = 5
228
+ round = 0
229
+
230
+ loop do
231
+ round += 1
232
+ puts "\n[Round #{round}]"
233
+
234
+ response = client.responses(
235
+ current_input,
236
+ model: MODEL,
237
+ tools: TOOLS
238
+ )
239
+
240
+ if response.has_tool_calls?
241
+ puts "Tool calls:"
242
+ response.tool_calls.each { |tc| puts " - #{tc.name}(#{tc.arguments})" }
243
+
244
+ # Execute tools
245
+ results = response.execute_tool_calls do |name, args|
246
+ result = execute_tool(name, args)
247
+ puts " -> #{result.to_json[0..60]}..."
248
+ result
249
+ end
250
+
251
+ # Build next input
252
+ current_input = response.build_follow_up_input(
253
+ original_input: current_input.is_a?(String) ? current_input : current_input,
254
+ tool_results: results
255
+ )
256
+ else
257
+ # Final response - no more tool calls
258
+ puts "\nFinal Answer:"
259
+ puts response.content
260
+ break
261
+ end
262
+
263
+ if round >= max_rounds
264
+ puts "\n[Max rounds reached]"
265
+ break
266
+ end
267
+ end
268
+
269
+ puts "\nToken usage: #{response.total_tokens} total (#{response.input_tokens} in, #{response.output_tokens} out)"
270
+
271
+ # -----------------------------------------------------------------------------
272
+ # Example 5: Adding Follow-Up Questions
273
+ # -----------------------------------------------------------------------------
274
+ puts "\n\n5. FOLLOW-UP QUESTIONS"
275
+ puts "-" * 40
276
+
277
+ original = "What's the current price of Google stock?"
278
+ puts "User: #{original}"
279
+
280
+ response = client.responses(original, model: MODEL, tools: TOOLS)
281
+
282
+ if response.has_tool_calls?
283
+ results = response.execute_tool_calls { |name, args| execute_tool(name, args) }
284
+
285
+ # Build follow-up WITH an additional question
286
+ follow_up = response.build_follow_up_input(
287
+ original_input: original,
288
+ tool_results: results,
289
+ follow_up_message: "Is that higher or lower than last week?"
290
+ )
291
+
292
+ next_response = client.responses(follow_up, model: MODEL, tools: TOOLS)
293
+ puts "\nAssistant: #{next_response.content}"
294
+ end
295
+
296
+ # -----------------------------------------------------------------------------
297
+ # Example 6: Comparing Responses API vs Chat Completions
298
+ # -----------------------------------------------------------------------------
299
+ puts "\n\n6. RESPONSES API vs CHAT COMPLETIONS COMPARISON"
300
+ puts "-" * 40
301
+
302
+ puts <<~COMPARISON
303
+ Key differences:
304
+
305
+ CHAT COMPLETIONS API:
306
+ - response["choices"][0]["message"]["content"]
307
+ - Tool calls under: choices[0].message.tool_calls[].function.{name, arguments}
308
+ - Manual message construction for continuation
309
+ - Use: response.to_message + tool_call.to_result_message(result)
310
+
311
+ RESPONSES API:
312
+ - response.content (or response["output"])
313
+ - Tool calls at: output[] where type="function_call"
314
+ - Automatic continuation with: response.build_follow_up_input()
315
+ - Use: response.execute_tool_calls { |name, args| ... }
316
+
317
+ When to use each:
318
+ - Chat Completions: Standard chat, fine-grained control, streaming
319
+ - Responses API: Multi-turn tool loops, reasoning output, simpler code
320
+ COMPARISON
321
+
322
+ puts "\n#{"=" * 60}"
323
+ puts "Examples complete!"
324
+ puts "=" * 60
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Tool Calling Loop Example (Chat Completions API)
5
+ # =================================================
6
+ # This example demonstrates a complete tool calling workflow using the
7
+ # Chat Completions API, including multi-tool calls and conversation continuation.
8
+ #
9
+ # Run with: ruby -I lib examples/tool_loop_example.rb
10
+
11
+ require "open_router"
12
+ require "json"
13
+
14
+ # Configure the client
15
+ OpenRouter.configure do |config|
16
+ config.access_token = ENV.fetch("OPENROUTER_API_KEY") do
17
+ abort "Please set OPENROUTER_API_KEY environment variable"
18
+ end
19
+ config.site_name = "Tool Loop Examples"
20
+ end
21
+
22
+ client = OpenRouter::Client.new
23
+
24
+ # Use a model with function calling support
25
+ MODEL = "openai/gpt-4o-mini"
26
+
27
+ puts "=" * 60
28
+ puts "TOOL CALLING LOOP EXAMPLE"
29
+ puts "=" * 60
30
+
31
+ # -----------------------------------------------------------------------------
32
+ # Define Our Tools
33
+ # -----------------------------------------------------------------------------
34
+
35
+ # Calculator tool for math operations
36
+ calculator_tool = OpenRouter::Tool.define do
37
+ name "calculate"
38
+ description "Perform mathematical calculations. Use this for any math operations."
39
+
40
+ parameters do
41
+ string :expression, required: true, description: "Mathematical expression to evaluate (e.g., '2 + 2', '15 * 7')"
42
+ end
43
+ end
44
+
45
+ # Weather lookup tool
46
+ weather_tool = OpenRouter::Tool.define do
47
+ name "get_weather"
48
+ description "Get current weather for a location"
49
+
50
+ parameters do
51
+ string :location, required: true, description: "City name (e.g., 'San Francisco', 'London')"
52
+ string :units, enum: %w[celsius fahrenheit], description: "Temperature units"
53
+ end
54
+ end
55
+
56
+ # Search tool
57
+ search_tool = OpenRouter::Tool.define do
58
+ name "search"
59
+ description "Search for information on a topic"
60
+
61
+ parameters do
62
+ string :query, required: true, description: "Search query"
63
+ integer :max_results, description: "Maximum number of results (default: 3)"
64
+ end
65
+ end
66
+
67
+ TOOLS = [calculator_tool, weather_tool, search_tool].freeze
68
+
69
+ # -----------------------------------------------------------------------------
70
+ # Tool Execution Functions (simulated)
71
+ # -----------------------------------------------------------------------------
72
+
73
+ def execute_tool(name, args)
74
+ case name
75
+ when "calculate"
76
+ expr = args["expression"]
77
+ # Handle percentage expressions like "15% of 378.90"
78
+ if expr =~ /(\d+(?:\.\d+)?)\s*%\s*of\s*(\d+(?:\.\d+)?)/i
79
+ result = (Regexp.last_match(1).to_f / 100) * Regexp.last_match(2).to_f
80
+ return { result: result.round(2), expression: expr }
81
+ end
82
+ # Standard math expression - safe eval for basic math
83
+ expression = expr.gsub(%r{[^0-9+\-*/.()\s]}, "")
84
+ result = begin
85
+ eval(expression)
86
+ rescue StandardError
87
+ "Error: Invalid expression"
88
+ end
89
+ { result: result, expression: expr }
90
+
91
+ when "get_weather"
92
+ # Simulated weather data
93
+ location = args["location"]
94
+ units = args["units"] || "celsius"
95
+ temp = rand(15..30)
96
+ temp = (temp * 9 / 5) + 32 if units == "fahrenheit"
97
+ {
98
+ location: location,
99
+ temperature: temp,
100
+ units: units,
101
+ conditions: %w[sunny cloudy partly_cloudy rainy].sample,
102
+ humidity: rand(40..80)
103
+ }
104
+
105
+ when "search"
106
+ # Simulated search results
107
+ query = args["query"]
108
+ max_results = args["max_results"] || 3
109
+ results = max_results.times.map do |i|
110
+ {
111
+ title: "Result #{i + 1} for '#{query}'",
112
+ snippet: "This is a simulated search result about #{query}.",
113
+ url: "https://example.com/result-#{i + 1}"
114
+ }
115
+ end
116
+ { query: query, results: results }
117
+
118
+ else
119
+ { error: "Unknown tool: #{name}" }
120
+ end
121
+ end
122
+
123
+ # -----------------------------------------------------------------------------
124
+ # Example 1: Simple Single Tool Call
125
+ # -----------------------------------------------------------------------------
126
+ puts "\n1. SIMPLE TOOL CALL"
127
+ puts "-" * 40
128
+
129
+ messages = [
130
+ { role: "user", content: "What is 15 multiplied by 23?" }
131
+ ]
132
+
133
+ puts "User: #{messages.last[:content]}"
134
+
135
+ response = client.complete(messages, model: MODEL, tools: TOOLS)
136
+
137
+ if response.has_tool_calls?
138
+ puts "\nAssistant wants to call tools:"
139
+ response.tool_calls.each do |tc|
140
+ puts " - #{tc.name}(#{tc.arguments})"
141
+ end
142
+
143
+ # Execute the tool calls and collect results
144
+ tool_results = response.tool_calls.map do |tc|
145
+ result = execute_tool(tc.name, tc.arguments)
146
+ puts " -> Result: #{result}"
147
+ tc.to_result_message(result)
148
+ end
149
+
150
+ # Continue the conversation with tool results
151
+ messages << response.to_message
152
+ messages.concat(tool_results)
153
+
154
+ final_response = client.complete(messages, model: MODEL, tools: TOOLS)
155
+ puts "\nAssistant: #{final_response.content}"
156
+ else
157
+ puts "Assistant: #{response.content}"
158
+ end
159
+
160
+ # -----------------------------------------------------------------------------
161
+ # Example 2: Multiple Tool Calls at Once
162
+ # -----------------------------------------------------------------------------
163
+ puts "\n\n2. MULTIPLE TOOL CALLS"
164
+ puts "-" * 40
165
+
166
+ messages = [
167
+ { role: "user", content: "I'm planning a trip. What's the weather like in Tokyo and Paris? Also, calculate how many hours are in 2 weeks." }
168
+ ]
169
+
170
+ puts "User: #{messages.last[:content]}"
171
+
172
+ response = client.complete(messages, model: MODEL, tools: TOOLS)
173
+
174
+ if response.has_tool_calls?
175
+ puts "\nAssistant wants to call #{response.tool_calls.length} tools:"
176
+ response.tool_calls.each do |tc|
177
+ puts " - #{tc.name}(#{tc.arguments})"
178
+ end
179
+
180
+ # Execute all tool calls
181
+ tool_results = response.tool_calls.map do |tc|
182
+ result = execute_tool(tc.name, tc.arguments)
183
+ puts " -> #{tc.name} result: #{result.to_json[0..80]}..."
184
+ tc.to_result_message(result)
185
+ end
186
+
187
+ # Continue conversation
188
+ messages << response.to_message
189
+ messages.concat(tool_results)
190
+
191
+ final_response = client.complete(messages, model: MODEL, tools: TOOLS)
192
+ puts "\nAssistant: #{final_response.content}"
193
+ end
194
+
195
+ # -----------------------------------------------------------------------------
196
+ # Example 3: Multi-Turn Tool Loop
197
+ # -----------------------------------------------------------------------------
198
+ puts "\n\n3. MULTI-TURN TOOL LOOP"
199
+ puts "-" * 40
200
+
201
+ messages = [
202
+ { role: "system", content: "You are a helpful assistant. Use tools when needed to provide accurate information." },
203
+ { role: "user", content: "Search for 'Ruby programming' and tell me what you find." }
204
+ ]
205
+
206
+ puts "User: #{messages.last[:content]}"
207
+
208
+ max_iterations = 5
209
+ iteration = 0
210
+
211
+ loop do
212
+ iteration += 1
213
+ puts "\n[Iteration #{iteration}]"
214
+
215
+ response = client.complete(messages, model: MODEL, tools: TOOLS)
216
+
217
+ if response.has_tool_calls?
218
+ # Process tool calls
219
+ response.tool_calls.each do |tc|
220
+ puts " Tool call: #{tc.name}(#{JSON.pretty_generate(tc.arguments)[0..50]}...)"
221
+ end
222
+
223
+ tool_results = response.tool_calls.map do |tc|
224
+ result = execute_tool(tc.name, tc.arguments)
225
+ tc.to_result_message(result)
226
+ end
227
+
228
+ # Add to conversation
229
+ messages << response.to_message
230
+ messages.concat(tool_results)
231
+ else
232
+ # No more tool calls - we have the final response
233
+ puts "\nAssistant: #{response.content}"
234
+ break
235
+ end
236
+
237
+ if iteration >= max_iterations
238
+ puts "\n[Max iterations reached]"
239
+ break
240
+ end
241
+ end
242
+
243
+ # -----------------------------------------------------------------------------
244
+ # Example 4: Error Handling in Tools
245
+ # -----------------------------------------------------------------------------
246
+ puts "\n\n4. ERROR HANDLING"
247
+ puts "-" * 40
248
+
249
+ # Define a tool handler that includes error handling
250
+ def safe_execute_tool(name, args)
251
+ result = execute_tool(name, args)
252
+ { success: true, data: result }
253
+ rescue StandardError => e
254
+ { success: false, error: e.message }
255
+ end
256
+
257
+ messages = [
258
+ { role: "user", content: "Calculate the result of: 100 / (5 - 5)" }
259
+ ]
260
+
261
+ puts "User: #{messages.last[:content]}"
262
+
263
+ response = client.complete(messages, model: MODEL, tools: TOOLS)
264
+
265
+ if response.has_tool_calls?
266
+ response.tool_calls.each do |tc|
267
+ puts " Tool call: #{tc.name}"
268
+
269
+ # Use the built-in execute method which handles errors
270
+ result = tc.execute do |name, args|
271
+ execute_tool(name, args)
272
+ end
273
+
274
+ if result.success?
275
+ puts " -> Success: #{result.result}"
276
+ else
277
+ puts " -> Error: #{result.error}"
278
+ end
279
+
280
+ # Continue with the result either way
281
+ messages << response.to_message
282
+ messages << result.to_message
283
+ end
284
+
285
+ final_response = client.complete(messages, model: MODEL, tools: TOOLS)
286
+ puts "\nAssistant: #{final_response.content}"
287
+ end
288
+
289
+ # -----------------------------------------------------------------------------
290
+ # Example 5: Tool Call Validation
291
+ # -----------------------------------------------------------------------------
292
+ puts "\n\n5. TOOL CALL VALIDATION"
293
+ puts "-" * 40
294
+
295
+ messages = [
296
+ { role: "user", content: "What's the weather in Boston?" }
297
+ ]
298
+
299
+ response = client.complete(messages, model: MODEL, tools: TOOLS)
300
+
301
+ if response.has_tool_calls?
302
+ response.tool_calls.each do |tc|
303
+ # Validate the tool call against our tool definitions
304
+ if tc.valid?(tools: TOOLS)
305
+ puts "Tool call '#{tc.name}' is valid"
306
+ puts " Arguments: #{tc.arguments}"
307
+ else
308
+ errors = tc.validation_errors(tools: TOOLS)
309
+ puts "Tool call '#{tc.name}' has validation errors:"
310
+ errors.each { |e| puts " - #{e}" }
311
+ end
312
+ end
313
+ end
314
+
315
+ puts "\n#{"=" * 60}"
316
+ puts "Examples complete!"
317
+ puts "=" * 60
@@ -36,12 +36,13 @@ module OpenRouter
36
36
  # @param model [String|Array] Model identifier or array of models for fallback
37
37
  # @param accumulate_response [Boolean] Whether to accumulate and return complete response
38
38
  # @param extras [Hash] Additional parameters for the completion request
39
+ # @param block [Proc] Optional block to call for each chunk (in addition to registered callbacks)
39
40
  # @return [Response, nil] Complete response if accumulate_response is true, nil otherwise
40
- def stream_complete(messages, model: "openrouter/auto", accumulate_response: true, **extras)
41
+ def stream_complete(messages, model: "openrouter/auto", accumulate_response: true, **extras, &block)
41
42
  response_accumulator = ResponseAccumulator.new if accumulate_response
42
43
 
43
- # Set up streaming handler
44
- stream_handler = build_stream_handler(response_accumulator)
44
+ # Set up streaming handler (pass optional per-call block)
45
+ stream_handler = build_stream_handler(response_accumulator, &block)
45
46
 
46
47
  # Trigger start callback
47
48
  trigger_streaming_callbacks(:on_start, { model: model, messages: messages })
@@ -92,11 +93,14 @@ module OpenRouter
92
93
 
93
94
  private
94
95
 
95
- def build_stream_handler(accumulator)
96
+ def build_stream_handler(accumulator, &per_call_block)
96
97
  proc do |chunk|
97
98
  # Trigger chunk callback
98
99
  trigger_streaming_callbacks(:on_chunk, chunk)
99
100
 
101
+ # Call per-call block if provided (used by #stream method)
102
+ per_call_block&.call(chunk)
103
+
100
104
  # Accumulate if needed
101
105
  accumulator&.add_chunk(chunk)
102
106
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenRouter
4
- VERSION = "1.2.2"
4
+ VERSION = "1.2.3"
5
5
  end