fal-ai 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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +133 -0
- data/Rakefile +12 -0
- data/lib/fal/client.rb +40 -0
- data/lib/fal/configuration.rb +38 -0
- data/lib/fal/connection.rb +71 -0
- data/lib/fal/endpoints.rb +71 -0
- data/lib/fal/errors.rb +45 -0
- data/lib/fal/queue.rb +37 -0
- data/lib/fal/request.rb +27 -0
- data/lib/fal/response.rb +53 -0
- data/lib/fal/status.rb +63 -0
- data/lib/fal/subscriber.rb +41 -0
- data/lib/fal/version.rb +5 -0
- data/lib/fal-ai.rb +5 -0
- data/lib/fal.rb +42 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8c31c5366749b46a776ea9f3805c748ba27b8a1db046caa47ed16675313acc3b
|
|
4
|
+
data.tar.gz: ecbcc1de0c64dd4a9f11921b64c4a1cb8c5aa8fb30607b0990a80281976a990a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e8ae00e9d1c02a4e69ea7448bfa1ddfa47e108a918259bed282e8e1e0c61ffe57a8000a39cc06c1943d5ea3fd45226d523bac465698028848d500ee294a5d1bd
|
|
7
|
+
data.tar.gz: c74109cc7c27b43fd987f39fa57f3a25ef6767e802b5333cbcb36591f5e77285e8e0b4de8361e7b449ba5581fa0181637ccb67bf2712cd095954db15ed34f737
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.3.6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-12-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release
|
|
13
|
+
- Synchronous `run` method for direct model inference
|
|
14
|
+
- Queue-based `subscribe` method with polling for long-running tasks
|
|
15
|
+
- Direct queue operations (`submit`, `status`, `result`)
|
|
16
|
+
- Support for all fal.ai models (Flux, Stable Diffusion, etc.)
|
|
17
|
+
- Configurable timeout and poll intervals
|
|
18
|
+
- Comprehensive error handling with typed exceptions
|
|
19
|
+
- Full test coverage
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Matt Culpepper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# fal-ai
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/fal-ai)
|
|
4
|
+
|
|
5
|
+
Ruby client for [fal.ai](https://fal.ai) - the generative AI platform with 600+ models.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "fal-ai"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install it yourself as:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install fal-ai
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Configuration
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require "fal-ai"
|
|
33
|
+
|
|
34
|
+
# Configure with API key (or set FAL_KEY environment variable)
|
|
35
|
+
Fal.configure do |config|
|
|
36
|
+
config.api_key = "your-api-key"
|
|
37
|
+
config.timeout = 300 # seconds (default: 300)
|
|
38
|
+
config.poll_interval = 0.5 # seconds (default: 0.5)
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Synchronous Run
|
|
43
|
+
|
|
44
|
+
For quick operations, use `run` to execute synchronously:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
client = Fal.client
|
|
48
|
+
|
|
49
|
+
result = client.run("fal-ai/flux/dev", {
|
|
50
|
+
prompt: "a beautiful sunset over mountains",
|
|
51
|
+
image_size: "landscape_16_9"
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
puts result["images"].first["url"]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Subscribe (Queue with Polling)
|
|
58
|
+
|
|
59
|
+
For longer operations, use `subscribe` to submit to the queue and poll until complete:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
client = Fal.client
|
|
63
|
+
|
|
64
|
+
result = client.subscribe("fal-ai/flux/dev", { prompt: "a cat" }) do |status|
|
|
65
|
+
case status
|
|
66
|
+
when Fal::Status::Queued
|
|
67
|
+
puts "Queued at position #{status.position}"
|
|
68
|
+
when Fal::Status::InProgress
|
|
69
|
+
puts "Processing..."
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
puts "Completed! Image: #{result['images'].first['url']}"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Direct Queue Operations
|
|
77
|
+
|
|
78
|
+
For more control, use queue operations directly:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
client = Fal.client
|
|
82
|
+
|
|
83
|
+
# Submit to queue
|
|
84
|
+
request_id = client.queue.submit("fal-ai/flux/dev", {
|
|
85
|
+
prompt: "a dog playing fetch"
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
puts "Submitted: #{request_id}"
|
|
89
|
+
|
|
90
|
+
# Poll for status
|
|
91
|
+
loop do
|
|
92
|
+
status = client.queue.status("fal-ai/flux/dev", request_id)
|
|
93
|
+
|
|
94
|
+
if status.completed?
|
|
95
|
+
result = client.queue.result("fal-ai/flux/dev", request_id)
|
|
96
|
+
puts "Done! #{result['images'].first['url']}"
|
|
97
|
+
break
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
puts "Status: #{status.class.name}"
|
|
101
|
+
sleep 1
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Error Handling
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
begin
|
|
109
|
+
result = client.run("fal-ai/flux/dev", { prompt: "a cat" })
|
|
110
|
+
rescue Fal::AuthenticationError
|
|
111
|
+
puts "Check your API key"
|
|
112
|
+
rescue Fal::RateLimitError => e
|
|
113
|
+
puts "Rate limited. Status: #{e.status_code}"
|
|
114
|
+
rescue Fal::ApiError => e
|
|
115
|
+
puts "API error: #{e.message}"
|
|
116
|
+
rescue Fal::ConnectionError => e
|
|
117
|
+
puts "Network issue: #{e.original_error}"
|
|
118
|
+
rescue Fal::TimeoutError
|
|
119
|
+
puts "Request timed out"
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt.
|
|
126
|
+
|
|
127
|
+
## Contributing
|
|
128
|
+
|
|
129
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mculp/fal-ai-ruby.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
data/Rakefile
ADDED
data/lib/fal/client.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fal
|
|
4
|
+
# Main client facade providing public API.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# client = Fal.client
|
|
8
|
+
# result = client.run("fal-ai/flux/dev", { prompt: "a cat" })
|
|
9
|
+
class Client
|
|
10
|
+
def initialize(config:, connection: nil)
|
|
11
|
+
@config = config
|
|
12
|
+
@connection = connection || Connection.new(config: config)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(app_id, input)
|
|
16
|
+
endpoint = Endpoints::Run.new(app_id: app_id, base_url: @config.run_url)
|
|
17
|
+
response = @connection.post(endpoint, body: input)
|
|
18
|
+
response.data
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def subscribe(app_id, input, &on_queue_update)
|
|
22
|
+
request_id = queue.submit(app_id, input)
|
|
23
|
+
subscriber.wait_for_completion(app_id, request_id, &on_queue_update)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def queue
|
|
27
|
+
@queue ||= Queue.new(connection: @connection, config: @config)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def subscriber
|
|
33
|
+
@subscriber ||= Subscriber.new(
|
|
34
|
+
queue: queue,
|
|
35
|
+
poll_interval: @config.poll_interval,
|
|
36
|
+
timeout: @config.timeout
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fal
|
|
4
|
+
# Holds configuration for the Fal client.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# Fal.configure do |config|
|
|
8
|
+
# config.api_key = "your-api-key"
|
|
9
|
+
# config.timeout = 300
|
|
10
|
+
# end
|
|
11
|
+
class Configuration
|
|
12
|
+
attr_accessor :timeout, :poll_interval
|
|
13
|
+
attr_writer :api_key
|
|
14
|
+
|
|
15
|
+
DEFAULT_TIMEOUT = 300
|
|
16
|
+
DEFAULT_POLL_INTERVAL = 0.5
|
|
17
|
+
RUN_HOST = "fal.run"
|
|
18
|
+
QUEUE_HOST = "queue.fal.run"
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@api_key = ENV.fetch("FAL_KEY", nil)
|
|
22
|
+
@timeout = DEFAULT_TIMEOUT
|
|
23
|
+
@poll_interval = DEFAULT_POLL_INTERVAL
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def api_key
|
|
27
|
+
@api_key or raise ConfigurationError, "API key not configured. Set FAL_KEY or call Fal.configure"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def run_url
|
|
31
|
+
"https://#{RUN_HOST}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def queue_url
|
|
35
|
+
"https://#{QUEUE_HOST}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "http"
|
|
4
|
+
|
|
5
|
+
module Fal
|
|
6
|
+
# HTTP connection wrapper using http.rb gem.
|
|
7
|
+
# Dependency-injected for testability.
|
|
8
|
+
class Connection
|
|
9
|
+
def initialize(config:, http: HTTP)
|
|
10
|
+
@config = config
|
|
11
|
+
@http = http
|
|
12
|
+
@request = Request.new(config: config)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def post(endpoint, body: nil)
|
|
16
|
+
response = perform_post(endpoint, body)
|
|
17
|
+
handle_response(response)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get(endpoint)
|
|
21
|
+
response = perform_get(endpoint)
|
|
22
|
+
handle_response(response)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def perform_post(endpoint, body)
|
|
28
|
+
@http
|
|
29
|
+
.headers(@request.headers)
|
|
30
|
+
.timeout(@config.timeout)
|
|
31
|
+
.post(endpoint.url, body: body ? @request.body(body) : nil)
|
|
32
|
+
rescue HTTP::Error => e
|
|
33
|
+
raise ConnectionError.new("HTTP request failed: #{e.message}", original_error: e)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def perform_get(endpoint)
|
|
37
|
+
@http
|
|
38
|
+
.headers(@request.headers)
|
|
39
|
+
.timeout(@config.timeout)
|
|
40
|
+
.get(endpoint.url)
|
|
41
|
+
rescue HTTP::Error => e
|
|
42
|
+
raise ConnectionError.new("HTTP request failed: #{e.message}", original_error: e)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def handle_response(http_response)
|
|
46
|
+
response = Response.new(http_response)
|
|
47
|
+
return response if response.success?
|
|
48
|
+
|
|
49
|
+
raise_api_error(response)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def raise_api_error(response)
|
|
53
|
+
error_class = error_class_for(response.status_code)
|
|
54
|
+
raise error_class.new(
|
|
55
|
+
response.error_message,
|
|
56
|
+
status_code: response.status_code,
|
|
57
|
+
response_body: response.data
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def error_class_for(status_code)
|
|
62
|
+
case status_code
|
|
63
|
+
when 401 then AuthenticationError
|
|
64
|
+
when 404 then NotFoundError
|
|
65
|
+
when 429 then RateLimitError
|
|
66
|
+
when 500..599 then ServerError
|
|
67
|
+
else ApiError
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fal
|
|
4
|
+
module Endpoints
|
|
5
|
+
# Endpoint for synchronous run: POST https://fal.run/{app_id}
|
|
6
|
+
class Run
|
|
7
|
+
def initialize(app_id:, base_url:)
|
|
8
|
+
@app_id = app_id
|
|
9
|
+
@base_url = base_url
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def url
|
|
13
|
+
"#{@base_url}/#{@app_id}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def method
|
|
17
|
+
:post
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Endpoint for queue submit: POST https://queue.fal.run/{app_id}
|
|
22
|
+
class Submit
|
|
23
|
+
def initialize(app_id:, base_url:)
|
|
24
|
+
@app_id = app_id
|
|
25
|
+
@base_url = base_url
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def url
|
|
29
|
+
"#{@base_url}/#{@app_id}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def method
|
|
33
|
+
:post
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Endpoint for queue status: GET https://queue.fal.run/{app_id}/requests/{request_id}/status
|
|
38
|
+
class Status
|
|
39
|
+
def initialize(app_id:, request_id:, base_url:)
|
|
40
|
+
@app_id = app_id
|
|
41
|
+
@request_id = request_id
|
|
42
|
+
@base_url = base_url
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def url
|
|
46
|
+
"#{@base_url}/#{@app_id}/requests/#{@request_id}/status"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def method
|
|
50
|
+
:get
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Endpoint for queue result: GET https://queue.fal.run/{app_id}/requests/{request_id}
|
|
55
|
+
class Result
|
|
56
|
+
def initialize(app_id:, request_id:, base_url:)
|
|
57
|
+
@app_id = app_id
|
|
58
|
+
@request_id = request_id
|
|
59
|
+
@base_url = base_url
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def url
|
|
63
|
+
"#{@base_url}/#{@app_id}/requests/#{@request_id}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def method
|
|
67
|
+
:get
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/fal/errors.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fal
|
|
4
|
+
# Base error for all Fal errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when configuration is invalid or missing
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when HTTP connection fails
|
|
11
|
+
class ConnectionError < Error
|
|
12
|
+
attr_reader :original_error
|
|
13
|
+
|
|
14
|
+
def initialize(message, original_error: nil)
|
|
15
|
+
super(message)
|
|
16
|
+
@original_error = original_error
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Base class for API errors (non-2xx responses)
|
|
21
|
+
class ApiError < Error
|
|
22
|
+
attr_reader :status_code, :response_body
|
|
23
|
+
|
|
24
|
+
def initialize(message, status_code:, response_body: nil)
|
|
25
|
+
super(message)
|
|
26
|
+
@status_code = status_code
|
|
27
|
+
@response_body = response_body
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 401 Unauthorized
|
|
32
|
+
class AuthenticationError < ApiError; end
|
|
33
|
+
|
|
34
|
+
# 404 Not Found
|
|
35
|
+
class NotFoundError < ApiError; end
|
|
36
|
+
|
|
37
|
+
# 429 Too Many Requests
|
|
38
|
+
class RateLimitError < ApiError; end
|
|
39
|
+
|
|
40
|
+
# 5xx Server Errors
|
|
41
|
+
class ServerError < ApiError; end
|
|
42
|
+
|
|
43
|
+
# Raised when polling times out
|
|
44
|
+
class TimeoutError < Error; end
|
|
45
|
+
end
|
data/lib/fal/queue.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fal
|
|
4
|
+
# Queue operations: submit, status, result.
|
|
5
|
+
class Queue
|
|
6
|
+
def initialize(connection:, config:)
|
|
7
|
+
@connection = connection
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def submit(app_id, input)
|
|
12
|
+
endpoint = Endpoints::Submit.new(app_id: app_id, base_url: @config.queue_url)
|
|
13
|
+
response = @connection.post(endpoint, body: input)
|
|
14
|
+
response.request_id
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def status(app_id, request_id)
|
|
18
|
+
endpoint = Endpoints::Status.new(
|
|
19
|
+
app_id: app_id,
|
|
20
|
+
request_id: request_id,
|
|
21
|
+
base_url: @config.queue_url
|
|
22
|
+
)
|
|
23
|
+
response = @connection.get(endpoint)
|
|
24
|
+
response.to_status
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def result(app_id, request_id)
|
|
28
|
+
endpoint = Endpoints::Result.new(
|
|
29
|
+
app_id: app_id,
|
|
30
|
+
request_id: request_id,
|
|
31
|
+
base_url: @config.queue_url
|
|
32
|
+
)
|
|
33
|
+
response = @connection.get(endpoint)
|
|
34
|
+
response.data
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/fal/request.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Fal
|
|
6
|
+
# Builds HTTP request components (headers, body).
|
|
7
|
+
class Request
|
|
8
|
+
CONTENT_TYPE = "application/json"
|
|
9
|
+
USER_AGENT = "fal-ruby/#{VERSION}"
|
|
10
|
+
|
|
11
|
+
def initialize(config:)
|
|
12
|
+
@config = config
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def headers
|
|
16
|
+
{
|
|
17
|
+
"Authorization" => "Key #{@config.api_key}",
|
|
18
|
+
"Content-Type" => CONTENT_TYPE,
|
|
19
|
+
"User-Agent" => USER_AGENT
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def body(input)
|
|
24
|
+
JSON.generate(input)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/fal/response.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Fal
|
|
6
|
+
# Parses HTTP responses and creates appropriate objects.
|
|
7
|
+
class Response
|
|
8
|
+
def initialize(http_response)
|
|
9
|
+
@http_response = http_response
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def status_code
|
|
13
|
+
@http_response.status.to_i
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def success?
|
|
17
|
+
status_code >= 200 && status_code < 300
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def data
|
|
21
|
+
@data ||= parse_body
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def request_id
|
|
25
|
+
data["request_id"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def error_message
|
|
29
|
+
data["detail"] || data["message"] || "Unknown error"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_status
|
|
33
|
+
status_class.new(data)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def parse_body
|
|
39
|
+
JSON.parse(@http_response.body.to_s)
|
|
40
|
+
rescue JSON::ParserError
|
|
41
|
+
{ "raw" => @http_response.body.to_s }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def status_class
|
|
45
|
+
case data["status"]
|
|
46
|
+
when "IN_QUEUE" then Status::Queued
|
|
47
|
+
when "IN_PROGRESS" then Status::InProgress
|
|
48
|
+
when "COMPLETED" then Status::Completed
|
|
49
|
+
else Status::Base
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/fal/status.rb
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fal
|
|
4
|
+
module Status
|
|
5
|
+
# Base class for all status types
|
|
6
|
+
class Base
|
|
7
|
+
attr_reader :raw_data
|
|
8
|
+
|
|
9
|
+
def initialize(raw_data)
|
|
10
|
+
@raw_data = raw_data
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def queued?
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def in_progress?
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def completed?
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Request is queued, waiting to be processed
|
|
27
|
+
class Queued < Base
|
|
28
|
+
def position
|
|
29
|
+
raw_data["queue_position"] || raw_data["position"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def queued?
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Request is currently being processed
|
|
38
|
+
class InProgress < Base
|
|
39
|
+
def logs
|
|
40
|
+
raw_data["logs"] || []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def in_progress?
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Request has completed successfully
|
|
49
|
+
class Completed < Base
|
|
50
|
+
def logs
|
|
51
|
+
raw_data["logs"] || []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def metrics
|
|
55
|
+
raw_data["metrics"] || {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def completed?
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fal
|
|
4
|
+
# Polls queue until completion, yielding status updates.
|
|
5
|
+
class Subscriber
|
|
6
|
+
def initialize(queue:, poll_interval:, timeout:)
|
|
7
|
+
@queue = queue
|
|
8
|
+
@poll_interval = poll_interval
|
|
9
|
+
@timeout = timeout
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def wait_for_completion(app_id, request_id, &on_update)
|
|
13
|
+
deadline = Time.now + @timeout
|
|
14
|
+
|
|
15
|
+
loop do
|
|
16
|
+
check_timeout(deadline)
|
|
17
|
+
status = @queue.status(app_id, request_id)
|
|
18
|
+
yield_status(status, &on_update)
|
|
19
|
+
return fetch_result(app_id, request_id) if status.completed?
|
|
20
|
+
|
|
21
|
+
sleep(@poll_interval)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def check_timeout(deadline)
|
|
28
|
+
return if Time.now < deadline
|
|
29
|
+
|
|
30
|
+
raise TimeoutError, "Polling timed out after #{@timeout} seconds"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def yield_status(status, &on_update)
|
|
34
|
+
on_update&.call(status)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fetch_result(app_id, request_id)
|
|
38
|
+
@queue.result(app_id, request_id)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/fal/version.rb
ADDED
data/lib/fal-ai.rb
ADDED
data/lib/fal.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "fal/version"
|
|
4
|
+
require_relative "fal/errors"
|
|
5
|
+
require_relative "fal/configuration"
|
|
6
|
+
require_relative "fal/endpoints"
|
|
7
|
+
require_relative "fal/status"
|
|
8
|
+
require_relative "fal/request"
|
|
9
|
+
require_relative "fal/response"
|
|
10
|
+
require_relative "fal/connection"
|
|
11
|
+
require_relative "fal/queue"
|
|
12
|
+
require_relative "fal/subscriber"
|
|
13
|
+
require_relative "fal/client"
|
|
14
|
+
|
|
15
|
+
# Ruby client for fal.ai Model APIs
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# Fal.configure do |config|
|
|
19
|
+
# config.api_key = "your-api-key"
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# client = Fal.client
|
|
23
|
+
# result = client.run("fal-ai/flux/dev", { prompt: "a cat" })
|
|
24
|
+
module Fal
|
|
25
|
+
class << self
|
|
26
|
+
def configure
|
|
27
|
+
yield(configuration)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configuration
|
|
31
|
+
@configuration ||= Configuration.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def client(config: configuration)
|
|
35
|
+
Client.new(config: config)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def reset_configuration!
|
|
39
|
+
@configuration = nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fal-ai
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matt Culpepper
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: http
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.0'
|
|
26
|
+
description: A Ruby client library for fal.ai's generative AI platform. Run inference
|
|
27
|
+
on 600+ AI models including Flux, Stable Diffusion, and more with synchronous and
|
|
28
|
+
queue-based APIs.
|
|
29
|
+
email:
|
|
30
|
+
- matt@culpepper.co
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- ".rspec"
|
|
36
|
+
- ".rubocop.yml"
|
|
37
|
+
- ".ruby-version"
|
|
38
|
+
- CHANGELOG.md
|
|
39
|
+
- LICENSE.txt
|
|
40
|
+
- README.md
|
|
41
|
+
- Rakefile
|
|
42
|
+
- lib/fal-ai.rb
|
|
43
|
+
- lib/fal.rb
|
|
44
|
+
- lib/fal/client.rb
|
|
45
|
+
- lib/fal/configuration.rb
|
|
46
|
+
- lib/fal/connection.rb
|
|
47
|
+
- lib/fal/endpoints.rb
|
|
48
|
+
- lib/fal/errors.rb
|
|
49
|
+
- lib/fal/queue.rb
|
|
50
|
+
- lib/fal/request.rb
|
|
51
|
+
- lib/fal/response.rb
|
|
52
|
+
- lib/fal/status.rb
|
|
53
|
+
- lib/fal/subscriber.rb
|
|
54
|
+
- lib/fal/version.rb
|
|
55
|
+
homepage: https://github.com/mculp/fal-ai-ruby
|
|
56
|
+
licenses:
|
|
57
|
+
- MIT
|
|
58
|
+
metadata:
|
|
59
|
+
homepage_uri: https://fal.ai
|
|
60
|
+
source_code_uri: https://github.com/mculp/fal-ai-ruby
|
|
61
|
+
changelog_uri: https://github.com/mculp/fal-ai-ruby/blob/main/CHANGELOG.md
|
|
62
|
+
documentation_uri: https://rubydoc.info/gems/fal-ai
|
|
63
|
+
bug_tracker_uri: https://github.com/mculp/fal-ai-ruby/issues
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 3.0.0
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 4.0.0
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Ruby client for fal.ai generative AI platform
|
|
82
|
+
test_files: []
|