fal 0.0.1 → 0.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: 294f1aa2fea312c2b0a42b2fe97753e45a8d8807083d5a60a3c739b7bdd722f6
4
- data.tar.gz: 93fb4193c178ffd22bfddeffab3926a4288afd89e42f9bd418d9a71ce45c2645
3
+ metadata.gz: 4ff4bd4724ee6b7ea39f7c72f51503e4e918b50f019972a08836efd6d22fdffc
4
+ data.tar.gz: 4f9020293f0b639487e836668116fadfcd51768ef0401e335a18394be155c930
5
5
  SHA512:
6
- metadata.gz: 3d328838cdf0dc3eff20b2275ecff9528d90bf8d549272b663f0b985b7fd2d3744e4a60e5b621a96a7a3f9a549b506e28f83776c5c84514d9d541fc376ed27d0
7
- data.tar.gz: 112f8673d40fd9195c1b157656e2f1c9dc8208e14c460207672cefd8e2264fac2f9857df17a11d2d174f9617c7771700f71e71ed797d6f1c820fcd9613cd472d
6
+ metadata.gz: d7646551a9cffa3a58b9ce0ac08f48e258cedd9e19f7ad18e9ae8fc3fe0aa85c774e5a21a283026ef5a22e0126c8c3db52d2667bc7cca285f8d9b6d74c4ac3b7
7
+ data.tar.gz: 0f1f6137e731796d859ff4bd51eb92078453530498dfd72d436e8c9999dd4f1f26087a99c010f533fb231bb2012186d10fee972811ff44d268e77a71d1180e3a
data/.env.example ADDED
@@ -0,0 +1 @@
1
+ FAL_KEY=
data/.rubocop.yml CHANGED
@@ -40,3 +40,11 @@ Style/HashSyntax:
40
40
  Naming/FileName:
41
41
  Exclude:
42
42
  - "lib/fal.rb"
43
+
44
+ Style/RedundantInitialize:
45
+ Exclude:
46
+ - "rbi/**/*.rbi"
47
+
48
+ Naming/BlockForwarding:
49
+ Exclude:
50
+ - "rbi/**/*.rbi"
data/Gemfile CHANGED
@@ -7,3 +7,8 @@ gemspec
7
7
  gem "minitest", "~> 5.0"
8
8
  gem "rake", "~> 13.0"
9
9
  gem "rubocop", "~> 1.21"
10
+ gem "dotenv"
11
+ gem "sorbet"
12
+ gem "tapioca"
13
+ gem "vcr"
14
+ gem "webmock"
data/Gemfile.lock CHANGED
@@ -1,62 +1,126 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fal (0.0.1)
4
+ fal (0.1.0)
5
5
  faraday (>= 1)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- ast (2.4.2)
10
+ addressable (2.8.7)
11
+ public_suffix (>= 2.0.2, < 7.0)
12
+ ast (2.4.3)
13
+ base64 (0.3.0)
14
+ benchmark (0.5.0)
15
+ bigdecimal (3.3.1)
16
+ crack (1.0.1)
17
+ bigdecimal
18
+ rexml
19
+ dotenv (3.1.8)
20
+ erubi (1.13.1)
11
21
  faraday (2.14.0)
12
22
  faraday-net_http (>= 2.0, < 3.5)
13
23
  json
14
24
  logger
15
25
  faraday-net_http (3.4.1)
16
26
  net-http (>= 0.5.0)
17
- json (2.7.2)
18
- language_server-protocol (3.17.0.3)
27
+ hashdiff (1.2.1)
28
+ json (2.15.2)
29
+ language_server-protocol (3.17.0.5)
30
+ lint_roller (1.1.0)
19
31
  logger (1.7.0)
20
- minitest (5.23.1)
21
- net-http (0.6.0)
32
+ minitest (5.26.0)
33
+ net-http (0.7.0)
22
34
  uri
