runway-ruby 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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.tool-versions +1 -0
  3. data/LICENSE.txt +21 -0
  4. data/OPENAPI_VALIDATION.md +100 -0
  5. data/README.md +789 -0
  6. data/Rakefile +11 -0
  7. data/lib/runway_ml/character_performance.rb +106 -0
  8. data/lib/runway_ml/client.rb +84 -0
  9. data/lib/runway_ml/contract_validator.rb +151 -0
  10. data/lib/runway_ml/errors.rb +179 -0
  11. data/lib/runway_ml/image_processor.rb +128 -0
  12. data/lib/runway_ml/image_to_video.rb +158 -0
  13. data/lib/runway_ml/media_processor.rb +343 -0
  14. data/lib/runway_ml/openapi_spec_loader.rb +120 -0
  15. data/lib/runway_ml/organization.rb +89 -0
  16. data/lib/runway_ml/sound_effect.rb +57 -0
  17. data/lib/runway_ml/spec_coverage_analyzer.rb +134 -0
  18. data/lib/runway_ml/speech_to_speech.rb +90 -0
  19. data/lib/runway_ml/task.rb +116 -0
  20. data/lib/runway_ml/test_client.rb +62 -0
  21. data/lib/runway_ml/text_to_speech.rb +52 -0
  22. data/lib/runway_ml/text_to_video.rb +103 -0
  23. data/lib/runway_ml/uploads.rb +74 -0
  24. data/lib/runway_ml/validator_builder.rb +95 -0
  25. data/lib/runway_ml/validators/base_validators.rb +338 -0
  26. data/lib/runway_ml/validators/character_performance_validator.rb +41 -0
  27. data/lib/runway_ml/validators/gen3a_turbo_validator.rb +48 -0
  28. data/lib/runway_ml/validators/gen4_turbo_validator.rb +50 -0
  29. data/lib/runway_ml/validators/prompt_image_validator.rb +97 -0
  30. data/lib/runway_ml/validators/sound_effect_validator.rb +27 -0
  31. data/lib/runway_ml/validators/speech_to_speech_validator.rb +57 -0
  32. data/lib/runway_ml/validators/text_to_speech_validator.rb +28 -0
  33. data/lib/runway_ml/validators/text_veo3_stable_validator.rb +35 -0
  34. data/lib/runway_ml/validators/text_veo3_validator.rb +33 -0
  35. data/lib/runway_ml/validators/uploads_validator.rb +44 -0
  36. data/lib/runway_ml/validators/veo3_stable_validator.rb +43 -0
  37. data/lib/runway_ml/validators/veo3_validator.rb +46 -0
  38. data/lib/runway_ml/validators/voice_dubbing_validator.rb +128 -0
  39. data/lib/runway_ml/validators/voice_isolation_validator.rb +35 -0
  40. data/lib/runway_ml/validators/voice_validator.rb +32 -0
  41. data/lib/runway_ml/version.rb +5 -0
  42. data/lib/runway_ml/voice_dubbing.rb +74 -0
  43. data/lib/runway_ml/voice_isolation.rb +57 -0
  44. data/lib/runway_ml.rb +81 -0
  45. data/lib/tasks/openapi.rake +33 -0
  46. metadata +90 -0
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "image_processor"
5
+ require_relative "media_processor"
6
+ require_relative "validators/gen4_turbo_validator"
7
+ require_relative "validators/veo3_validator"
8
+ require_relative "validators/veo3_stable_validator"
9
+ require_relative "validators/gen3a_turbo_validator"
10
+
11
+ module RunwayML
12
+ class ImageToVideo
13
+ VALID_MODELS = [ "gen4_turbo", "veo3.1", "gen3a_turbo", "veo3.1_fast", "veo3" ].freeze
14
+
15
+ def initialize(client:)
16
+ @client = client
17
+ end
18
+
19
+ def create(model:, prompt_image:, prompt_text:, ratio:, duration:, seed: nil, public_figure_threshold: nil, audio: nil, auto_upload: true)
20
+ errors = {}
21
+
22
+ # Validate model
23
+ unless VALID_MODELS.include?(model)
24
+ errors[:model] = "must be one of: #{VALID_MODELS.join(', ')}"
25
+ raise ValidationError, errors
26
+ end
27
+
28
+ # Process prompt_image with auto-upload support
29
+ processed_prompt_image = MediaProcessor.process(
30
+ prompt_image,
31
+ errors,
32
+ client: client,
33
+ auto_upload: auto_upload
34
+ )
35
+
36
+ # Process audio with auto-upload support if provided
37
+ processed_audio = if audio && audio != false
38
+ MediaProcessor.process(
39
+ audio,
40
+ errors,
41
+ client: client,
42
+ auto_upload: auto_upload
43
+ )
44
+ end
45
+
46
+ # Delegate to model-specific validator
47
+ validator = get_validator(model)
48
+ result = validator.validate(
49
+ **build_validator_params(model, processed_prompt_image, prompt_text, ratio, duration, seed, public_figure_threshold, processed_audio || audio)
50
+ )
51
+
52
+ errors = result[:errors]
53
+ processed_prompt_image = result[:processed_prompt_image]
54
+
55
+ raise ValidationError, errors unless errors.empty?
56
+
57
+ inputs = build_inputs(model, processed_prompt_image, prompt_text, ratio, duration, seed, public_figure_threshold, processed_audio || audio)
58
+
59
+ response = client.post(
60
+ "image_to_video",
61
+ inputs.merge(model: model)
62
+ )
63
+
64
+ Task.new(id: response["id"], client: client)
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :client
70
+
71
+ def get_validator(model)
72
+ case model
73
+ when "gen4_turbo"
74
+ Validators::Gen4TurboValidator.new(image_processor: ImageProcessor.new)
75
+ when "veo3.1", "veo3.1_fast"
76
+ Validators::Veo3Validator.new(image_processor: ImageProcessor.new)
77
+ when "veo3"
78
+ Validators::Veo3StableValidator.new(image_processor: ImageProcessor.new)
79
+ when "gen3a_turbo"
80
+ Validators::Gen3aTurboValidator.new(image_processor: ImageProcessor.new)
81
+ else
82
+ raise ArgumentError, "No validator configured for model: #{model}"
83
+ end
84
+ end
85
+
86
+ def build_validator_params(model, prompt_image, prompt_text, ratio, duration, seed, public_figure_threshold, audio)
87
+ case model
88
+ when "gen4_turbo"
89
+ {
90
+ prompt_image: prompt_image,
91
+ prompt_text: prompt_text,
92
+ ratio: ratio,
93
+ duration: duration,
94
+ seed: seed,
95
+ public_figure_threshold: public_figure_threshold
96
+ }
97
+ when "veo3.1", "veo3.1_fast"
98
+ {
99
+ prompt_image: prompt_image,
100
+ prompt_text: prompt_text,
101
+ ratio: ratio,
102
+ duration: duration,
103
+ audio: audio
104
+ }
105
+ when "veo3"
106
+ {
107
+ prompt_image: prompt_image,
108
+ prompt_text: prompt_text,
109
+ ratio: ratio,
110
+ duration: duration
111
+ }
112
+ when "gen3a_turbo"
113
+ {
114
+ prompt_image: prompt_image,
115
+ prompt_text: prompt_text,
116
+ ratio: ratio,
117
+ duration: duration,
118
+ seed: seed,
119
+ public_figure_threshold: public_figure_threshold
120
+ }
121
+ else
122
+ {}
123
+ end
124
+ end
125
+
126
+ def build_inputs(model, processed_prompt_image, prompt_text, ratio, duration, seed, public_figure_threshold, audio)
127
+ inputs = {
128
+ promptImage: processed_prompt_image,
129
+ promptText: prompt_text,
130
+ ratio: ratio,
131
+ duration: duration
132
+ }
133
+
134
+ case model
135
+ when "gen4_turbo"
136
+ inputs[:seed] = seed if seed
137
+ if public_figure_threshold
138
+ inputs[:contentModeration] = {
139
+ publicFigureThreshold: public_figure_threshold
140
+ }
141
+ end
142
+ when "veo3.1", "veo3.1_fast"
143
+ inputs[:audio] = audio unless audio.nil?
144
+ when "veo3"
145
+ # No additional parameters for veo3
146
+ when "gen3a_turbo"
147
+ inputs[:seed] = seed if seed
148
+ if public_figure_threshold
149
+ inputs[:contentModeration] = {
150
+ publicFigureThreshold: public_figure_threshold
151
+ }
152
+ end
153
+ end
154
+
155
+ inputs
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,343 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "stringio"
5
+
6
+ module RunwayML
7
+ class MediaProcessor
8
+ # Processes media files, optionally uploading them to Runway
9
+ # If auto_upload is true (default), local files/StringIO are uploaded and converted to runway URIs
10
+ # If auto_upload is false, converts them to data URIs instead
11
+ def self.process(media, errors, client: nil, auto_upload: true)
12
+ new(client: client).process(media, errors, auto_upload: auto_upload)
13
+ end
14
+
15
+ def initialize(client: nil)
16
+ @client = client
17
+ end
18
+
19
+ def process(media, errors, auto_upload: true)
20
+ return media if media.nil?
21
+
22
+ if auto_upload && @client
23
+ process_with_upload(media, errors)
24
+ else
25
+ process_without_upload(media, errors)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :client
32
+
33
+ # ======================
34
+ # WITH UPLOAD PROCESSING
35
+ # ======================
36
+
37
+ def process_with_upload(media, errors)
38
+ case media
39
+ when Array
40
+ process_array_with_upload(media, errors)
41
+ when Hash
42
+ process_hash_with_upload(media, errors)
43
+ when String, File, StringIO
44
+ convert_to_runway_uri(media, errors)
45
+ else
46
+ media
47
+ end
48
+ end
49
+
50
+ def process_array_with_upload(items, errors)
51
+ items.map do |item|
52
+ case item
53
+ when Hash
54
+ process_hash_with_upload(item, errors)
55
+ when String, File, StringIO
56
+ convert_to_runway_uri(item, errors) || item
57
+ else
58
+ item
59
+ end
60
+ end
61
+ end
62
+
63
+ def process_hash_with_upload(item, errors)
64
+ return item unless item.is_a?(Hash)
65
+
66
+ item = item.dup
67
+ uri = item[:uri] || item["uri"]
68
+
69
+ return item unless uri.is_a?(String) || uri.respond_to?(:read) || (uri.is_a?(String) && looks_like_file_path?(uri))
70
+
71
+ converted_uri = convert_to_runway_uri(uri, errors)
72
+ return item unless converted_uri
73
+
74
+ if item.key?(:uri)
75
+ item[:uri] = converted_uri
76
+ else
77
+ item["uri"] = converted_uri
78
+ end
79
+
80
+ item
81
+ end
82
+
83
+ def convert_to_runway_uri(media, errors)
84
+ return media if media.nil?
85
+
86
+ # If it's already a URI, return as-is
87
+ if media.is_a?(String) && (media.start_with?("https://", "http://", "runway://", "data:"))
88
+ return media
89
+ end
90
+
91
+ # Check if it's a file path
92
+ if media.is_a?(String) && looks_like_file_path?(media)
93
+ upload_file(media, errors)
94
+ # Check if it's a file-like object
95
+ elsif media.respond_to?(:read)
96
+ upload_io(media, errors)
97
+ else
98
+ media
99
+ end
100
+ end
101
+
102
+ def upload_file(path, errors)
103
+ return nil if client.nil?
104
+
105
+ begin
106
+ uploads = Uploads.new(client: client)
107
+ uploads.create_ephemeral(path)
108
+ rescue ValidationError => e
109
+ errors[:media] = "Failed to upload file: #{e.message}"
110
+ nil
111
+ rescue => e
112
+ errors[:media] = "Error uploading file: #{e.message}"
113
+ nil
114
+ end
115
+ end
116
+
117
+ def upload_io(io, errors)
118
+ return nil if client.nil?
119
+
120
+ begin
121
+ # Determine a good filename
122
+ filename = if io.respond_to?(:path)
123
+ File.basename(io.path)
124
+ else
125
+ # For StringIO without a path, infer the extension from content type
126
+ content_type = infer_content_type_for_io(io)
127
+ ext = infer_extension_for_content_type(content_type)
128
+ "upload#{ext}"
129
+ end
130
+
131
+ uploads = Uploads.new(client: client)
132
+ uploads.create_ephemeral(io, filename: filename)
133
+ rescue ValidationError => e
134
+ errors[:media] = "Failed to upload file: #{e.message}"
135
+ nil
136
+ rescue => e
137
+ errors[:media] = "Error uploading file: #{e.message}"
138
+ nil
139
+ end
140
+ end
141
+
142
+ def infer_content_type_for_io(io)
143
+ io.rewind if io.respond_to?(:rewind)
144
+ content = io.read
145
+ io.rewind if io.respond_to?(:rewind)
146
+
147
+ detect_from_magic_bytes(content) || "application/octet-stream"
148
+ end
149
+
150
+ def infer_extension_for_content_type(content_type)
151
+ {
152
+ "image/jpeg" => ".jpg",
153
+ "image/png" => ".png",
154
+ "image/webp" => ".webp",
155
+ "audio/mpeg" => ".mp3",
156
+ "audio/wav" => ".wav",
157
+ "audio/flac" => ".flac",
158
+ "audio/mp4" => ".m4a",
159
+ "audio/aac" => ".aac",
160
+ "audio/ogg" => ".ogg",
161
+ "audio/webp" => ".weba",
162
+ "video/mp4" => ".mp4",
163
+ "video/quicktime" => ".mov",
164
+ "video/x-matroska" => ".mkv",
165
+ "video/webm" => ".webm",
166
+ "video/3gpp" => ".3gp",
167
+ "video/ogg" => ".ogv",
168
+ "video/x-msvideo" => ".avi",
169
+ "video/x-flv" => ".flv",
170
+ "video/mpeg" => ".mpg"
171
+ }[content_type] || ".mp3" # Default to mp3 for unknown audio
172
+ end
173
+
174
+ # ========================
175
+ # WITHOUT UPLOAD PROCESSING
176
+ # ========================
177
+
178
+ def process_without_upload(media, errors)
179
+ case media
180
+ when Array
181
+ process_array_without_upload(media, errors)
182
+ when Hash
183
+ process_hash_without_upload(media, errors)
184
+ when String, File, StringIO
185
+ convert_to_data_uri(media, errors)
186
+ else
187
+ media
188
+ end
189
+ end
190
+
191
+ def process_array_without_upload(items, errors)
192
+ items.map do |item|
193
+ case item
194
+ when Hash
195
+ process_hash_without_upload(item, errors)
196
+ when String, File, StringIO
197
+ convert_to_data_uri(item, errors) || item
198
+ else
199
+ item
200
+ end
201
+ end
202
+ end
203
+
204
+ def process_hash_without_upload(item, errors)
205
+ return item unless item.is_a?(Hash)
206
+
207
+ item = item.dup
208
+ uri = item[:uri] || item["uri"]
209
+
210
+ return item unless uri.is_a?(String) || uri.respond_to?(:read) || (uri.is_a?(String) && looks_like_file_path?(uri))
211
+
212
+ converted_uri = convert_to_data_uri(uri, errors)
213
+ return item unless converted_uri
214
+
215
+ if item.key?(:uri)
216
+ item[:uri] = converted_uri
217
+ else
218
+ item["uri"] = converted_uri
219
+ end
220
+
221
+ item
222
+ end
223
+
224
+ def convert_to_data_uri(media, errors)
225
+ # If it's already a URI, return as-is
226
+ if media.is_a?(String) && (media.start_with?("https://", "http://", "runway://", "data:"))
227
+ return media
228
+ end
229
+
230
+ # Check if it's a file path
231
+ if media.is_a?(String) && looks_like_file_path?(media)
232
+ load_file_to_data_uri(media, errors)
233
+ # Check if it's a file-like object
234
+ elsif media.respond_to?(:read)
235
+ convert_io_to_data_uri(media, errors)
236
+ else
237
+ media
238
+ end
239
+ end
240
+
241
+ def load_file_to_data_uri(path, errors)
242
+ file = File.open(path, "rb")
243
+ result = convert_io_to_data_uri(file, errors)
244
+ file.close
245
+ result
246
+ rescue Errno::ENOENT
247
+ errors[:media] = "file not found: #{path}"
248
+ nil
249
+ rescue => e
250
+ errors[:media] = "error reading file: #{e.message}"
251
+ nil
252
+ end
253
+
254
+ def convert_io_to_data_uri(io, errors)
255
+ io.rewind if io.respond_to?(:rewind)
256
+ content = io.read
257
+ io.rewind if io.respond_to?(:rewind)
258
+
259
+ content_type = detect_content_type(io, content)
260
+ unless content_type
261
+ errors[:media] = "unable to detect media type"
262
+ return nil
263
+ end
264
+
265
+ encoded = Base64.strict_encode64(content)
266
+ "data:#{content_type};base64,#{encoded}"
267
+ end
268
+
269
+ # =======================
270
+ # CONTENT TYPE DETECTION
271
+ # =======================
272
+
273
+ def detect_content_type(io, content)
274
+ # Try to detect from file path if it's a File object
275
+ if io.is_a?(File) && io.respond_to?(:path)
276
+ ext = File.extname(io.path).downcase
277
+ mime_type = mime_type_for_extension(ext)
278
+ return mime_type if mime_type
279
+ end
280
+
281
+ # Detect from magic bytes
282
+ detect_from_magic_bytes(content)
283
+ end
284
+
285
+ def detect_from_magic_bytes(content)
286
+ return nil if content.nil? || content.empty?
287
+
288
+ if content.start_with?("\xFF\xD8\xFF".b)
289
+ "image/jpeg"
290
+ elsif content.start_with?("\x89PNG\r\n\x1A\n".b)
291
+ "image/png"
292
+ elsif content.start_with?("RIFF".b) && content[8..11] == "WEBP".b
293
+ "image/webp"
294
+ elsif content.start_with?("\xFF\xFB".b) || content.start_with?("\xFF\xFA".b)
295
+ "audio/mpeg"
296
+ elsif content.start_with?("RIFF".b) && content[8..11] == "WAVE".b
297
+ "audio/wav"
298
+ elsif content.start_with?("ftyp".b)
299
+ "video/mp4"
300
+ end
301
+ end
302
+
303
+ def mime_type_for_extension(ext)
304
+ {
305
+ ".jpg" => "image/jpeg",
306
+ ".jpeg" => "image/jpeg",
307
+ ".png" => "image/png",
308
+ ".webp" => "image/webp",
309
+ ".mp3" => "audio/mpeg",
310
+ ".wav" => "audio/wav",
311
+ ".flac" => "audio/flac",
312
+ ".m4a" => "audio/mp4",
313
+ ".aac" => "audio/aac",
314
+ ".ogg" => "audio/ogg",
315
+ ".weba" => "audio/webp",
316
+ ".mp4" => "video/mp4",
317
+ ".mov" => "video/quicktime",
318
+ ".mkv" => "video/x-matroska",
319
+ ".webm" => "video/webm",
320
+ ".3gp" => "video/3gpp",
321
+ ".ogv" => "video/ogg",
322
+ ".avi" => "video/x-msvideo",
323
+ ".flv" => "video/x-flv",
324
+ ".mpg" => "video/mpeg",
325
+ ".mpeg" => "video/mpeg"
326
+ }[ext]
327
+ end
328
+
329
+ # ============
330
+ # HELPERS
331
+ # ============
332
+
333
+ def looks_like_file_path?(string)
334
+ return false unless string.is_a?(String)
335
+
336
+ # If it starts with a known URI scheme, it's not a file path
337
+ return false if string.start_with?("https://", "http://", "runway://", "data:")
338
+
339
+ # Check if it's a file that exists
340
+ File.exist?(string)
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+ require "net/http"
6
+ require "tempfile"
7
+ require "fileutils"
8
+
9
+ module RunwayML
10
+ # Loads and caches the OpenAPI spec from the RunwayML GitHub repository
11
+ class OpenAPISpecLoader
12
+ SPEC_URL = "https://raw.githubusercontent.com/runwayml/openapi/main/openapi.json"
13
+ SPEC_HASH = "9cf1febc614000f72d4332f8bee51b6873b6bce49c9bdc02daae300e6882f9dd"
14
+ CACHE_DIR = File.join(Dir.home, ".cache", "runway-ml-openapi")
15
+
16
+ class << self
17
+ attr_accessor :spec
18
+
19
+ def load_spec(force_reload: false)
20
+ return @spec if @spec && !force_reload
21
+
22
+ @spec = fetch_or_cache_spec(force_reload)
23
+ @spec
24
+ end
25
+
26
+ def get_endpoint_schema(method, path)
27
+ load_spec
28
+ path_item = @spec.dig("paths", path)
29
+ return nil unless path_item
30
+
31
+ operation = path_item[method.downcase]
32
+ return nil unless operation
33
+
34
+ operation
35
+ end
36
+
37
+ def get_request_schema(method, path)
38
+ operation = get_endpoint_schema(method, path)
39
+ return nil unless operation
40
+
41
+ request_body = operation.dig("requestBody", "content", "application/json", "schema")
42
+ request_body
43
+ end
44
+
45
+ def get_response_schema(method, path, status_code = "200")
46
+ operation = get_endpoint_schema(method, path)
47
+ return nil unless operation
48
+
49
+ response_schema = operation.dig("responses", status_code, "content", "application/json", "schema")
50
+ response_schema
51
+ end
52
+
53
+ def get_path_parameters(method, path)
54
+ operation = get_endpoint_schema(method, path)
55
+ return [] unless operation
56
+
57
+ parameters = operation["parameters"] || []
58
+ parameters.select { |p| p["in"] == "path" }
59
+ end
60
+
61
+ def get_query_parameters(method, path)
62
+ operation = get_endpoint_schema(method, path)
63
+ return [] unless operation
64
+
65
+ parameters = operation["parameters"] || []
66
+ parameters.select { |p| p["in"] == "query" }
67
+ end
68
+
69
+ private
70
+
71
+ def fetch_or_cache_spec(force_reload)
72
+ if force_reload
73
+ fetch_spec
74
+ else
75
+ cached = load_from_cache
76
+ cached || fetch_and_cache_spec
77
+ end
78
+ end
79
+
80
+ def load_from_cache
81
+ cache_file = cache_path
82
+ return nil unless File.exist?(cache_file)
83
+
84
+ # Check if cache is less than 24 hours old
85
+ if File.mtime(cache_file) > Time.now - (24 * 3600)
86
+ JSON.parse(File.read(cache_file))
87
+ else
88
+ nil
89
+ end
90
+ rescue StandardError
91
+ nil
92
+ end
93
+
94
+ def fetch_and_cache_spec
95
+ spec = fetch_spec
96
+ save_to_cache(spec)
97
+ spec
98
+ end
99
+
100
+ def fetch_spec
101
+ uri = URI(SPEC_URL)
102
+ response = Net::HTTP.get(uri)
103
+ JSON.parse(response)
104
+ rescue StandardError => e
105
+ raise "Failed to fetch OpenAPI spec: #{e.message}"
106
+ end
107
+
108
+ def save_to_cache(spec)
109
+ FileUtils.mkdir_p(CACHE_DIR)
110
+ File.write(cache_path, JSON.pretty_generate(spec))
111
+ rescue StandardError
112
+ # Silently fail cache writes
113
+ end
114
+
115
+ def cache_path
116
+ File.join(CACHE_DIR, "openapi.json")
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module RunwayML
6
+ class Organization
7
+ def initialize(client:)
8
+ @client = client
9
+ end
10
+
11
+ def retrieve
12
+ response = client.get("organization")
13
+ OrganizationInfo.new(response)
14
+ end
15
+
16
+ def usage(start_date: nil, before_date: nil)
17
+ params = {}
18
+ params[:startDate] = start_date if start_date
19
+ params[:beforeDate] = before_date if before_date
20
+
21
+ response = if params.empty?
22
+ client.post("organization/usage", {})
23
+ else
24
+ client.post("organization/usage", params)
25
+ end
26
+
27
+ UsageInfo.new(response)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :client
33
+ end
34
+
35
+ class OrganizationInfo
36
+ attr_reader :tier, :credit_balance, :usage
37
+
38
+ def initialize(data)
39
+ @data = data
40
+ @tier = data["tier"] || {}
41
+ @credit_balance = data["creditBalance"] || 0
42
+ @usage = data["usage"] || {}
43
+ end
44
+
45
+ def max_monthly_credit_spend
46
+ tier["maxMonthlyCreditSpend"]
47
+ end
48
+
49
+ def tier_models
50
+ tier["models"] || {}
51
+ end
52
+
53
+ def usage_models
54
+ usage["models"] || {}
55
+ end
56
+
57
+ def to_h
58
+ @data
59
+ end
60
+
61
+ def inspect
62
+ "#<RunwayML::OrganizationInfo tier=#{tier.inspect} credit_balance=#{credit_balance}>"
63
+ end
64
+ end
65
+
66
+ class UsageInfo
67
+ attr_reader :data
68
+
69
+ def initialize(data)
70
+ @data = data
71
+ end
72
+
73
+ def models
74
+ data["models"] || {}
75
+ end
76
+
77
+ def by_model
78
+ models
79
+ end
80
+
81
+ def to_h
82
+ data
83
+ end
84
+
85
+ def inspect
86
+ "#<RunwayML::UsageInfo models=#{models.keys.join(', ')}>"
87
+ end
88
+ end
89
+ end