fal 0.0.3 → 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: f22dff48b2f5121df441220f3fffe41f3e548c8a2c229dd0f1c7d3c19072b74f
4
- data.tar.gz: 8e5c092fb5246f8ea7ee88b80123e63d5ed7384f37b1cfc61474bcd3daef1431
3
+ metadata.gz: 4ff4bd4724ee6b7ea39f7c72f51503e4e918b50f019972a08836efd6d22fdffc
4
+ data.tar.gz: 4f9020293f0b639487e836668116fadfcd51768ef0401e335a18394be155c930
5
5
  SHA512:
6
- metadata.gz: '05697e66efb4f7d26fb110d707627755395244fa5a9fca86d3624e4bd6d33cae7a42ccbe625cd14dd8646b4a934496e2f169247a387ad046cf049ece487eb0d4'
7
- data.tar.gz: b7bd9bf3bddb0f1a62ed61bb9d681a5531f4e00ff628cbc4a77b720e4367ef9a6d7a45253bb4777654e078c1e55e67ab6aaa95cebb2bf83ebc0e4a28df5b49c0
6
+ metadata.gz: d7646551a9cffa3a58b9ce0ac08f48e258cedd9e19f7ad18e9ae8fc3fe0aa85c774e5a21a283026ef5a22e0126c8c3db52d2667bc7cca285f8d9b6d74c4ac3b7
7
+ data.tar.gz: 0f1f6137e731796d859ff4bd51eb92078453530498dfd72d436e8c9999dd4f1f26087a99c010f533fb231bb2012186d10fee972811ff44d268e77a71d1180e3a
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
@@ -8,3 +8,7 @@ gem "minitest", "~> 5.0"
8
8
  gem "rake", "~> 13.0"
9
9
  gem "rubocop", "~> 1.21"
10
10
  gem "dotenv"
11
+ gem "sorbet"
12
+ gem "tapioca"
13
+ gem "vcr"
14
+ gem "webmock"
data/Gemfile.lock CHANGED
@@ -1,57 +1,115 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fal (0.0.3)
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
11
19
  dotenv (3.1.8)
20
+ erubi (1.13.1)
12
21
  faraday (2.14.0)
13
22
  faraday-net_http (>= 2.0, < 3.5)
14
23
  json
15
24
  logger
16
25
  faraday-net_http (3.4.1)
17
26
  net-http (>= 0.5.0)
18
- json (2.7.2)
19
- 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)
20
31
  logger (1.7.0)
21
- minitest (5.23.1)
22
- net-http (0.6.0)
32
+ minitest (5.26.0)
33
+ net-http (0.7.0)
23
34
  uri
24
- parallel (1.24.0)
25
- parser (3.3.1.0)
35
+ netrc (0.11.0)
36
+ parallel (1.27.0)
37
+ parser (3.3.10.0)
26
38
  ast (~> 2.4.1)
27
39
  racc
28
- racc (1.8.0)
40
+ prism (1.6.0)
41
+ public_suffix (6.0.2)
42
+ racc (1.8.1)
29
43
  rainbow (3.1.1)
30
- rake (13.2.1)
31
- regexp_parser (2.9.2)
32
- rexml (3.2.8)
33
- strscan (>= 3.0.9)
34
- 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)
35
53
  json (~> 2.3)
36
- language_server-protocol (>= 3.17.0)
54
+ language_server-protocol (~> 3.17.0.2)
55
+ lint_roller (~> 1.1.0)
37
56
  parallel (~> 1.10)
38
57
  parser (>= 3.3.0.2)
39
58
  rainbow (>= 2.2.2, < 4.0)
40
- regexp_parser (>= 1.8, < 3.0)
41
- rexml (>= 3.2.5, < 4.0)
42
- rubocop-ast (>= 1.31.1, < 2.0)
59
+ regexp_parser (>= 2.9.3, < 3.0)
60
+ rubocop-ast (>= 1.47.1, < 2.0)
43
61
  ruby-progressbar (~> 1.7)
44
- unicode-display_width (>= 2.4.0, < 3.0)
45
- rubocop-ast (1.31.3)
46
- 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)
47
66
  ruby-progressbar (1.13.0)
48
- strscan (3.1.0)
49
- unicode-display_width (2.5.0)
50
- 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
51
108
 
52
109
  PLATFORMS
53
- ruby
54
- x86_64-darwin-23
110
+ aarch64-linux
111
+ universal-darwin
112
+ x86_64-linux
55
113
 
56
114
  DEPENDENCIES
57
115
  dotenv
@@ -59,6 +117,10 @@ DEPENDENCIES
59
117
  minitest (~> 5.0)
60
118
  rake (~> 13.0)
61
119
  rubocop (~> 1.21)
120
+ sorbet
121
+ tapioca
122
+ vcr
123
+ webmock
62
124
 
