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.
- checksums.yaml +4 -4
- data/lib/zuno/version.rb +1 -1
- data/lib/zuno.rb +141 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d6d4d8dd97c7ac8743ddc66545aeda41ed824357965c693ac244fcf46de5df2
|
|
4
|
+
data.tar.gz: d7e0a1a6029482af54dbf7bc6e5196b39dd60e424b1fb18b4003543cfbe79867
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4862131a38d657f175bbc488599d5cbcd97899b43bdac7a26117a437c081bfdaa365e4a2e5255e7e48464aa5e5d129e14106364727b19bae4b8166ff779cd621
|
|
7
|
+
data.tar.gz: 96340e7d0b4a2153d58b328cbdcbec5836ddd76724941fd8efa39343de14c052a51c2031e916562e0b66de5044832a429f2332c92e4325f40d6eefc2d1e67454
|
data/lib/zuno/version.rb
CHANGED
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|