kavel 0.1.0 → 0.2.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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -0
  3. data/lib/kavel/version.rb +1 -1
  4. data/lib/kavel.rb +39 -16
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 679fb519c2cc395d4219b8e350479387aed76177523c661eb6f9a4b37c8bc5dd
4
- data.tar.gz: 4b77c92e7c6e7cc66ffa0432cec5a71b7a9cc1e22c416e04328031bb8cfa1284
3
+ metadata.gz: 3414ddd1c59fa21b575190d22e60c64d4c5e2ef6c8a2c843b55b3d15a3b29a24
4
+ data.tar.gz: bfbd5468f1109256684a3bd7580abe178cfd7bbb8a1cd07556a7a59043cd2251
5
5
  SHA512:
6
- metadata.gz: f112956669e912aa67ce9a0ddd20be03e042f2f82e9330f0dba678c8d7c956600eb7f2239b583ba7fa4e297937d2276c56f3f1135d48cb3a6be704a00fddef4d
7
- data.tar.gz: 599a92a96c155b8bbcd976b76d05e23ceee3b33c0ed99db13d57bd30529302d0350f3fa6d3bb64061e1cea8495f5abed0981d6be8e53fe794064e6d9f78ef74b
6
+ metadata.gz: 2432f1bddc83e6102612deee1ecbcfff03546827145490977b3bddf126aab46878333525b455f5b659adbe3040f4e58e64e45a864eb81f8f48287dbaa251deab
7
+ data.tar.gz: 283153dc729a97e5e13a0440bd6ebd3bde8360cf82266c96558a93c12b2a840fd29a4e507aa0576fc8c244bceea8d9d986ec863b47a0edf258941080601dcb42
data/README.md CHANGED
@@ -55,6 +55,20 @@ when the picture has to contain correctly spelled text, and
55
55
  [Seedream 5.0 Pro](https://www.kavel.ai/image/seedream-5-pro?utm_source=rubygems&utm_medium=package) for
56
56
  photoreal people. [Pricing](https://www.kavel.ai/pricing?utm_source=rubygems&utm_medium=package) has every tier.
57
57
 
58
+ ## Past the free tier: an API key
59
+
60
+ When the free allowance runs out, the error tells you where to go next. Create a key at
61
+ [kavel.ai/settings/apikeys](https://www.kavel.ai/settings/apikeys?utm_source=rubygems&utm_medium=package) — the same account
62
+ you use on the site — and pass it (or set `KAVEL_API_KEY`):
63
+
64
+ ```ruby
65
+ img = Kavel.generate(prompt, api_key: ENV["KAVEL_API_KEY"], model: "gpt-image-2")
66
+ ```
67
+
68
+ With a key every call runs on your account, exactly as it would on the site: your
69
+ [credits and plan](https://www.kavel.ai/pricing?utm_source=rubygems&utm_medium=package), no per-IP ceiling, no watermark on a
70
+ paid plan, and any image model your plan includes. `Kavel::AuthError` means the key is wrong; `Kavel::QuotaError` with a key means the account is out of credits.
71
+
58
72
  ## Prompts that work
59
73
 
60
74
  Name the light, the material and the composition — that moves the result more than adjectives do.
data/lib/kavel/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kavel
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/kavel.rb CHANGED
@@ -31,17 +31,23 @@ module Kavel
31
31
  class QuotaError < Error; end
32
32
  # The content filter or model refused the prompt. Reword it.
33
33
  class RejectedError < Error; end
34
- # The request needs a signed-in account (video, or a model off the free shelf).
34
+ # The request needs a signed-in account, or a plan that includes this model.
35
35
  class SignInError < Error; end
36
+ # The API key is invalid or has been deleted.
37
+ class AuthError < Error; end
36
38
  # The deadline passed while the job was queued or running.
37
39
  class TimeoutError < Error; end
38
40
 
39
41
  class << self
40
42
  # Turn a prompt into a new image. Name the light, the material and the composition.
43
+ #
44
+ # Pass +api_key:+ (a key from https://www.kavel.ai/settings/apikeys, or set
45
+ # KAVEL_API_KEY) to run on your account: your credits, your plan's models and
46
+ # no watermark on a paid plan. +model:+ is honoured only with a key.
41
47
  def generate(prompt, aspect_ratio: "1:1", **opts)
42
48
  raise ArgumentError, "prompt is required" if prompt.to_s.strip.empty?
43
49
 
44
- run({ provider: "kie", mediaType: "image", model: MODEL_GENERATE, scene: "text-to-image",
50
+ run({ provider: "kie", mediaType: "image", model: pick_model(opts, MODEL_GENERATE), scene: "text-to-image",
45
51
  prompt: prompt, options: { aspect_ratio: aspect_ratio } }, **opts)
46
52
  end
47
53
 
@@ -50,7 +56,7 @@ module Kavel
50
56
  raise ArgumentError, "source_url must be a public http(s) url" unless source_url.to_s =~ %r{\Ahttps?://}
51
57
  raise ArgumentError, "instruction is required" if instruction.to_s.strip.empty?
52
58
 
53
- run({ provider: "kie", mediaType: "image", model: MODEL_EDIT, scene: "image-to-image",
59
+ run({ provider: "kie", mediaType: "image", model: pick_model(opts, MODEL_EDIT), scene: "image-to-image",
54
60
  prompt: instruction, options: { image_input: [source_url] } }, **opts)
55
61
  end
56
62
 
@@ -64,16 +70,19 @@ module Kavel
64
70
  def parse_submit(env)
65
71
  if env["code"] != 0
66
72
  msg = env["message"].to_s
67
- raise SignInError, msg if msg.downcase.include?("sign in")
73
+ low = msg.downcase
74
+ raise AuthError, msg if low.include?("invalid api key")
75
+ raise QuotaError, msg if low.include?("insufficient credits")
76
+ raise SignInError, msg if low.include?("sign in") || low.include?("subscription")
68
77
 
69
78
  raise Error, msg
70
79
  end
71
80
  data = env["data"] || {}
72
81
  if data["wall"] == true
73
- raise QuotaError, "this machine has used its #{IP_DAILY_CEILING} credits for today" if data["reason"] == "anon_ip_daily"
82
+ raise QuotaError, "this machine has used its #{IP_DAILY_CEILING} free credits for today — pass api_key: from #{BASE_URL}/settings/apikeys to keep going" if data["reason"] == "anon_ip_daily"
74
83
  raise SignInError, "video" if data["reason"] == "anon_unmetered_video"
75
84
 
76
- raise QuotaError, "free allowance spent"
85
+ raise QuotaError, "free allowance spent — pass api_key: from #{BASE_URL}/settings/apikeys to keep going"
77
86
  end
78
87
  id = data["id"].to_s
79
88
  raise Error, "the service returned no task id" if id.empty?
@@ -83,19 +92,30 @@ module Kavel
83
92
 
84
93
  private
85
94
 
86
- def run(payload, base_url: BASE_URL, poll_every: 5, timeout: 360, transport: nil)
95
+ def pick_model(opts, free)
96
+ key = opts[:api_key] || ENV["KAVEL_API_KEY"]
97
+ key && !key.empty? && opts[:model] ? opts[:model] : free
98
+ end
99
+
100
+ def run(payload, base_url: BASE_URL, poll_every: 5, timeout: 360, transport: nil, api_key: nil, model: nil)
87
101
  http = transport || method(:request)
88
102
  deadline = Time.now + timeout
89
- id = anon_id
90
- task = parse_submit(http.call(:post, "#{base_url}/api/ai/generate", payload, id))
91
-
92
- # Free runs queue 25-80s and only reach the model on the poll that crosses
93
- # the end of it, so polling is what starts the work.
94
- query = "#{base_url}/api/ai/anon-query?taskId=#{URI.encode_www_form_component(task)}&provider=kie&mediaType=image"
103
+ key = api_key || ENV["KAVEL_API_KEY"]
104
+ key = nil if key && key.empty?
105
+ # With a key the account is the identity; without one, a fresh anonymous id.
106
+ headers = key ? { "authorization" => "Bearer #{key}" } : { "x-anon-id" => anon_id }
107
+ task = parse_submit(http.call(:post, "#{base_url}/api/ai/generate", payload, headers))
108
+
109
+ # Queued runs only reach the model on the poll that crosses the end of the
110
+ # wait, so polling is what starts the work.
95
111
  while Time.now < deadline
96
112
  sleep poll_every
97
113
  begin
98
- env = http.call(:get, query, nil, id)
114
+ env = if key
115
+ http.call(:post, "#{base_url}/api/ai/query", { taskId: task }, headers)
116
+ else
117
+ http.call(:get, "#{base_url}/api/ai/anon-query?taskId=#{URI.encode_www_form_component(task)}&provider=kie&mediaType=image", nil, headers)
118
+ end
99
119
  rescue StandardError => e
100
120
  raise if e.is_a?(Error)
101
121
 
@@ -104,6 +124,9 @@ module Kavel
104
124
  next unless env["code"] == 0
105
125
 
106
126
  data = env["data"] || {}
127
+ clean = data["cleanImages"] || []
128
+ return Image.new(clean[0], false) unless clean.empty?
129
+
107
130
  images = data["images"] || []
108
131
  return Image.new(images[0], (data["watermarked"] || [])[0] == true) unless images.empty?
109
132
  raise RejectedError, "prompt refused" if %w[failed error].include?(data["status"])
@@ -111,10 +134,10 @@ module Kavel
111
134
  raise TimeoutError, "deadline passed"
112
135
  end
113
136
 
114
- def request(verb, url, body, anon_id)
137
+ def request(verb, url, body, headers)
115
138
  uri = URI(url)
116
139
  req = verb == :post ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
117
- req["x-anon-id"] = anon_id
140
+ headers.each { |k, v| req[k] = v }
118
141
  req["user-agent"] = "kavel-rb/#{VERSION}"
119
142
  if body
120
143
  req["content-type"] = "application/json"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kavel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kavel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-13 00:00:00.000000000 Z
11
+ date: 2026-09-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'A zero-dependency client for the free tier of kavel.ai: text-to-image
14
14
  and image editing without an API key, an account or a card.'