zuno 1.0.0 → 1.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/zuno/version.rb +1 -1
  3. data/lib/zuno.rb +141 -5
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1dea919bc404d8bafb4e970ffd4d2d418d5ea38123bc78abc4668fc72ce985d
4
- data.tar.gz: 4953724ce6061eb7641d7e4c5ee7564204d4647968822fd8d88303b481cf09c1
3
+ metadata.gz: 7d6d4d8dd97c7ac8743ddc66545aeda41ed824357965c693ac244fcf46de5df2
4
+ data.tar.gz: d7e0a1a6029482af54dbf7bc6e5196b39dd60e424b1fb18b4003543cfbe79867
5
5
  SHA512:
6
- metadata.gz: 16d264d3143fca0a55bb4a2ed1ad4cc08e0f8dbbb6effdf8ed75c7425b5087fd1a8763135117bcc3b1ea8cf9061a579fa95b21e813b81d90c7de1076978d3a32
7
- data.tar.gz: 8f19a868f736ec65ddf5484affe0a538a54690fc420c234afa7ead82f6943040441bce2336b334becb14fedad7babaaec03025854bf7e638ee31330f4eadddbb
6
+ metadata.gz: 4862131a38d657f175bbc488599d5cbcd97899b43bdac7a26117a437c081bfdaa365e4a2e5255e7e48464aa5e5d129e14106364727b19bae4b8166ff779cd621
7
+ data.tar.gz: 96340e7d0b4a2153d58b328cbdcbec5836ddd76724941fd8efa39343de14c052a51c2031e916562e0b66de5044832a429f2332c92e4325f40d6eefc2d1e67454
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.0"
4
+ VERSION = "1.0.1"
5
5
  end
data/lib/zuno.rb CHANGED
@@ -76,6 +76,7 @@ module Zuno
76
76
  end
77
77
 
78
78
  OPENROUTER_ADAPTER_CONFIG_KEYS = %i[api_key app_url title timeout].freeze
79
+ AI_GATEWAY_ADAPTER_CONFIG_KEYS = %i[api_key timeout base_url].freeze
79
80
  REPLICATE_ADAPTER_CONFIG_KEYS = %i[api_key timeout].freeze
80
81
  DEFAULT_MAX_ITERATIONS = 1
81
82
  REPLICATE_PREFER_WAIT_SECONDS = 60
@@ -113,6 +114,18 @@ module Zuno
113
114
  )
114
115
  end
115
116
 
117
+ def ai_gateway(
118
+ api_key: nil,
119
+ timeout: Providers::AIGateway::DEFAULT_TIMEOUT,
120
+ base_url: Providers::AIGateway::DEFAULT_BASE_URL
121
+ )
122
+ Providers::AIGateway.new(
123
+ api_key: api_key,
124
+ timeout: timeout,
125
+ base_url: base_url
126
+ )
127
+ end
128
+
116
129
  def tool(name:, description:, input_schema:, &execute)
117
130
  raise ToolError, "A block is required for tool '#{name}'" unless block_given?
118
131
 
@@ -176,7 +189,7 @@ module Zuno
176
189
 
177
190
  result =
178
191
  case provider
179
- when :openrouter
192
+ when :openrouter, :ai_gateway
180
193
  adapter = provider_adapter(provider, resolved_provider_options)
181
194
  generate_openrouter_single(
182
195
  model_descriptor: model_descriptor,
@@ -246,7 +259,9 @@ module Zuno
246
259
  )
247
260
  callback_control = nil
248
261
  model_descriptor = normalize_model(model)
249
- raise Error, "loop only supports openrouter provider" unless model_descriptor.provider.to_sym == :openrouter
262
+ unless %i[openrouter ai_gateway].include?(model_descriptor.provider.to_sym)
263
+ raise Error, "loop only supports openrouter or ai_gateway provider"
264
+ end
250
265
 
