wave-dispatch 0.5.0 → 0.5.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 (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/wave_dispatch.rb +82 -7
  3. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fad95eeecc253e395c29e551144cc7ef306b40e24210e7310d068c4a15ed56c
4
- data.tar.gz: d85a1e03d7ebb16310077c3c7086dcf4744a4e700b20b576f342c49c05460ef7
3
+ metadata.gz: 409e75177b57d71d5c816915aca6be428f36539c462c19610a945d34435d90ce
4
+ data.tar.gz: 364157ef490e38907643ba7b5a46a5d33a7c0bf41bc5392681fdf3ff4831f512
5
5
  SHA512:
6
- metadata.gz: c9af2f42bfa9f0335d24b8062cb316ac5679b37753420418802caa073e86cb32eb291c1e32ad6c0ec03c5d3ed8f67030fe505f96c78f267831c1ced7456abd7a
7
- data.tar.gz: 163bfff112548e72f2c18099b7fc0bb3d0d32e6ac450ffda0773b9237a92207247bf697ee9319a0f86bbfe67aed515ebaf77e943dc66cf0620693609020f7a58
6
+ metadata.gz: 5cef51963137cdccaf2df37be1d4337bbdc54904afda8a02541451fe2755645d139e6fc246f183aa923fc35d3fbcca432016f4c368419dd1d248ec8696355e4e
7
+ data.tar.gz: 76f60d66aebc013a709155442104dcd47e7dc50db030e326e8c8e75f58cf5a7b9061e8f3a2b4586998af0d121edf1c664eaeaa472d4cda1d98b0b6b582fdc10b
data/lib/wave_dispatch.rb CHANGED
@@ -1,19 +1,25 @@
1
1
  # wave Dispatch — thin Ruby client. Route each request to the cheapest capable model (local-first;
2
2
  # escalate to your frontier only when needed). BYO keys + infra. Stdlib only (net/http).
3
+ require "base64"
3
4
  require "net/http"
4
5
  require "json"
5
6
  require "uri"
6
7
  require "openssl"
7
8
 
8
9
  module WaveDispatch
9
- VERSION = "0.5.0"
10
+ VERSION = "0.5.1"
10
11
 
12
+ # 0.5.1 — payment hook: a Proc called once with the 402 challenge body (Hash) that returns the
13
+ # headers (Hash[String => String]) to retry the request with. Pair with `Client.wallet_hook(provider:,
14
+ # credentials:)` for the built-in CDP / Privy / Bridge factories, or build a Proc yourself.
11
15
  class Client
12
16
  def initialize(license = ENV["WAVE_LICENSE"], endpoint: "https://dispatch.wave.online",
13
- agents_endpoint: ENV["WAVE_AGENTS_ENDPOINT"] || "https://dispatch-agents.wave.online")
17
+ agents_endpoint: ENV["WAVE_AGENTS_ENDPOINT"] || "https://dispatch-agents.wave.online",
18
+ payment_hook: nil)
14
19
  @license = license
15
20
  @endpoint = endpoint
16
21
  @agents = agents_endpoint
22
+ @payment_hook = payment_hook # 0.5.1 — handles 402 inside the client
17
23
  end
18
24
 
19
25
  # Classify a prompt (no execution) -> {"route", "probability", "margin", "forward"}
@@ -22,6 +28,9 @@ module WaveDispatch
22
28
  # Classify and run on the edge if your plan allows it.
23
29
  def execute(prompt) = send_req(:post, @endpoint + "/", { prompt: prompt, execute: true })
24
30
 
31
+ # Classify a pre-computed 768-d embedding (matmul-only: cheapest + fastest).
32
+ def route_vector(vector) = send_req(:post, @endpoint + "/", { vector: vector })
33
+
25
34
  # This license's savings ledger (decisions, saved_usd, saved_pct, ...). Requires a license.
26
35
  def savings = send_req(:get, "#{@agents}/ledger/summary?license=#{lic}")
27
36
 
@@ -31,6 +40,17 @@ module WaveDispatch
31
40
  # Start/replace a programmatic subscription (plan: agent_starter | agent_pro | agent_scale).
32
41
  def subscribe(plan) = send_req(:post, "#{@agents}/subscription/create", { license: @license, plan: plan })
33
42
 
43
+ # 0.5.1 — build a payment_hook for a built-in provider. provider: :cdp | :privy | :bridge.
44
+ # credentials per provider: cdp: {api_key:, api_secret:, address:}; privy: {app_id:, app_secret:,
45
+ # wallet_id:}; bridge: {api_key:, source_wallet:, destination:}. For custom wallets, build a Proc
46
+ # of your own that returns the header Hash you want set on the retry.
47
+ def self.wallet_hook(provider:, credentials: {})
48
+ provider = provider.to_sym
49
+ header = { cdp: "cdp-payment", privy: "privy-payment", bridge: "bridge-payment" }[provider]
50
+ raise "dispatch.wallet_hook: unknown provider #{provider.inspect}" unless header
51
+ ->(challenge) { { header => WaveDispatch.wallet_sign(provider, credentials, challenge) } }
52
+ end
53
+
34
54
  private
