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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 375f446f6d41ed1c8f05361915316c3da7fbc571ee5ec4d59316739f9fd6a14f
4
- data.tar.gz: 641e201f64dc3076185e2a6593932e67847f05ce4c5a66866b200b8db4d38241
3
+ metadata.gz: cf9be3e305d36fb21db7c92926787237b8db4322a993d3e8c8a38448c439d7b4
4
+ data.tar.gz: b004707c8eb242a35c631defafd13a13f0bf3533f9959e4b8ec2c62829097337
5
5
  SHA512:
6
- metadata.gz: 76d4be87dad8f35fc9e879a14705035344823f43c0ba45c6d8317d5b8e0a273c6349db11294b8e17c5dfb901dec9006fa488b8ebe8633baec33aa66e74be9166
7
- data.tar.gz: 8555df550de0513330c086ec4b676d584a1908feb983d2ca59d32c8d685f626d2c767960e6e1dde23dbb17ac0cfc903aee251bef71519ffde27f8c1524cd2d68
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
- Callbacks can accept a second argument (`control`) and call `control.stop!(reason: "...")`.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zuno
4
- VERSION = "1.0.2"
4
+ VERSION = "1.1.0"
5
5
  end
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
- tool_map = normalize_tools(tools)
325
- llm_messages = normalize_messages(messages: messages, system: system, prompt: prompt)
326
- resolved_tool_choice = normalize_tool_choice(
327
- explicit_tool_choice: tool_choice,
328
- provider_options: resolved_provider_options,
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: llm_messages,
341
- tool_names: tool_map.keys,
342
- tool_choice: resolved_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: llm_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: llm_messages,
393
- tools: tool_map,
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
- llm_messages << build_assistant_tool_call_message(message: message, tool_calls: tool_calls)
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: tool_map,
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
- llm_messages << {
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
- iteration_index: current_iteration,
501
- iteration: iteration_record
502
- },
503
- callback_control
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
- normalized << { "role" => "system", "content" => system.to_s } if system
879
- normalized << { "role" => "user", "content" => prompt.to_s } if prompt
880
- return normalized
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 :normalize_messages
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
- if control && callback_accepts_control?(callback)
1147
- callback.call(payload, control)
1148
- else
1149
- callback.call(payload)
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 callback_accepts_control?(callback)
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 >= 2
1387
+ positional_count >= count
1162
1388
  end
1163
- private_class_method :callback_accepts_control?
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.2
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-01 00:00:00.000000000 Z
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.dev
58
+ - team@hyperaide.com
59
59
  executables: []
60
60
  extensions: []
61
61
  extra_rdoc_files: []