23
- parallel (1.24.0)
24
- parser (3.3.1.0)
35
+ netrc (0.11.0)
36
+ parallel (1.27.0)
37
+ parser (3.3.10.0)
25
38
  ast (~> 2.4.1)
26
39
  racc
27
- racc (1.8.0)
40
+ prism (1.6.0)
41
+ public_suffix (6.0.2)
42
+ racc (1.8.1)
28
43
  rainbow (3.1.1)
29
- rake (13.2.1)
30
- regexp_parser (2.9.2)
31
- rexml (3.2.8)
32
- strscan (>= 3.0.9)
33
- rubocop (1.64.0)
44
+ rake (13.3.1)
45
+ rbi (0.3.7)
46
+ prism (~> 1.0)
47
+ rbs (>= 3.4.4)
48
+ rbs (3.9.5)
49
+ logger
50
+ regexp_parser (2.11.3)
51
+ rexml (3.4.4)
52
+ rubocop (1.81.7)
34
53
  json (~> 2.3)
35
- language_server-protocol (>= 3.17.0)
54
+ language_server-protocol (~> 3.17.0.2)
55
+ lint_roller (~> 1.1.0)
36
56
  parallel (~> 1.10)
37
57
  parser (>= 3.3.0.2)
38
58
  rainbow (>= 2.2.2, < 4.0)
39
- regexp_parser (>= 1.8, < 3.0)
40
- rexml (>= 3.2.5, < 4.0)
41
- rubocop-ast (>= 1.31.1, < 2.0)
59
+ regexp_parser (>= 2.9.3, < 3.0)
60
+ rubocop-ast (>= 1.47.1, < 2.0)
42
61
  ruby-progressbar (~> 1.7)
43
- unicode-display_width (>= 2.4.0, < 3.0)
44
- rubocop-ast (1.31.3)
45
- parser (>= 3.3.1.0)
62
+ unicode-display_width (>= 2.4.0, < 4.0)
63
+ rubocop-ast (1.47.1)
64
+ parser (>= 3.3.7.2)
65
+ prism (~> 1.4)
46
66
  ruby-progressbar (1.13.0)
47
- strscan (3.1.0)
48
- unicode-display_width (2.5.0)
49
- uri (1.0.3)
67
+ sorbet (0.6.12689)
68
+ sorbet-static (= 0.6.12689)
69
+ sorbet-runtime (0.6.12689)
70
+ sorbet-static (0.6.12689-aarch64-linux)
71
+ sorbet-static (0.6.12689-universal-darwin)
72
+ sorbet-static (0.6.12689-x86_64-linux)
73
+ sorbet-static-and-runtime (0.6.12689)
74
+ sorbet (= 0.6.12689)
75
+ sorbet-runtime (= 0.6.12689)
76
+ spoom (1.6.3)
77
+ erubi (>= 1.10.0)
78
+ prism (>= 0.28.0)
79
+ rbi (>= 0.3.3)
80
+ rexml (>= 3.2.6)
81
+ sorbet-static-and-runtime (>= 0.5.10187)
82
+ thor (>= 0.19.2)
83
+ tapioca (0.16.11)
84
+ benchmark
85
+ bundler (>= 2.2.25)
86
+ netrc (>= 0.11.0)
87
+ parallel (>= 1.21.0)
88
+ rbi (~> 0.2)
89
+ sorbet-static-and-runtime (>= 0.5.11087)
90
+ spoom (>= 1.2.0)
91
+ thor (>= 1.2.0)
92
+ yard-sorbet
93
+ thor (1.4.0)
94
+ unicode-display_width (3.2.0)
95
+ unicode-emoji (~> 4.1)
96
+ unicode-emoji (4.1.0)
97
+ uri (1.1.0)
98
+ vcr (6.3.1)
99
+ base64
100
+ webmock (3.26.1)
101
+ addressable (>= 2.8.0)
102
+ crack (>= 0.3.2)
103
+ hashdiff (>= 0.4.0, < 2.0.0)
104
+ yard (0.9.37)
105
+ yard-sorbet (0.9.0)
106
+ sorbet-runtime
107
+ yard
50
108
 