63
125
  BUNDLED WITH
64
126
  2.5.7
data/README.md CHANGED
@@ -21,19 +21,20 @@ Fal.configure do |config|
21
21
  config.api_key = "your-key" # Optional. Defaults to ENV["FAL_KEY"] if not set.
22
22
  config.queue_base = "https://queue.fal.run" # Optional (default: https://queue.fal.run)
23
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)
24
25
  config.request_timeout = 120 # Optional (default: 120)
25
26
  end
26
27
  ```
27
28
 
28
29
  ### Create a queued request
29
30
 
30
- The Queue API is the recommended way to call models on fal. Provide a `model_id` in "namespace/name" format and an input payload.
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.
31
32
 
32
33
  ```ruby
33
- model_id = "fal-ai/fast-sdxl"
34
+ endpoint_id = "fal-ai/fast-sdxl"
34
35
 
35
36
  request = Fal::Request.create!(
36
- model_id: model_id,
37
+ endpoint_id: endpoint_id,
37
38
  input: { prompt: "a cat" }
38
39
  )
39
40
 
@@ -45,7 +46,7 @@ You can also specify a webhook URL to be notified when the request is finished.
45
46
 
46
47
  ```ruby
47
48
  request = Fal::Request.create!(
48
- model_id: model_id,
49
+ endpoint_id: endpoint_id,
49
50
  input: { prompt: "a cat playing piano" },
50
51
  webhook_url: "https://example.com/fal/webhook"
51
52
  )
@@ -56,7 +57,7 @@ request = Fal::Request.create!(
56
57
  Fetch the current status by id:
57
58
 
58
59
  ```ruby
59
- status = Fal::Request.find_by!(id: request.id, model_id: model_id)
60
+ status = Fal::Request.find_by!(id: request.id, endpoint_id: endpoint_id)
60
61
  status.in_queue? # => true/false
61
62
  status.in_progress? # => true/false
62
63
  status.completed? # => true/false
@@ -138,7 +139,7 @@ Rescue them as needed:
138
139
 
139
140
  ```ruby
140
141
  begin
141
- Fal::Request.create!(model_id: model_id, input: { prompt: "hi" })
142
+ Fal::Request.create!(endpoint_id: endpoint_id, input: { prompt: "hi" })
142
143
  rescue Fal::UnauthorizedError
143
144
  # handle invalid/missing FAL_KEY
144
145
  end
@@ -149,9 +150,9 @@ end
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.
150
151
 
151
152
  ```ruby
152
- model_id = "fal-ai/flux/dev"
153
+ endpoint_id = "fal-ai/flux/dev"
153
154
 
154
- last = Fal::Request.stream!(model_id: model_id, input: { prompt: "a cat" }) do |chunk|
155
+ last = Fal::Request.stream!(endpoint_id: endpoint_id, input: { prompt: "a cat" }) do |chunk|
155
156
  # chunk is a Hash, e.g. { images: [...] }
156
157
  puts chunk
157
158
  end
@@ -160,6 +161,112 @@ last.completed? # => true/false
160
161
  last.response # => last streamed data hash (e.g., { "response" => { ... } } or final payload)