251
266
  resolved_provider_options = merge_provider_options(
252
267
  model_descriptor.provider_options,
@@ -320,6 +335,7 @@ module Zuno
320
335
 
321
336
  payload = build_payload(
322
337
  model_id: model_descriptor.id,
338
+ provider: model_descriptor.provider,
323
339
  messages: llm_messages,
324
340
  tools: tool_map,
325
341
  tool_choice: resolved_tool_choice,
@@ -497,7 +513,9 @@ module Zuno
497
513
  raise ArgumentError, "stream requires a block callback" unless block_given?
498
514
 
499
515
  model_descriptor = normalize_model(model)
500
- raise ProviderError, "stream only supports openrouter provider" unless model_descriptor.provider.to_sym == :openrouter
516
+ unless %i[openrouter ai_gateway].include?(model_descriptor.provider.to_sym)
517
+ raise ProviderError, "stream only supports openrouter or ai_gateway provider"
518
+ end
501
519
 
502
520
  resolved_provider_options = merge_provider_options(
503
521
  model_descriptor.provider_options,
@@ -508,6 +526,7 @@ module Zuno
508
526
 
509
527
  payload = build_payload(
510
528
  model_id: model_descriptor.id,
529
+ provider: model_descriptor.provider,
511
530
  messages: llm_messages,
512
531
  tools: {},
513
532
  tool_choice: nil,
@@ -608,6 +627,7 @@ module Zuno
608
627
 
609
628
  payload = build_payload(
610
629
  model_id: model_descriptor.id,
630
+ provider: model_descriptor.provider,
611
631
  messages: llm_messages,
612
632
  tools: tool_map,
613
633
  tool_choice: resolved_tool_choice,
@@ -795,7 +815,7 @@ module Zuno
795
815
  end
796
816
  private_class_method :normalize_messages
797
817
 
798
- def build_payload(model_id:, messages:, tools:, tool_choice:, temperature:, max_tokens:, provider_options:)
818
+ def build_payload(model_id:, provider:, messages:, tools:, tool_choice:, temperature:, max_tokens:, provider_options:)
799
819
  payload = {
800
820
  "model" => model_id,
801
821
  "messages" => messages
@@ -806,12 +826,26 @@ module Zuno
806
826
  payload["tools"] = tools.values.map(&:as_provider_tool) unless tools.empty?
807
827
  payload["tool_choice"] = deep_stringify(tool_choice) unless tool_choice.nil?
808
828
 
809
- request_options = reject_keys(provider_options, OPENROUTER_ADAPTER_CONFIG_KEYS + [ :tool_choice ])
829
+ request_options = reject_keys(provider_options, provider_adapter_config_keys(provider) + [ :tool_choice ])
810
830
  payload.merge!(deep_stringify(request_options)) if request_options.is_a?(Hash)
811
831
  payload
812
832
  end
813
833
  private_class_method :build_payload
814
834
 
835
+ def provider_adapter_config_keys(provider)
836
+ case provider.to_sym
837
+ when :openrouter
838
+ OPENROUTER_ADAPTER_CONFIG_KEYS
839
+ when :ai_gateway
840
+ AI_GATEWAY_ADAPTER_CONFIG_KEYS
841
+ when :replicate
842
+ REPLICATE_ADAPTER_CONFIG_KEYS
843
+ else
844
+ []
845
+ end
846
+ end
847
+ private_class_method :provider_adapter_config_keys
848
+
815
849
  def merge_provider_options(model_provider_options, call_provider_options)
816
850
  merged = {}
817
851
  merged.merge!(default_provider_options) if default_provider_options.is_a?(Hash)
@@ -826,6 +860,9 @@ module Zuno
826
860
  when :openrouter
827
861
  config = pick_keys(provider_options, OPENROUTER_ADAPTER_CONFIG_KEYS)
828
862
  Providers::OpenRouter.new(**config)
863
+ when :ai_gateway
864
+ config = pick_keys(provider_options, AI_GATEWAY_ADAPTER_CONFIG_KEYS)
865
+ Providers::AIGateway.new(**config)
829
866
  when :replicate
830
867
  config = pick_keys(provider_options, REPLICATE_ADAPTER_CONFIG_KEYS)
831
868
  Providers::Replicate.new(**config)
@@ -1243,6 +1280,105 @@ module Zuno
1243
1280
 
1244
1281
  end
1245
1282
 
1283
+ class AIGateway
1284
+ DEFAULT_BASE_URL = "https://ai-gateway.vercel.sh/v1".freeze
1285
+ DEFAULT_TIMEOUT = 120_000
1286
+
1287
+ def initialize(api_key: nil, timeout: DEFAULT_TIMEOUT, base_url: DEFAULT_BASE_URL)
1288
+ @api_key = api_key
1289
+ raise ProviderError, "Vercel Gateway API key not configured" if @api_key.nil? || @api_key.to_s.empty?
1290
+
1291
+ @timeout = timeout
1292
+ @base_url = base_url.to_s.empty? ? DEFAULT_BASE_URL : base_url.to_s
1293
+ end
1294
+
1295
+ def model(model_id)
1296
+ ModelDescriptor.new(
1297
+ id: model_id,
1298
+ provider: :ai_gateway,
1299
+ provider_options: provider_options
1300
+ )
1301
+ end
1302
+
1303
+ def chat(payload)
1304
+ response = Typhoeus.post(
1305
+ chat_completions_url,
1306
+ headers: headers,
1307
+ body: JSON.generate(payload),
1308
+ timeout: @timeout
1309
+ )
1310
+
1311
+ validate_response!(response)
1312
+ parsed = JSON.parse(response.body)
1313
+ raise ProviderError, "Vercel Gateway returned invalid JSON" unless parsed.is_a?(Hash)
1314
+
1315
+ parsed
1316
+ rescue JSON::ParserError => e
1317
+ raise ProviderError, "Failed to parse Vercel Gateway response: #{e.message}"
1318
+ end
1319
+
1320
+ def stream(payload)
1321
+ raise ArgumentError, "stream requires a block callback" unless block_given?
1322
+
1323
+ request = Typhoeus::Request.new(
1324
+ chat_completions_url,
1325
+ method: :post,
1326
+ headers: headers,
1327
+ body: JSON.generate(payload),
1328
+ timeout: @timeout
1329
+ )
1330
+
1331
+ parser = SseParser.new { |data| yield(data) }
1332
+ request.on_body do |chunk|
1333
+ parser.push(chunk)
1334
+ nil
1335
+ end
1336
+
1337
+ request.run
1338
+ validate_response!(request.response)
1339
+ parser.flush
1340
+ end
1341
+
1342
+ private
1343
+
1344
+ def provider_options
1345
+ {
1346
+ api_key: @api_key,
1347
+ timeout: @timeout,
1348
+ base_url: @base_url
1349
+ }
1350
+ end
1351
+
1352
+ def chat_completions_url
1353
+ "#{@base_url}/chat/completions"
1354
+ end
1355
+
1356
+ def headers
1357
+ {
1358
+ "Authorization" => "Bearer #{@api_key}",
1359
+ "Content-Type" => "application/json"
1360
+ }
1361
+ end
1362
+
1363
+ def validate_response!(response)
1364
+ raise ProviderError, "No response returned from Vercel Gateway" if response.nil?
1365
+ raise ProviderError, "Vercel Gateway request timed out" if response.timed_out?
1366
+
1367
+ status = response.code.to_i
1368
+ body = response.body.to_s
1369
+ message = body.length > 300 ? "#{body[0, 300]}..." : body
1370
+
1371
+ return if status >= 200 && status < 300
1372
+
1373
+ if status.positive?
1374
+ raise ProviderError, "Vercel Gateway responded with HTTP #{status}: #{message}"
1375
+ end
1376
+
1377
+ suffix = message.empty? ? "" : ": #{message}"
1378
+ raise ProviderError, "Vercel Gateway request failed: #{response.return_code}#{suffix}"
1379
+ end
1380
+ end
1381
+
1246
1382
  class Replicate
1247
1383
  API_BASE_URL = "https://api.replicate.com/v1".freeze
1248
1384
  DEFAULT_TIMEOUT = 120_000
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zuno
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hyperaide