51
109
  PLATFORMS
52
- ruby
53
- x86_64-darwin-23
110
+ aarch64-linux
111
+ universal-darwin
112
+ x86_64-linux
54
113
 
55
114
  DEPENDENCIES
115
+ dotenv
56
116
  fal!
57
117
  minitest (~> 5.0)
58
118
  rake (~> 13.0)
59
119
  rubocop (~> 1.21)
120
+ sorbet
121
+ tapioca
122
+ vcr
123
+ webmock
60
124
 
61
125
  BUNDLED WITH
62
126
  2.5.7
data/README.md CHANGED
@@ -9,3 +9,275 @@ Install the gem and add to the application"s Gemfile by executing:
9
9
  If bundler is not being used to manage dependencies, install the gem by executing:
10
10
 
11
11
  $ gem install fal
12
+
13
+ ## Usage
14
+
15
+ ### Configuration
16
+
17
+ Configure the client once at boot (e.g., in Rails an initializer) using `Fal.configure`.
18
+
19
+ ```ruby
20
+ Fal.configure do |config|
21
+ config.api_key = "your-key" # Optional. Defaults to ENV["FAL_KEY"] if not set.
22
+ config.queue_base = "https://queue.fal.run" # Optional (default: https://queue.fal.run)
23
+ config.sync_base = "https://fal.run" # Optional (default: https://fal.run)
24
+ config.api_base = "https://api.fal.ai/v1" # Optional (default: https://api.fal.ai/v1)
25
+ config.request_timeout = 120 # Optional (default: 120)
26
+ end
27
+ ```
28
+
29
+ ### Create a queued request
30
+
31
+ The Queue API is the recommended way to call models on fal. Provide a `endpoint_id` in "namespace/name" format and an input payload.
32
+
33
+ ```ruby
34
+ endpoint_id = "fal-ai/fast-sdxl"
35
+
36
+ request = Fal::Request.create!(
37
+ endpoint_id: endpoint_id,
38
+ input: { prompt: "a cat" }
39
+ )
40
+
41
+ request.id # => request_id from fal
42
+ request.status # => "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED"
43
+ ```
44
+
45
+ You can also specify a webhook URL to be notified when the request is finished.
46
+
47
+ ```ruby
48
+ request = Fal::Request.create!(
49
+ endpoint_id: endpoint_id,
50
+ input: { prompt: "a cat playing piano" },
51
+ webhook_url: "https://example.com/fal/webhook"
52
+ )
53
+ ```
54
+
55
+ ### Get request status (find and reload)
56
+
57
+ Fetch the current status by id:
58
+
59
+ ```ruby
60
+ status = Fal::Request.find_by!(id: request.id, endpoint_id: endpoint_id)
61
+ status.in_queue? # => true/false
62
+ status.in_progress? # => true/false
63
+ status.completed? # => true/false
64
+ ```
65
+
66
+ Reload an instance in-place, optionally including logs.
67
+
68
+ ```ruby
69
+ request.reload! # refreshes state
70
+ request.reload!(logs: true)
71
+ request.logs # => array of log entries (if provided by model and logs=1)
72
+ # When status is COMPLETED, reload! will also fetch and set request.response
73
+ ```
74
+
75
+ Status constants are available for direct comparisons:
76
+
77
+ ```ruby
78
+ Fal::Request::Status::IN_QUEUE
79
+ Fal::Request::Status::IN_PROGRESS
80
+ Fal::Request::Status::COMPLETED
81
+ ```
82
+
83
+ ### Fetch the response payload after completion
84
+
85
+ Call `reload!` to populate `request.response`:
86
+
87
+ ```ruby
88
+ # poll until completed
89
+ until request.completed?
90
+ request.reload!
91
+ sleep 1
92
+ end
93
+
94
+ request.response # => model-specific response body
95
+ ```
96
+
97
+ ### Cancel a request
98
+
99
+ Requests that are still in the queue can be cancelled:
100
+
101
+ ```ruby
102
+ request.cancel! # => { "status" => "CANCELLATION_REQUESTED" }
103
+ ```
104
+
105
+ ### Webhooks
106
+
107
+ fal can POST a webhook to your server when a request completes. Use `Fal::WebhookRequest` to parse the incoming payload.
108
+
109
+ ```ruby
110
+ # rails controller example
111
+ class FalWebhooksController < ApplicationController
112
+ skip_before_action :verify_authenticity_token
113
+
114
+ def create
115
+ webhook = Fal::WebhookRequest.from_rack_request(request)
116
+
117
+ if webhook.success?
118
+ # webhook.response contains the model-specific payload
119
+ # webhook.logs, webhook.metrics may also be present
120
+ head :ok
121
+ else
122
+ Rails.logger.error("fal webhook error: #{webhook.error} detail=#{webhook.error_detail}")
123
+ head :ok
124
+ end
125
+ end
126
+ end
127
+ ```
128
+
129
+ ### Error handling
130
+
131
+ HTTP and API errors raise typed exceptions:
132
+
133
+ - `Fal::UnauthorizedError` (401)
134
+ - `Fal::ForbiddenError` (403)
135
+ - `Fal::NotFoundError` (404)
136
+ - `Fal::ServerError` (other non-success)
137
+
138
+ Rescue them as needed:
139
+
140
+ ```ruby
141
+ begin
142
+ Fal::Request.create!(endpoint_id: endpoint_id, input: { prompt: "hi" })
143
+ rescue Fal::UnauthorizedError
144
+ # handle invalid/missing FAL_KEY
145
+ end
146
+ ```
147
+
148
+ ### Stream synchronous responses
149
+
150
+ Use `stream!` for SSE streaming from synchronous endpoints. It yields each chunk’s data Hash and returns a `Fal::Request` whose `response` contains the last chunk’s payload.
151
+
152
+ ```ruby
153
+ endpoint_id = "fal-ai/flux/dev"
154
+
155
+ last = Fal::Request.stream!(endpoint_id: endpoint_id, input: { prompt: "a cat" }) do |chunk|
156
+ # chunk is a Hash, e.g. { images: [...] }
157
+ puts chunk
158
+ end
159
+
160
+ last.completed? # => true/false
161
+ last.response # => last streamed data hash (e.g., { "response" => { ... } } or final payload)
162
+ ```
163
+
164
+ ### Pricing
165
+
166
+ Use the Platform API to fetch per-endpoint pricing.
167
+
168
+ Find pricing for a single endpoint:
169
+
170
+ ```ruby
171
+ price = Fal::Price.find_by(endpoint_id: "fal-ai/flux/dev")
172
+ price.unit_price # => e.g., 0.025
173
+ price.unit # => e.g., "image"
174
+ price.currency # => e.g., "USD"
175
+ ```
176
+
177
+ Iterate through all prices (auto-paginates):
178
+
179
+ ```ruby
180
+ Fal::Price.each do |p|
181
+ puts "#{p.endpoint_id} => #{p.unit_price} #{p.currency} per #{p.unit}"
182
+ end
183
+ ```
184
+
185
+ Collect all prices as an array:
186
+
187
+ ```ruby
188
+ prices = Fal::Price.all
189
+ ```
190
+
191
+ ### Estimate cost
192
+
193
+ Compute a total cost estimate across endpoints using historical API price or unit price.
194
+
195
+ Unit price (uses billing units like images/videos):
196
+
197
+ ```ruby
198
+ estimate = Fal::PriceEstimate.create(
199
+ estimate_type: Fal::PriceEstimate::EstimateType::UNIT_PRICE,
200
+ endpoints: [
201
+ # You can pass unit_quantity directly
202
+ Fal::PriceEstimate::Endpoint.new(endpoint_id: "fal-ai/flux/dev", unit_quantity: 50),
203
+ # Or use call_quantity as a convenience alias for units
204
+ Fal::PriceEstimate::Endpoint.new(endpoint_id: "fal-ai/flux-pro", call_quantity: 25)
205
+ ]
206
+ )
207
+
208
+ estimate.estimate_type # => "unit_price"
209
+ estimate.total_cost # => e.g., 1.88
210
+ estimate.currency # => "USD"
211
+ ```
212
+
213
+ Historical API price (uses calls per endpoint):
214
+
215
+ ```ruby
216
+ estimate = Fal::PriceEstimate.create(
217
+ estimate_type: Fal::PriceEstimate::EstimateType::HISTORICAL_API_PRICE,
218
+ endpoints: [
219
+ Fal::PriceEstimate::Endpoint.new(endpoint_id: "fal-ai/flux/dev", call_quantity: 100)
220
+ ]
221
+ )
222
+ ```
223
+
224
+ ### Models
225
+
226
+ List, search, and find models via the Models API.
227
+
228
+ Find a model by endpoint ID:
229
+
230
+ ```ruby
231
+ model = Fal::Model.find_by(endpoint_id: "fal-ai/flux/dev")
232
+ model.endpoint_id # => "fal-ai/flux/dev"
233
+ model.display_name # => e.g., "FLUX.1 [dev]"
234
+ model.category # => e.g., "text-to-image"
235
+ model.status # => "active" | "deprecated"
236
+ model.tags # => ["fast", "pro"]
237
+ model.model_url # => "https://fal.run/..."
238
+ model.thumbnail_url # => "https://..."
239
+ ```
240
+
241
+ Iterate or collect all models (auto-paginates):
242
+
243
+ ```ruby
244
+ Fal::Model.each do |m|
245
+ puts m.endpoint_id
246
+ end
247
+
248
+ all_models = Fal::Model.all
249
+ ```
250
+
251
+ Search with filters:
252
+
253
+ ```ruby
254
+ results = Fal::Model.search(query: "text to image", status: "active")
255
+ ```
256
+
257
+ Get a model’s price (memoized):
258
+
259
+ ```ruby
260
+ price = model.price
261
+ price.unit_price # => e.g., 0.025
262
+ ```
263
+
264
+ Run a request for a model (uses the model's `endpoint_id` as `endpoint_id`):
265
+
266
+ ```ruby
267
+ request = model.run(input: { prompt: "a cat" })
268
+ ```
269
+
270
+ ### Development
271
+
272
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
273
+
274
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to rubygems.org.
275
+
276
+ For local development, copy the example environment file and set your API key so `bin/console` can load it automatically:
277
+
278
+ ```
279
+ cp .env.example .env
280
+ echo 'FAL_KEY=your_api_key_here' >> .env
281
+ ```
282
+
283
+ The console uses dotenv to load `.env`, so `Fal.configure` will default to `ENV["FAL_KEY"]`.
data/lib/fal/client.rb ADDED
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fal
4
+ class Client
5
+ attr_accessor :configuration
6
+
7
+ def initialize(configuration = Fal.configuration)
8
+ @configuration = configuration
9
+ end
10
+
11
+ def post(path, payload = {}, headers: {})
12
+ response = connection.post(build_url(path)) do |request|
13
+ request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
14
+ request.headers["Content-Type"] = "application/json"
15
+ request.headers["Accept"] = "application/json"
16
+ request.headers.merge!(headers)
17
+ request.body = payload.compact.to_json
18
+ end
19
+
20
+ handle_error(response) unless response.success?
21
+
22
+ parse_json(response.body)
23
+ end
24
+
25
+ # Perform a POST against the platform API base (api.fal.ai/v1)
26
+ # @param path [String]
27
+ # @param payload [Hash]
28
+ # @param headers [Hash]
29
+ # @return [Hash, nil]
30
+ def post_api(path, payload = {}, headers: {})
31
+ response = connection.post(build_api_url(path)) do |request|
32
+ request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
33
+ request.headers["Content-Type"] = "application/json"
34
+ request.headers["Accept"] = "application/json"
35
+ request.headers.merge!(headers)
36
+ request.body = payload.compact.to_json
37
+ end
38
+
39
+ handle_error(response) unless response.success?
40
+
41
+ parse_json(response.body)
42
+ end
43
+
44
+ # Perform a POST to the streaming (sync) base with SSE/text-event-stream handling.
45
+ # The provided on_data Proc will be used to receive chunked data.
46
+ # @param path [String]
47
+ # @param payload [Hash]
48
+ # @param on_data [Proc] called with chunks as they arrive
49
+ # @return [void]
50
+ def post_stream(path, payload = {}, on_data:)
51
+ url = build_sync_url(path)
52
+ connection.post(url) do |request|
53
+ request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
54
+ request.headers["Accept"] = "text/event-stream"
55
+ request.headers["Cache-Control"] = "no-store"
56
+ request.headers["Content-Type"] = "application/json"
57
+ request.body = payload.compact.to_json
58
+ request.options.on_data = on_data
59
+ end
60
+ end
61
+
62
+ def get(path, query: nil, headers: {})
63
+ url = build_url(path)
64
+ url = "#{url}?#{URI.encode_www_form(query)}" if query && !query.empty?
65
+
66
+ response = connection.get(url) do |request|
67
+ request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
68
+ request.headers["Accept"] = "application/json"
69
+ request.headers.merge!(headers)
70
+ end
71
+
72
+ handle_error(response) unless response.success?
73
+
74
+ parse_json(response.body)
75
+ end
76
+
77
+ # Perform a GET against the platform API base (api.fal.ai/v1)
78
+ # @param path [String]
79
+ # @param query [Hash, nil]
80
+ # @param headers [Hash]
81
+ # @return [Hash, nil]
82
+ def get_api(path, query: nil, headers: {})
83
+ url = build_api_url(path)
84
+ url = "#{url}?#{URI.encode_www_form(query)}" if query && !query.empty?
85
+
86
+ response = connection.get(url) do |request|
87
+ request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
88
+ request.headers["Accept"] = "application/json"
89
+ request.headers.merge!(headers)
90
+ end
91
+
92
+ handle_error(response) unless response.success?
93
+
94
+ parse_json(response.body)
95
+ end
96
+
97
+ def put(path)
98
+ response = connection.put(build_url(path)) do |request|
99
+ request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
100
+ request.headers["Content-Type"] = "application/json"
101
+ request.headers["Accept"] = "application/json"
102
+ request.body = {}.to_json
103
+ end
104
+
105
+ handle_error(response) unless response.success?
106
+
107
+ parse_json(response.body)
108
+ end
109
+
110
+ def handle_error(response)
111
+ case response.status
112
+ when 401
113
+ raise UnauthorizedError, response.body
114
+ when 403
115
+ raise ForbiddenError, response.body
116
+ when 404
117
+ raise NotFoundError, response.body
118
+ else
119
+ raise ServerError, response.body
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def parse_json(body)
126
+ return nil if body.nil? || body.strip.empty?
127
+
128
+ JSON.parse(body)
129
+ end
130
+
131
+ def build_url(path)
132
+ "#{@configuration.queue_base}#{path}"
133
+ end
134
+
135
+ def build_sync_url(path)
136
+ "#{@configuration.sync_base}#{path}"
137
+ end
138
+
139
+ def build_api_url(path)
140
+ "#{@configuration.api_base}#{path}"
141
+ end
142
+
143
+ def connection
144
+ Faraday.new do |faraday|
145
+ faraday.request :url_encoded
146
+ faraday.options.timeout = @configuration.request_timeout
147
+ faraday.options.open_timeout = @configuration.request_timeout
148
+ end
149
+ end
150
+ end
151
+ end