zuno 1.0.2 → 1.1.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 +8 -0
- data/README.md +33 -1
- data/lib/zuno/version.rb +1 -1
- data/lib/zuno.rb +316 -51
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf9be3e305d36fb21db7c92926787237b8db4322a993d3e8c8a38448c439d7b4
|
|
4
|
+
data.tar.gz: b004707c8eb242a35c631defafd13a13f0bf3533f9959e4b8ec2c62829097337
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4153961f2ece3493d232af7f742c40be7fa09adbf76a82fb2722923630033e778f846c7489582f1e4bc06afafdc40663dcd237c4f70f6af0a4004da6dad5b09b
|
|
7
|
+
data.tar.gz: 486f38c407011327c5a9c336341bcc742233cfda7112b8596f9f5050565791d7ab24a36777c6061fe14dbfc65126e310c9ad69f372423b221726533ab8c37fa9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.0](https://github.com/dqnamo/zuno/compare/v1.0.2...v1.1.0) (2026-04-04)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* add `Zuno.embed` for OpenAI-compatible embedding providers including OpenRouter and AI Gateway
|
|
8
|
+
* add mutable loop callback context for tools, messages, and system prompt updates
|
|
9
|
+
* refresh callback, loop, tools, and API reference documentation for the split `generate` and `loop` APIs
|
|
10
|
+
|
|
3
11
|
## [1.0.2](https://github.com/dqnamo/zuno/compare/v1.0.1...v1.0.2) (2026-04-01)
|
|
4
12
|
|
|
5
13
|
### Features
|
data/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Standalone Ruby SDK for:
|
|
4
4
|
|
|
5
5
|
- provider/model abstraction
|
|
6
|
+
- embeddings via OpenAI-compatible providers
|
|
6
7
|
- single-shot generation
|
|
7
8
|
- iterative tool loops
|
|
8
9
|
- streaming via SSE (OpenRouter)
|
|
@@ -54,6 +55,19 @@ result = Zuno.generate(
|
|
|
54
55
|
puts result[:text]
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
## Embeddings (`embed`)
|
|
59
|
+
|
|
60
|
+
`embed` supports OpenAI-compatible embedding providers such as OpenRouter and AI Gateway.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
result = Zuno.embed(
|
|
64
|
+
model: openrouter.model("openai/text-embedding-3-small"),
|
|
65
|
+
content: "Sunny day at the beach"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
pp result[:embedding]
|
|
69
|
+
```
|
|
70
|
+
|
|
57
71
|
`generate` supports tool definitions and executes returned tool calls once, without a follow-up LLM request.
|
|
58
72
|
|
|
59
73
|
### Replicate
|
|
@@ -114,7 +128,25 @@ result = Zuno.loop(
|
|
|
114
128
|
- `max_iterations` (`Integer`, `:infinite`, or `Float::INFINITY`)
|
|
115
129
|
- `stop_when: { tool_called: ... }`
|
|
116
130
|
|
|
117
|
-
|
|
131
|
+
Loop callbacks can accept:
|
|
132
|
+
|
|
133
|
+
- a second argument (`control`) and call `control.stop!(reason: "...")`
|
|
134
|
+
- a third argument (`context`) to mutate the next LLM request state
|
|
135
|
+
|
|
136
|
+
`context` supports:
|
|
137
|
+
|
|
138
|
+
- `context.system = "..."` or `context.clear_system!`
|
|
139
|
+
- `context.messages = [...]`, `context.add_message(...)`, `context.add_messages(...)`
|
|
140
|
+
- `context.tools = {...}`, `context.add_tool(:name, tool)`, `context.add_tools(...)`, `context.remove_tool(:name)`
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
before_iteration: ->(_payload, _control, context) {
|
|
146
|
+
context.system = "Be concise"
|
|
147
|
+
context.add_message(role: "user", content: "Answer in bullet points")
|
|
148
|
+
}
|
|
149
|
+
```
|
|
118
150
|
|
|
119
151
|
## Tool choice
|
|
120
152
|
|
data/lib/zuno/version.rb
CHANGED
data/lib/zuno.rb
CHANGED
|
@@ -31,6 +31,88 @@ module Zuno
|
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
class CallbackContext
|
|
35
|
+
attr_reader :messages, :tools
|
|
36
|
+
attr_accessor :system
|
|
37
|
+
|
|
38
|
+
def initialize(messages:, system:, tools:)
|
|
39
|
+
@messages = messages
|
|
40
|
+
@system = system
|
|
41
|
+
@tools = tools
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def messages=(messages)
|
|
45
|
+
@messages = messages
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def tools=(tools)
|
|
49
|
+
@tools = tools
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_message(message = nil, role: nil, content: nil, **attributes)
|
|
53
|
+
if message
|
|
54
|
+
raise ArgumentError, "Provide either a message hash or role/content attributes" unless role.nil? && content.nil? && attributes.empty?
|
|
55
|
+
|
|
56
|
+
entry = message
|
|
57
|
+
elsif role.nil? && content.nil? && !attributes.empty?
|
|
58
|
+
entry = attributes
|
|
59
|
+
else
|
|
60
|
+
raise ArgumentError, "add_message requires role:" if role.nil?
|
|
61
|
+
|
|
62
|
+
entry = { role: role, content: content }.merge(attributes)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@messages = [] unless @messages.is_a?(Array)
|
|
66
|
+
@messages << entry
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def add_messages(messages)
|
|
71
|
+
@messages = [] unless @messages.is_a?(Array)
|
|
72
|
+
@messages.concat(Array(messages))
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def add_tool(name, tool)
|
|
77
|
+
@tools = {} unless @tools.is_a?(Hash)
|
|
78
|
+
@tools[name] = tool
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def add_tools(tools)
|
|
83
|
+
@tools = {} unless @tools.is_a?(Hash)
|
|
84
|
+
|
|
85
|
+
case tools
|
|
86
|
+
when Hash
|
|
87
|
+
@tools.merge!(tools)
|
|
88
|
+
when Array
|
|
89
|
+
tools.each do |entry|
|
|
90
|
+
raise ToolError, "tools must be a Hash or Array of ToolDefinition entries" unless entry.is_a?(ToolDefinition)
|
|
91
|
+
|
|
92
|
+
@tools[entry.name] = entry
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
raise ToolError, "tools must be a Hash or Array"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def remove_tool(name)
|
|
102
|
+
return self unless @tools.is_a?(Hash)
|
|
103
|
+
|
|
104
|
+
@tools.delete(name)
|
|
105
|
+
@tools.delete(name.to_s)
|
|
106
|
+
@tools.delete(name.to_sym)
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def clear_system!
|
|
111
|
+
@system = nil
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
34
116
|
ModelDescriptor = Struct.new(:id, :provider, :provider_options, keyword_init: true) do
|
|
35
117
|
def initialize(id:, provider:, provider_options: {})
|
|
36
118
|
super(
|
|
@@ -190,6 +272,58 @@ module Zuno
|
|
|
190
272
|
raise Error, e.message
|
|
191
273
|
end
|
|
192
274
|
|
|
275
|
+
def embed(
|
|
276
|
+
model:,
|
|
277
|
+
content:,
|
|
278
|
+
dimensions: nil,
|
|
279
|
+
encoding_format: nil,
|
|
280
|
+
provider_options: {},
|
|
281
|
+
**options
|
|
282
|
+
)
|
|
283
|
+
validate_embedding_input!(content: content)
|
|
284
|
+
|
|
285
|
+
model_descriptor = normalize_model(model)
|
|
286
|
+
resolved_provider_options = merge_provider_options(
|
|
287
|
+
model_descriptor.provider_options,
|
|
288
|
+
provider_options
|
|
289
|
+
)
|
|
290
|
+
provider = model_descriptor.provider.to_sym
|
|
291
|
+
|
|
292
|
+
case provider
|
|
293
|
+
when :openrouter, :ai_gateway
|
|
294
|
+
adapter = provider_adapter(provider, resolved_provider_options)
|
|
295
|
+
response = adapter.embed(
|
|
296
|
+
build_embedding_payload(
|
|
297
|
+
model_id: model_descriptor.id,
|
|
298
|
+
provider: provider,
|
|
299
|
+
content: content,
|
|
300
|
+
dimensions: dimensions,
|
|
301
|
+
encoding_format: encoding_format,
|
|
302
|
+
provider_options: resolved_provider_options,
|
|
303
|
+
options: options
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
embeddings = Array(response["data"]).filter_map do |item|
|
|
308
|
+
item["embedding"] if item.is_a?(Hash)
|
|
309
|
+
end
|
|
310
|
+
result = {
|
|
311
|
+
embeddings: embeddings,
|
|
312
|
+
usage: response["usage"],
|
|
313
|
+
model: response["model"],
|
|
314
|
+
raw_response: response
|
|
315
|
+
}
|
|
316
|
+
result[:embedding] = embeddings.first if embeddings.length == 1
|
|
317
|
+
result
|
|
318
|
+
else
|
|
319
|
+
raise ProviderError, "embed only supports openrouter or ai_gateway provider"
|
|
320
|
+
end
|
|
321
|
+
rescue ProviderError
|
|
322
|
+
raise
|
|
323
|
+
rescue StandardError => e
|
|
324
|
+
raise Error, e.message
|
|
325
|
+
end
|
|
326
|
+
|
|
193
327
|
def generate(
|
|
194
328
|
model:,
|
|
195
329
|
messages: nil,
|
|
@@ -311,6 +445,7 @@ module Zuno
|
|
|
311
445
|
after_generation: nil
|
|
312
446
|
)
|
|
313
447
|
callback_control = nil
|
|
448
|
+
callback_context = nil
|
|
314
449
|
model_descriptor = normalize_model(model)
|
|
315
450
|
unless %i[openrouter ai_gateway].include?(model_descriptor.provider.to_sym)
|
|
316
451
|
raise Error, "loop only supports openrouter or ai_gateway provider"
|
|
@@ -321,12 +456,11 @@ module Zuno
|
|
|
321
456
|
provider_options
|
|
322
457
|
)
|
|
323
458
|
adapter = provider_adapter(model_descriptor.provider, resolved_provider_options)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
tools: tool_map
|
|
459
|
+
callback_context = build_callback_context(
|
|
460
|
+
messages: messages,
|
|
461
|
+
system: system,
|
|
462
|
+
prompt: prompt,
|
|
463
|
+
tools: tools
|
|
330
464
|
)
|
|
331
465
|
resolved_stop_when = normalize_stop_when(stop_when)
|
|
332
466
|
resolved_max_iterations = normalize_max_iterations(max_iterations)
|
|
@@ -337,14 +471,20 @@ module Zuno
|
|
|
337
471
|
before_generation,
|
|
338
472
|
{
|
|
339
473
|
model: model_descriptor,
|
|
340
|
-
messages:
|
|
341
|
-
tool_names:
|
|
342
|
-
tool_choice:
|
|
474
|
+
messages: materialize_messages(messages: callback_context.messages, system: callback_context.system),
|
|
475
|
+
tool_names: callback_context.tools.keys,
|
|
476
|
+
tool_choice: normalize_tool_choice(
|
|
477
|
+
explicit_tool_choice: tool_choice,
|
|
478
|
+
provider_options: resolved_provider_options,
|
|
479
|
+
tools: callback_context.tools
|
|
480
|
+
),
|
|
343
481
|
max_iterations: resolved_max_iterations,
|
|
344
482
|
stop_when: resolved_stop_when
|
|
345
483
|
},
|
|
346
|
-
callback_control
|
|
484
|
+
callback_control,
|
|
485
|
+
callback_context
|
|
347
486
|
)
|
|
487
|
+
normalize_callback_context!(callback_context)
|
|
348
488
|
if callback_control.stopped?
|
|
349
489
|
result = callback_stopped_result(
|
|
350
490
|
control: callback_control,
|
|
@@ -354,7 +494,7 @@ module Zuno
|
|
|
354
494
|
raw_response: nil
|
|
355
495
|
)
|
|
356
496
|
after_generation_called = true
|
|
357
|
-
call_callback!(after_generation, { ok: true, result: result }, callback_control)
|
|
497
|
+
call_callback!(after_generation, { ok: true, result: result }, callback_control, callback_context)
|
|
358
498
|
return result
|
|
359
499
|
end
|
|
360
500
|
|
|
@@ -369,10 +509,12 @@ module Zuno
|
|
|
369
509
|
before_iteration,
|
|
370
510
|
{
|
|
371
511
|
iteration_index: current_iteration,
|
|
372
|
-
messages:
|
|
512
|
+
messages: materialize_messages(messages: callback_context.messages, system: callback_context.system)
|
|
373
513
|
},
|
|
374
|
-
callback_control
|
|
514
|
+
callback_control,
|
|
515
|
+
callback_context
|
|
375
516
|
)
|
|
517
|
+
normalize_callback_context!(callback_context)
|
|
376
518
|
if callback_control.stopped?
|
|
377
519
|
result = callback_stopped_result(
|
|
378
520
|
control: callback_control,
|
|
@@ -382,15 +524,20 @@ module Zuno
|
|
|
382
524
|
raw_response: nil
|
|
383
525
|
)
|
|
384
526
|
after_generation_called = true
|
|
385
|
-
call_callback!(after_generation, { ok: true, result: result }, callback_control)
|
|
527
|
+
call_callback!(after_generation, { ok: true, result: result }, callback_control, callback_context)
|
|
386
528
|
return result
|
|
387
529
|
end
|
|
388
530
|
|
|
531
|
+
resolved_tool_choice = normalize_tool_choice(
|
|
532
|
+
explicit_tool_choice: tool_choice,
|
|
533
|
+
provider_options: resolved_provider_options,
|
|
534
|
+
tools: callback_context.tools
|
|
535
|
+
)
|
|
389
536
|
payload = build_payload(
|
|
390
537
|
model_id: model_descriptor.id,
|
|
391
538
|
provider: model_descriptor.provider,
|
|
392
|
-
messages:
|
|
393
|
-
tools:
|
|
539
|
+
messages: materialize_messages(messages: callback_context.messages, system: callback_context.system),
|
|
540
|
+
tools: callback_context.tools,
|
|
394
541
|
tool_choice: resolved_tool_choice,
|
|
395
542
|
temperature: temperature,
|
|
396
543
|
max_tokens: max_tokens,
|
|
@@ -418,8 +565,10 @@ module Zuno
|
|
|
418
565
|
iteration_index: current_iteration,
|
|
419
566
|
iteration: iteration_record
|
|
420
567
|
},
|
|
421
|
-
callback_control
|
|
568
|
+
callback_control,
|
|
569
|
+
callback_context
|
|
422
570
|
)
|
|
571
|
+
normalize_callback_context!(callback_context)
|
|
423
572
|
if callback_control.stopped?
|
|
424
573
|
result = callback_stopped_result(
|
|
425
574
|
control: callback_control,
|
|
@@ -429,7 +578,7 @@ module Zuno
|
|
|
429
578
|
raw_response: response
|
|
430
579
|
)
|
|
431
580
|
after_generation_called = true
|
|
432
|
-
call_callback!(after_generation, { ok: true, result: result }, callback_control)
|
|
581
|
+
call_callback!(after_generation, { ok: true, result: result }, callback_control, callback_context)
|
|
433
582
|
return result
|
|
434
583
|
end
|
|
435
584
|
|
|
@@ -443,11 +592,11 @@ module Zuno
|
|
|
443
592
|
}
|
|
444
593
|
|
|
445
594
|
after_generation_called = true
|
|
446
|
-
call_callback!(after_generation, { ok: true, result: result }, callback_control)
|
|
595
|
+
call_callback!(after_generation, { ok: true, result: result }, callback_control, callback_context)
|
|
447
596
|
return result
|
|
448
597
|
end
|
|
449
598
|
|
|
450
|
-
|
|
599
|
+
callback_context.add_message(build_assistant_tool_call_message(message: message, tool_calls: tool_calls))
|
|
451
600
|
stop_triggered = false
|
|
452
601
|
stop_triggered_tool_name = nil
|
|
453
602
|
|
|
@@ -465,12 +614,14 @@ module Zuno
|
|
|
465
614
|
input: arguments,
|
|
466
615
|
raw_tool_call: tool_call
|
|
467
616
|
},
|
|
468
|
-
callback_control
|
|
617
|
+
callback_control,
|
|
618
|
+
callback_context
|
|
469
619
|
)
|
|
620
|
+
normalize_callback_context!(callback_context)
|
|
470
621
|
|
|
471
622
|
tool_result = execute_tool_call(
|
|
472
623
|
tool_call: tool_call,
|
|
473
|
-
tools:
|
|
624
|
+
tools: callback_context.tools,
|
|
474
625
|
tool_call_id: tool_call_id,
|
|
475
626
|
arguments: arguments
|
|
476
627
|
)
|
|
@@ -479,29 +630,33 @@ module Zuno
|
|
|
479
630
|
call_callback!(
|
|
480
631
|
after_tool_execution,
|
|
481
632
|
tool_result.merge(iteration_index: current_iteration),
|
|
482
|
-
callback_control
|
|
633
|
+
callback_control,
|
|
634
|
+
callback_context
|
|
483
635
|
)
|
|
636
|
+
normalize_callback_context!(callback_context)
|
|
484
637
|
if tool_stop_condition_met?(resolved_stop_when, tool_result)
|
|
485
638
|
stop_triggered = true
|
|
486
639
|
stop_triggered_tool_name ||= tool_result[:tool_name]
|
|
487
640
|
end
|
|
488
641
|
|
|
489
|
-
|
|
642
|
+
callback_context.add_message(
|
|
490
643
|
"role" => "tool",
|
|
491
644
|
"tool_call_id" => tool_result[:tool_call_id],
|
|
492
645
|
"content" => serialize_tool_content(tool_result[:output])
|
|
493
|
-
|
|
646
|
+
)
|
|
494
647
|
end
|
|
495
648
|
|
|
496
649
|
iterations << iteration_record
|
|
497
650
|
call_callback!(
|
|
498
651
|
after_iteration,
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
652
|
+
{
|
|
653
|
+
iteration_index: current_iteration,
|
|
654
|
+
iteration: iteration_record
|
|
655
|
+
},
|
|
656
|
+
callback_control,
|
|
657
|
+
callback_context
|
|
658
|
+
)
|
|
659
|
+
normalize_callback_context!(callback_context)
|
|
505
660
|
if callback_control.stopped?
|
|
506
661
|
result = callback_stopped_result(
|
|
507
662
|
control: callback_control,
|
|
@@ -511,7 +666,7 @@ module Zuno
|
|
|
511
666
|
raw_response: response
|
|
512
667
|
)
|
|
513
668
|
after_generation_called = true
|
|
514
|
-
call_callback!(after_generation, { ok: true, result: result }, callback_control)
|
|
669
|
+
call_callback!(after_generation, { ok: true, result: result }, callback_control, callback_context)
|
|
515
670
|
return result
|
|
516
671
|
end
|
|
517
672
|
|
|
@@ -530,7 +685,7 @@ module Zuno
|
|
|
530
685
|
}
|
|
531
686
|
|
|
532
687
|
after_generation_called = true
|
|
533
|
-
call_callback!(after_generation, { ok: true, result: result }, callback_control)
|
|
688
|
+
call_callback!(after_generation, { ok: true, result: result }, callback_control, callback_context)
|
|
534
689
|
return result
|
|
535
690
|
end
|
|
536
691
|
|
|
@@ -542,13 +697,13 @@ module Zuno
|
|
|
542
697
|
rescue ProviderError, MaxIterationsExceeded => e
|
|
543
698
|
unless after_generation_called
|
|
544
699
|
after_generation_called = true
|
|
545
|
-
call_callback!(after_generation, { ok: false, error: e }, callback_control)
|
|
700
|
+
call_callback!(after_generation, { ok: false, error: e }, callback_control, callback_context)
|
|
546
701
|
end
|
|
547
702
|
raise
|
|
548
703
|
rescue StandardError => e
|
|
549
704
|
unless after_generation_called
|
|
550
705
|
after_generation_called = true
|
|
551
|
-
call_callback!(after_generation, { ok: false, error: e }, callback_control)
|
|
706
|
+
call_callback!(after_generation, { ok: false, error: e }, callback_control, callback_context)
|
|
552
707
|
end
|
|
553
708
|
raise Error, e.message
|
|
554
709
|
end
|
|
@@ -835,6 +990,34 @@ module Zuno
|
|
|
835
990
|
end
|
|
836
991
|
private_class_method :validate_transcription_input!
|
|
837
992
|
|
|
993
|
+
def validate_embedding_input!(content:)
|
|
994
|
+
case content
|
|
995
|
+
when String
|
|
996
|
+
raise Error, "content is required" if content.strip.empty?
|
|
997
|
+
when Array
|
|
998
|
+
raise Error, "content must include at least one item" if content.empty?
|
|
999
|
+
when nil
|
|
1000
|
+
raise Error, "content is required"
|
|
1001
|
+
end
|
|
1002
|
+
end
|
|
1003
|
+
private_class_method :validate_embedding_input!
|
|
1004
|
+
|
|
1005
|
+
def build_embedding_payload(model_id:, provider:, content:, dimensions:, encoding_format:, provider_options:, options:)
|
|
1006
|
+
payload = {
|
|
1007
|
+
"model" => model_id,
|
|
1008
|
+
"input" => deep_stringify(content)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
payload["dimensions"] = dimensions unless dimensions.nil?
|
|
1012
|
+
payload["encoding_format"] = encoding_format unless encoding_format.nil?
|
|
1013
|
+
|
|
1014
|
+
request_options = reject_keys(provider_options, provider_adapter_config_keys(provider) + [:tool_choice])
|
|
1015
|
+
payload.merge!(deep_stringify(request_options)) if request_options.is_a?(Hash)
|
|
1016
|
+
payload.merge!(deep_stringify(options)) if options.is_a?(Hash) && !options.empty?
|
|
1017
|
+
payload
|
|
1018
|
+
end
|
|
1019
|
+
private_class_method :build_embedding_payload
|
|
1020
|
+
|
|
838
1021
|
def normalize_tools(tools)
|
|
839
1022
|
return {} if tools.nil?
|
|
840
1023
|
|
|
@@ -873,19 +1056,57 @@ module Zuno
|
|
|
873
1056
|
private_class_method :normalize_tool_entry
|
|
874
1057
|
|
|
875
1058
|
def normalize_messages(messages:, system:, prompt:)
|
|
1059
|
+
materialize_messages(
|
|
1060
|
+
messages: normalize_callback_messages(messages: messages, prompt: prompt),
|
|
1061
|
+
system: normalize_callback_system(system)
|
|
1062
|
+
)
|
|
1063
|
+
end
|
|
1064
|
+
private_class_method :normalize_messages
|
|
1065
|
+
|
|
1066
|
+
def build_callback_context(messages:, system:, prompt:, tools:)
|
|
1067
|
+
CallbackContext.new(
|
|
1068
|
+
messages: normalize_callback_messages(messages: messages, prompt: prompt),
|
|
1069
|
+
system: normalize_callback_system(system),
|
|
1070
|
+
tools: normalize_tools(tools)
|
|
1071
|
+
)
|
|
1072
|
+
end
|
|
1073
|
+
private_class_method :build_callback_context
|
|
1074
|
+
|
|
1075
|
+
def normalize_callback_context!(context)
|
|
1076
|
+
context.messages = normalize_callback_messages(messages: context.messages, prompt: nil)
|
|
1077
|
+
context.system = normalize_callback_system(context.system)
|
|
1078
|
+
context.tools = normalize_tools(context.tools)
|
|
1079
|
+
context
|
|
1080
|
+
end
|
|
1081
|
+
private_class_method :normalize_callback_context!
|
|
1082
|
+
|
|
1083
|
+
def normalize_callback_messages(messages:, prompt:)
|
|
876
1084
|
if messages.nil? || messages.empty?
|
|
877
1085
|
normalized = []
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1086
|
+
else
|
|
1087
|
+
raise Error, "messages must be an Array" unless messages.is_a?(Array)
|
|
1088
|
+
|
|
1089
|
+
normalized = deep_stringify(messages)
|
|
881
1090
|
end
|
|
882
1091
|
|
|
883
|
-
normalized = deep_stringify(messages)
|
|
884
|
-
normalized.unshift({ "role" => "system", "content" => system.to_s }) if system
|
|
885
1092
|
normalized << { "role" => "user", "content" => prompt.to_s } if prompt
|
|
886
1093
|
normalized
|
|
887
1094
|
end
|
|
888
|
-
private_class_method :
|
|
1095
|
+
private_class_method :normalize_callback_messages
|
|
1096
|
+
|
|
1097
|
+
def normalize_callback_system(system)
|
|
1098
|
+
return nil if system.nil?
|
|
1099
|
+
|
|
1100
|
+
system.to_s
|
|
1101
|
+
end
|
|
1102
|
+
private_class_method :normalize_callback_system
|
|
1103
|
+
|
|
1104
|
+
def materialize_messages(messages:, system:)
|
|
1105
|
+
normalized = normalize_callback_messages(messages: messages, prompt: nil)
|
|
1106
|
+
normalized.unshift({ "role" => "system", "content" => system.to_s }) if system
|
|
1107
|
+
normalized
|
|
1108
|
+
end
|
|
1109
|
+
private_class_method :materialize_messages
|
|
889
1110
|
|
|
890
1111
|
def build_payload(model_id:, provider:, messages:, tools:, tool_choice:, temperature:, max_tokens:, provider_options:)
|
|
891
1112
|
payload = {
|
|
@@ -1139,28 +1360,33 @@ module Zuno
|
|
|
1139
1360
|
end
|
|
1140
1361
|
private_class_method :callback_stopped_result
|
|
1141
1362
|
|
|
1142
|
-
def call_callback!(callback, payload, control = nil)
|
|
1363
|
+
def call_callback!(callback, payload, control = nil, context = nil)
|
|
1143
1364
|
return if callback.nil?
|
|
1144
1365
|
raise Error, "Callback must respond to #call" unless callback.respond_to?(:call)
|
|
1145
1366
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1367
|
+
unless callback.lambda?
|
|
1368
|
+
args = [payload]
|
|
1369
|
+
args << control if control
|
|
1370
|
+
args << context if context
|
|
1371
|
+
callback.call(*args)
|
|
1372
|
+
return
|
|
1150
1373
|
end
|
|
1374
|
+
|
|
1375
|
+
args = [payload]
|
|
1376
|
+
args << control if control && callback_accepts_argument_count?(callback, 2)
|
|
1377
|
+
args << context if context && callback_accepts_argument_count?(callback, 3)
|
|
1378
|
+
callback.call(*args)
|
|
1151
1379
|
end
|
|
1152
1380
|
private_class_method :call_callback!
|
|
1153
1381
|
|
|
1154
|
-
def
|
|
1155
|
-
return true unless callback.lambda?
|
|
1156
|
-
|
|
1382
|
+
def callback_accepts_argument_count?(callback, count)
|
|
1157
1383
|
params = callback.parameters
|
|
1158
1384
|
return true if params.any? { |param_type, _| param_type == :rest }
|
|
1159
1385
|
|
|
1160
1386
|
positional_count = params.count { |param_type, _| param_type == :req || param_type == :opt }
|
|
1161
|
-
positional_count >=
|
|
1387
|
+
positional_count >= count
|
|
1162
1388
|
end
|
|
1163
|
-
private_class_method :
|
|
1389
|
+
private_class_method :callback_accepts_argument_count?
|
|
1164
1390
|
|
|
1165
1391
|
def normalize_output_payload(payload)
|
|
1166
1392
|
case payload
|
|
@@ -1259,6 +1485,7 @@ module Zuno
|
|
|
1259
1485
|
module Providers
|
|
1260
1486
|
class OpenRouter
|
|
1261
1487
|
CHAT_COMPLETIONS_URL = "https://openrouter.ai/api/v1/chat/completions".freeze
|
|
1488
|
+
EMBEDDINGS_URL = "https://openrouter.ai/api/v1/embeddings".freeze
|
|
1262
1489
|
DEFAULT_TIMEOUT = 120_000
|
|
1263
1490
|
|
|
1264
1491
|
def initialize(api_key: nil, app_url: nil, title: nil, timeout: DEFAULT_TIMEOUT)
|
|
@@ -1295,6 +1522,23 @@ module Zuno
|
|
|
1295
1522
|
raise ProviderError, "Failed to parse OpenRouter response: #{e.message}"
|
|
1296
1523
|
end
|
|
1297
1524
|
|
|
1525
|
+
def embed(payload)
|
|
1526
|
+
response = Typhoeus.post(
|
|
1527
|
+
EMBEDDINGS_URL,
|
|
1528
|
+
headers: headers,
|
|
1529
|
+
body: JSON.generate(payload),
|
|
1530
|
+
timeout: @timeout
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
validate_response!(response)
|
|
1534
|
+
parsed = JSON.parse(response.body)
|
|
1535
|
+
raise ProviderError, "OpenRouter returned invalid JSON" unless parsed.is_a?(Hash)
|
|
1536
|
+
|
|
1537
|
+
parsed
|
|
1538
|
+
rescue JSON::ParserError => e
|
|
1539
|
+
raise ProviderError, "Failed to parse OpenRouter response: #{e.message}"
|
|
1540
|
+
end
|
|
1541
|
+
|
|
1298
1542
|
def stream(payload)
|
|
1299
1543
|
raise ArgumentError, "stream requires a block callback" unless block_given?
|
|
1300
1544
|
|
|
@@ -1394,6 +1638,23 @@ module Zuno
|
|
|
1394
1638
|
raise ProviderError, "Failed to parse Vercel Gateway response: #{e.message}"
|
|
1395
1639
|
end
|
|
1396
1640
|
|
|
1641
|
+
def embed(payload)
|
|
1642
|
+
response = Typhoeus.post(
|
|
1643
|
+
embeddings_url,
|
|
1644
|
+
headers: headers,
|
|
1645
|
+
body: JSON.generate(payload),
|
|
1646
|
+
timeout: @timeout
|
|
1647
|
+
)
|
|
1648
|
+
|
|
1649
|
+
validate_response!(response)
|
|
1650
|
+
parsed = JSON.parse(response.body)
|
|
1651
|
+
raise ProviderError, "Vercel Gateway returned invalid JSON" unless parsed.is_a?(Hash)
|
|
1652
|
+
|
|
1653
|
+
parsed
|
|
1654
|
+
rescue JSON::ParserError => e
|
|
1655
|
+
raise ProviderError, "Failed to parse Vercel Gateway response: #{e.message}"
|
|
1656
|
+
end
|
|
1657
|
+
|
|
1397
1658
|
def stream(payload)
|
|
1398
1659
|
raise ArgumentError, "stream requires a block callback" unless block_given?
|
|
1399
1660
|
|
|
@@ -1430,6 +1691,10 @@ module Zuno
|
|
|
1430
1691
|
"#{@base_url}/chat/completions"
|
|
1431
1692
|
end
|
|
1432
1693
|
|
|
1694
|
+
def embeddings_url
|
|
1695
|
+
"#{@base_url}/embeddings"
|
|
1696
|
+
end
|
|
1697
|
+
|
|
1433
1698
|
def headers
|
|
1434
1699
|
{
|
|
1435
1700
|
"Authorization" => "Bearer #{@api_key}",
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zuno
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hyperaide
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: typhoeus
|
|
@@ -55,7 +55,7 @@ dependencies:
|
|
|
55
55
|
description: Standalone Ruby SDK for AI generation across OpenRouter and Replicate,
|
|
56
56
|
ElevenLabs speech-to-text, with iterative tool loops and SSE streaming.
|
|
57
57
|
email:
|
|
58
|
-
- team@hyperaide.
|
|
58
|
+
- team@hyperaide.com
|
|
59
59
|
executables: []
|
|
60
60
|
extensions: []
|
|
61
61
|
extra_rdoc_files: []
|