161
162
  ```
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
+
163
270
  ### Development
164
271
 
165
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.
data/lib/fal/client.rb CHANGED
@@ -22,6 +22,25 @@ module Fal
22
22
  parse_json(response.body)
23
23
  end
24
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
+
25
44
  # Perform a POST to the streaming (sync) base with SSE/text-event-stream handling.
26
45
  # The provided on_data Proc will be used to receive chunked data.
27
46
  # @param path [String]
@@ -55,6 +74,26 @@ module Fal
55
74
  parse_json(response.body)
56
75
  end
57
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
+
58
97
  def put(path)
59
98
  response = connection.put(build_url(path)) do |request|
60
99
  request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
@@ -97,6 +136,10 @@ module Fal
97
136
  "#{@configuration.sync_base}#{path}"
98
137
  end
99
138
 
139
+ def build_api_url(path)
140
+ "#{@configuration.api_base}#{path}"
141
+ end
142
+
100
143
  def connection
101
144
  Faraday.new do |faraday|
102
145
  faraday.request :url_encoded
data/lib/fal/model.rb ADDED
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fal
4
+ # Represents a model endpoint discoverable via the Models API.
5
+ # Provides helpers to list, search, and fetch pricing and to run requests.
6
+ class Model
7
+ MODELS_PATH = "/models"
8
+
9
+ # @return [String]
10
+ attr_reader :endpoint_id
11
+ # Flattened metadata fields
12
+ # @return [String, nil]
13
+ attr_reader :display_name
14
+ # @return [String, nil]
15
+ attr_reader :category
16
+ # @return [String, nil]
17
+ attr_reader :description
18
+ # @return [String, nil]
19
+ attr_reader :status
20
+ # @return [Array<String>, nil]
21
+ attr_reader :tags
22
+ # @return [String, nil]
23
+ attr_reader :updated_at
24
+ # @return [Boolean, nil]
25
+ attr_reader :is_favorited
26
+ # @return [String, nil]
27
+ attr_reader :thumbnail_url
28
+ # @return [String, nil]
29
+ attr_reader :thumbnail_animated_url
30
+ # @return [String, nil]
31
+ attr_reader :model_url
32
+ # @return [String, nil]
33
+ attr_reader :github_url
34
+ # @return [String, nil]
35
+ attr_reader :license_type
36
+ # @return [String, nil]
37
+ attr_reader :date
38
+ # @return [Hash, nil]
39
+ attr_reader :group
40
+ # @return [Boolean, nil]
41
+ attr_reader :highlighted
42
+ # @return [String, nil]
43
+ attr_reader :kind
44
+ # @return [Array<String>, nil]
45
+ attr_reader :training_endpoint_ids
46
+ # @return [Array<String>, nil]
47
+ attr_reader :inference_endpoint_ids
48
+ # @return [String, nil]
49
+ attr_reader :stream_url
50
+ # @return [Float, nil]
51
+ attr_reader :duration_estimate
52
+ # @return [Boolean, nil]
53
+ attr_reader :pinned
54
+ # @return [Hash, nil]
55
+ attr_reader :openapi
56
+
57
+ # @param attributes [Hash]
58
+ # @param client [Fal::Client]
59
+ def initialize(attributes, client: Fal.client)
60
+ @client = client
61
+ reset_attributes(attributes)
62
+ end
63
+
64
+ # Fetch and memoize the price object for this model's endpoint.
65
+ # @return [Fal::Price, nil]
66
+ def price
67
+ @price ||= Fal::Price.find_by(endpoint_id: @endpoint_id, client: @client)
68
+ end
69
+
70
+ # Run a queued request for this model endpoint.
71
+ # @param input [Hash]
72
+ # @param webhook_url [String, nil]
73
+ # @return [Fal::Request]
74
+ def run(input:, webhook_url: nil)
75
+ Fal::Request.create!(endpoint_id: @endpoint_id, input: input, webhook_url: webhook_url, client: @client)
76
+ end
77
+
78
+ class << self
79
+ # Find a specific model by endpoint_id.
80
+ # @param endpoint_id [String]
81
+ # @param client [Fal::Client]
82
+ # @return [Fal::Model, nil]
83
+ def find_by(endpoint_id:, client: Fal.client)
84
+ response = client.get_api(MODELS_PATH, query: { endpoint_id: endpoint_id })
85
+ entry = Array(response && response["models"]).find { |m| m["endpoint_id"] == endpoint_id }
86
+ entry ? new(entry, client: client) : nil
87
+ end
88
+
89
+ # Iterate through models with optional search filters.
90
+ # @param client [Fal::Client]
91
+ # @param query [String, nil] Free-text search query
92
+ # @param category [String, nil]
93
+ # @param status [String, nil]
94
+ # @param expand [Array<String>, String, nil]
95
+ # @yield [Fal::Model]
96
+ # @return [void]
97
+ def each(client: Fal.client, query: nil, category: nil, status: nil, expand: nil, &block)
98
+ cursor = nil
99
+ loop do
100
+ query_hash = { limit: 50, cursor: cursor, q: query, category: category, status: status }.compact
101
+ query_hash[:expand] = expand if expand
102
+ response = client.get_api(MODELS_PATH, query: query_hash)
103
+ models = Array(response && response["models"])
104
+ models.each { |attributes| block.call(new(attributes, client: client)) }
105
+ cursor = response && response["next_cursor"]
106
+ break if cursor.nil?
107
+ end
108
+ end
109
+
110
+ # Return an array of models for the given filters (or all models).
111
+ # @param client [Fal::Client]
112
+ # @param query [String, nil]
113
+ # @param category [String, nil]
114
+ # @param status [String, nil]
115
+ # @param expand [Array<String>, String, nil]
116
+ # @return [Array<Fal::Model>]
117
+ def all(client: Fal.client, query: nil, category: nil, status: nil, expand: nil)
118
+ results = []
119
+ each(client: client, query: query, category: category, status: status, expand: expand) { |m| results << m }
120
+ results
121
+ end
122
+
123
+ # Convenience search wrapper that returns all matching models.
124
+ # @param client [Fal::Client]
125
+ # @param query [String, nil]
126
+ # @param category [String, nil]
127
+ # @param status [String, nil]
128
+ # @param expand [Array<String>, String, nil]
129
+ # @return [Array<Fal::Model>]
130
+ def search(query: nil, category: nil, status: nil, expand: nil, client: Fal.client)
131
+ all(client: client, query: query, category: category, status: status, expand: expand)
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ def reset_attributes(attributes)
138
+ @endpoint_id = attributes["endpoint_id"]
139
+
140
+ meta = attributes["metadata"] || {}
141
+ @display_name = meta["display_name"]
142
+ @category = meta["category"]
143
+ @description = meta["description"]
144
+ @status = meta["status"]
145
+ @tags = meta["tags"]
146
+ @updated_at = meta["updated_at"]
147
+ @is_favorited = meta["is_favorited"]
148
+ @thumbnail_url = meta["thumbnail_url"]
149
+ @thumbnail_animated_url = meta["thumbnail_animated_url"]
150
+ @model_url = meta["model_url"]
151
+ @github_url = meta["github_url"]
152
+ @license_type = meta["license_type"]
153
+ @date = meta["date"]
154
+ @group = meta["group"]
155
+ @highlighted = meta["highlighted"]
156
+ @kind = meta["kind"]
157
+ @training_endpoint_ids = meta["training_endpoint_ids"]
158
+ @inference_endpoint_ids = meta["inference_endpoint_ids"]
159
+ @stream_url = meta["stream_url"]
160
+ @duration_estimate = meta["duration_estimate"]
161
+ @pinned = meta["pinned"]
162
+
163
+ @openapi = attributes["openapi"]
164
+ end
165
+ end
166
+ end
data/lib/fal/price.rb ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fal
4
+ # Represents pricing information for a model endpoint.
5
+ # Fetches data from the Platform API at /models and /models/pricing.
6
+ class Price
7
+ MODELS_PATH = "/models"
8
+ PRICING_PATH = "/models/pricing"
9
+
10
+ # Billing units returned by the pricing service.
11
+ module Unit
12
+ # Output-based units
13
+ IMAGES = "image"
14
+ VIDEOS = "video"
15
+ MEGAPIXELS = "megapixels"
16
+
17
+ # Compute-based units (provider-specific)
18
+ GPU_SECONDS = "gpu_second"
19
+ GPU_MINUTES = "gpu_minute"
20
+ GPU_HOURS = "gpu_hour"
21
+ end
22
+
23
+ # @return [String]
24
+ attr_reader :endpoint_id
25
+ # @return [Float]
26
+ attr_reader :unit_price
27
+ # @return [String]
28
+ attr_reader :unit
29
+ # @return [String]
30
+ attr_reader :currency
31
+
32
+ # @param attributes [Hash] Raw attributes from pricing API
33
+ # @param client [Fal::Client]
34
+ def initialize(attributes, client: Fal.client)
35
+ @client = client
36
+ reset_attributes(attributes)
37
+ end
38
+
39
+ class << self
40
+ # Find pricing for a specific model endpoint.
41
+ # @param endpoint_id [String]
42
+ # @param client [Fal::Client]
43
+ # @return [Fal::Price, nil]
44
+ def find_by(endpoint_id:, client: Fal.client)
45
+ response = client.get_api(PRICING_PATH, query: { endpoint_id: endpoint_id })
46
+ entry = Array(response && response["prices"]).find { |p| p["endpoint_id"] == endpoint_id }
47
+ entry ? new(entry, client: client) : nil
48
+ end
49
+
50
+ # Iterate over all prices by paging through models and fetching pricing in batches.
51
+ # @param client [Fal::Client]
52
+ # @yield [Fal::Price]
53
+ # @return [void]
54
+ def each(client: Fal.client, &block)
55
+ cursor = nil
56
+ loop do
57
+ models_response = client.get_api(MODELS_PATH, query: { limit: 50, cursor: cursor }.compact)
58
+ models = Array(models_response && models_response["models"])
59
+ endpoint_ids = models.map { |m| m["endpoint_id"] }.compact
60
+
61
+ if endpoint_ids.any?
62
+ pricing_response = client.get_api(PRICING_PATH, query: { endpoint_id: endpoint_ids })
63
+ Array(pricing_response && pricing_response["prices"]).each do |attributes|
64
+ block.call(new(attributes, client: client))
65
+ end
66
+ end
67
+
68
+ cursor = models_response && models_response["next_cursor"]
69
+ break if cursor.nil?
70
+ end
71
+ end
72
+
73
+ # Return an array of all prices.
74
+ # @param client [Fal::Client]
75
+ # @return [Array<Fal::Price>]
76
+ def all(client: Fal.client)
77
+ results = []
78
+ each(client: client) { |price| results << price }
79
+ results
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def reset_attributes(attributes)
86
+ @endpoint_id = attributes["endpoint_id"]
87
+ @unit_price = attributes["unit_price"]
88
+ @unit = attributes["unit"]
89
+ @currency = attributes["currency"]
90
+ end
91
+ end
92
+ end