35
55
 
36
56
  def lic
@@ -38,18 +58,73 @@ module WaveDispatch
38
58
  URI.encode_www_form_component(@license)
39
59
  end
40
60
 
41
- def send_req(method, url, body = nil)
61
+ def build_req(method, url, body)
42
62
  uri = URI(url)
43
63
  req = (method == :post ? Net::HTTP::Post : Net::HTTP::Get).new(uri, "content-type" => "application/json")
44
64
  req["authorization"] = "Bearer #{@license}" if @license
45
65
  req.body = body.to_json if body
46
- res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
47
- verify_mode: OpenSSL::SSL::VERIFY_PEER) { |h| h.request(req) }
48
- raise "dispatch: 402 payment required (x402)" if res.code == "402"
66
+ [uri, req]
67
+ end
68
+
69
+ def http_call(uri, req)
70
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
71
+ verify_mode: OpenSSL::SSL::VERIFY_PEER) { |h| h.request(req) }
72
+ end
73
+
74
+ def send_req(method, url, body = nil)
75
+ uri, req = build_req(method, url, body)
76
+ res = http_call(uri, req)
77
+ if res.code == "402" && @payment_hook
78
+ challenge = (JSON.parse(res.body) rescue {})
79
+ extra = @payment_hook.call(challenge) || {}
80
+ uri2, req2 = build_req(method, url, body)
81
+ extra.each { |k, v| req2[k.to_s] = v }
82
+ res = http_call(uri2, req2)
83
+ end
84
+ raise "dispatch: 402 payment required (x402) — pay and retry, or set a license / payment_hook" if res.code == "402"
49
85
  raise "dispatch: 401 unauthorized — set a valid license" if res.code == "401"
50
- # any other non-2xx must raise — never JSON.parse an error body and return it as a success result
51
86
  raise "dispatch: #{res.code} #{res.body.to_s[0, 160]}" unless res.code.start_with?("2")
52
87
  JSON.parse(res.body)
53
88
  end
54
89
  end
90
+
91
+ # Built-in provider sign — HTTP orchestration only; actual signing happens at the provider.
92
+ def self.wallet_sign(provider, creds, challenge)
93
+ accepts = challenge.is_a?(Hash) ? (challenge["accepts"] || []) : []
94
+ accept = accepts.find { |a| a.is_a?(Hash) && a["protocol"] == provider.to_s } || accepts.first || {}
95
+ case provider
96
+ when :cdp
97
+ # CDP-JWT signing is non-trivial in stdlib-only Ruby; the built-in returns a marker payload that
98
+ # the worker accepts via the wave-payments adapter when WAVE_VERIFY_URL is set. For full on-chain
99
+ # CDP signing, build your own Proc with the official Coinbase CDP gem.
100
+ { provider: "cdp", address: creds[:address], accept: accept,
101
+ hint: "use the coinbase-cdp gem for CDP-JWT signing in production" }.to_json
102
+ when :privy
103
+ %i[app_id app_secret wallet_id].each { |k| raise "dispatch.wallet_hook(privy): #{k} required" unless creds[k] }
104
+ basic = Base64.strict_encode64("#{creds[:app_id]}:#{creds[:app_secret]}")
105
+ uri = URI("https://auth.privy.io/api/v1/wallets/#{URI.encode_www_form_component(creds[:wallet_id])}/rpc")
106
+ req = Net::HTTP::Post.new(uri, "content-type" => "application/json",
107
+ "authorization" => "Basic #{basic}", "privy-app-id" => creds[:app_id])
108
+ req.body = { method: "personal_sign", params: { message: accept.to_json }, chain_type: "ethereum" }.to_json
109
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
110
+ verify_mode: OpenSSL::SSL::VERIFY_PEER) { |h| h.request(req) }
111
+ raise "dispatch.wallet_hook(privy): provider #{res.code}" unless res.code.start_with?("2")
112
+ j = (JSON.parse(res.body) rescue {})
113
+ sig = (j.dig("data", "signature") || j["signature"])
114
+ { provider: "privy", signature: sig, accept: accept }.to_json
115
+ when :bridge
116
+ raise "dispatch.wallet_hook(bridge): :api_key required" unless creds[:api_key]
117
+ uri = URI("https://api.bridge.xyz/v0/transfers")
118
+ req = Net::HTTP::Post.new(uri, "content-type" => "application/json", "api-key" => creds[:api_key])
119
+ req.body = { source: creds[:source_wallet], destination: creds[:destination] || accept["payTo"],
120
+ amount: accept["maxAmountRequired"] }.to_json
121
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
122
+ verify_mode: OpenSSL::SSL::VERIFY_PEER) { |h| h.request(req) }
123
+ raise "dispatch.wallet_hook(bridge): provider #{res.code}" unless res.code.start_with?("2")
124
+ j = (JSON.parse(res.body) rescue {})
125
+ { provider: "bridge", id: j["id"], accept: accept }.to_json
126
+ else
127
+ raise "dispatch.wallet_sign: unsupported provider #{provider.inspect}"
128
+ end
129
+ end
55
130
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wave-dispatch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - WAVE Online, LLC