ruby_decision_model 0.2.0 → 0.3.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: f1c6972a68e2a0545795663a4c792603a692eb65b609d630ce2754e9f032d6b2
4
- data.tar.gz: 4252b768aa5f7ab6e16eee20ef09ca16923fdf2b192eb21a0fb79f06f51f8b42
3
+ metadata.gz: 0d92e5f4d5ae068f244d1adb69f59f536361046d28fe58923f0b347650122df1
4
+ data.tar.gz: 8e6a26811a717449fc52e4c53f076931fa1998fa28e4ec9937df27eff3bfd0f6
5
5
  SHA512:
6
- metadata.gz: 69abd24fe4a39391791c183b0880a8c909e8521a5155c7d1ac92be380780572a5d23423ed32cb9b1267f2f110fbe9a9958982dca5edd2486f4b43a9e3e0cb1d6
7
- data.tar.gz: 40d47a12bf922d452ad350180d2d1fbac189a16f2a732f3885784a7f71438e30a7e8dd7b75b875e5eeb8882ffc36a22c214177c287d13622a329322edc1b6030
6
+ metadata.gz: c100363ee1b8f540916da3e32a051e3de427dcb5625a63254604cc5388f9059aa2df74497d3a1a449d4a6121f366be36de0b548b03966f95616b65d07e0d3723
7
+ data.tar.gz: efcc1c0abf9412978ca89dc8777c91950d9cf90600b1bc6444d51cf8fedde10b5b9399b9af16105821f3773d28ae2905fed30563558faefd8e50bdd9ee981146
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 - 2026-10-12
4
+
5
+ Cloudflare's `clef-omni` reads audio and video.
6
+
7
+ - `Client#ask` takes `audio:` and `videos:`, arrays of base64 data URLs, for
8
+ `clef-omni`. Cloudflare reads WAV or MP3 audio and MP4 or WebM video. Added
9
+ `RubyDecisionModel::Media.data_url` and `.from_file`, which build data URLs
10
+ for images, audio, and video.
11
+ - When the provider or model doesn't read audio or video, including `clef`
12
+ and `clef-flash`, the client raises `RequestError` before sending.
13
+ - `cloudflare/clef-omni` and `@cf/cloudflare/clef-omni` resolve to
14
+ `clef-omni` on the Cloudflare provider. In 0.2.0 they raised
15
+ `ConfigurationError`. The bare `clef-omni` already worked.
16
+ - New provider hooks `supports_audio?(model)` and `supports_videos?(model)`,
17
+ both false by default. `Base#request_body` accepts `audio:` and `videos:`.
18
+ - `rake smoke` takes `SMOKE_AUDIO` and `SMOKE_VIDEO`.
19
+ - Hosted `clef-flash` now has a 24,576-token context window. Cloudflare cuts
20
+ a text state that runs past the window to fit instead of rejecting it. The
21
+ gem is unchanged.
22
+
3
23
  ## 0.2.0 - 2026-10-06
4
24
 
5
25
  The decision models that shipped after Jev, behind the same `Client`.
data/README.md CHANGED
@@ -127,7 +127,9 @@ client = RubyDecisionModel::Client.new(provider: provider)
127
127
 
128
128
  Requests go to
129
129
  `https://api.cloudflare.com/client/v4/accounts/<account_id>/ai/run/@cf/cloudflare/<model>`.
130
- The default model is `clef`; `clef-flash` is the smaller, faster one. The body
130
+ The default model is `clef`; `clef-flash` is the smaller, faster one, and
131
+ `clef-omni` also reads audio and video (see
132
+ [Images, audio, and video](#images-audio-and-video)). The body
131
133
  is System One. The provider unwraps Cloudflare's `{"result": ...}` envelope,
132
134
  and a body with `"success": false` raises `InvalidResponse` carrying
133
135
  Cloudflare's error text. The account id and model end up in the URL path, so
@@ -138,6 +140,11 @@ raises `ConfigurationError`.
138
140
  Usage reports tokens with no cost, and `response.request_id` is the `cf-ray`
139
141
  header.
140
142
 
143
+ Hosted `clef-flash` has a 24,576-token context window; `clef` and `clef-omni`
144
+ have about 64k. Cloudflare cuts a text state that runs past the window to
145
+ fit and returns no error. Images, audio, and video that don't fit fail the
146
+ request.
147
+
141
148
  ### Perplexity (pplx-decider)
142
149
 
143
150
  ```ruby
@@ -206,7 +213,9 @@ Databricks, and System One servers default to 30, since Perplexity documents
206
213
  responses of up to 23 seconds on large inputs and a local server may load the
207
214
  model on the first request. Connecting gets 5 seconds either way. A number
208
215
  passed as `timeout:` sets both, as in 0.1.0. Through OpenRouter, pass a
209
- longer `timeout:` yourself for large inputs to slower models.
216
+ longer `timeout:` yourself for large inputs to slower models. Audio and
217
+ video take longer than text: Cloudflare reports about 1.5 seconds for a
218
+ 21-second video with sound. Pass a longer `timeout:` for long clips.
210
219
 
211
220
  ### Model aliases
212
221
 
@@ -225,26 +234,31 @@ whatever the provider returned.
225
234
  | `"luna"`, `"gpt-6-luna"` | `openai/gpt-6-luna-decisions` | `gpt-6-luna` (OpenAI) |
226
235
  | `"openai/gpt-6-luna-decisions"`, `"openai/gpt-6-luna"`, `"gpt-6-luna-decisions"` | as given | `gpt-6-luna` (OpenAI) |
227
236
  | `"clef"`, `"clef-flash"` | `cloudflare/clef`, `cloudflare/clef-flash` | as given (Cloudflare) |
228
- | `"cloudflare/clef"`, `"cloudflare/clef-flash"` | as given | `clef`, `clef-flash` (Cloudflare) |
229
- | `"@cf/cloudflare/clef"`, `"@cf/cloudflare/clef-flash"` | as given | `clef`, `clef-flash` (Cloudflare) |
237
+ | `"cloudflare/clef"`, `"cloudflare/clef-flash"`, `"cloudflare/clef-omni"` | as given | `clef`, `clef-flash`, `clef-omni` (Cloudflare) |
238
+ | `"@cf/cloudflare/clef"`, `"@cf/cloudflare/clef-flash"`, `"@cf/cloudflare/clef-omni"` | as given | `clef`, `clef-flash`, `clef-omni` (Cloudflare) |
230
239
  | `"pplx-decider"` | `perplexity/pplx-decider-v1-27b` | `pplx-decider-v1.1-27b` (Perplexity) |
231
240
  | `"pplx-decider-v1-27b"` | `perplexity/pplx-decider-v1-27b` | as given (Perplexity) |
232
241
  | `"perplexity/pplx-decider-v1-27b"` | as given | `pplx-decider-v1-27b` (Perplexity) |
233
242
  | anything else | as given | as given |
234
243
 
235
244
  OpenRouter carries pplx-decider v1 only, so `"pplx-decider"` means v1 there
236
- and v1.1 on Perplexity's own API.
245
+ and v1.1 on Perplexity's own API. OpenRouter doesn't list `clef-omni` as of
246
+ October 2026, so it has no alias there; on Cloudflare pass `"clef-omni"`.
237
247
 
238
248
  ### Writing a provider
239
249
 
240
250
  Subclass `RubyDecisionModel::Providers::Base` and define `name`, `env_var`,
241
251
  `default_base_url`, `endpoint_path`, and `default_model`. Optional hooks:
242
- `aliases`, `reports_cost?`, `supports_images?`, `request_id_header`,
252
+ `aliases`, `reports_cost?`, `supports_images?`, `supports_audio?(model)`,
253
+ `supports_videos?(model)`, `request_id_header`,
243
254
  `requires_api_key?`, `default_timeout`, `error_message`, and `validate!`. When the wire format differs from
244
255
  System One, override `request_body` to encode and `normalize_response` to turn
245
256
  the parsed body back into System One answers keyed by question id; the
246
257
  OpenAI provider shows both directions. Override `url(model)` when the model
247
- belongs in the path. Pass an instance as `provider:`.
258
+ belongs in the path. A provider that answers true from `supports_audio?` or
259
+ `supports_videos?` gets `audio:` or `videos:` passed to `request_body`, so
260
+ an override of `request_body` has to accept them. Pass an instance as
261
+ `provider:`.
248
262
 
249
263
  ## Questions and answers
250
264
 
@@ -268,7 +282,7 @@ approximately 1; treat them as calibrated, not normalized. Noul
268
282
  `probabilities` are always `{"true" => p, "false" => 1 - p}`. Most providers
269
283
  send only the probability, and the client fills in the split.
270
284
 
271
- ## Images
285
+ ## Images, audio, and video
272
286
 
273
287
  OpenAI, Cloudflare, Perplexity, and System One servers read images. Pass them
274
288
  as base64 data URLs; no provider fetches a remote URL.
@@ -290,6 +304,39 @@ OpenAI, the `images` field for Cloudflare and System One servers, and
290
304
  Databricks don't take images, so the client raises `RequestError` before
291
305
  sending. Size and count limits vary by vendor and are enforced server side.
292
306
 
307
+ Cloudflare's `clef-omni` also reads audio (WAV or MP3) and video (MP4 or
308
+ WebM), passed as `audio:` and `videos:`. `RubyDecisionModel::Media.from_file`
309
+ and `.data_url` build data URLs for all three kinds, and `Images` still works
310
+ for images.
311
+
312
+ ```ruby
313
+ client = RubyDecisionModel::Client.new(provider: :cloudflare, model: "clef-omni")
314
+
315
+ client.ask(
316
+ state: "Customer sent a photo, a voice note, and a short video of the fault.",
317
+ questions: { "faulty" => RubyDecisionModel::Questions.noul("Is the device faulty?") },
318
+ images: [RubyDecisionModel::Media.from_file("device.jpg")],
319
+ audio: [RubyDecisionModel::Media.from_file("note.mp3")],
320
+ videos: [RubyDecisionModel::Media.from_file("fault.mp4")]
321
+ )
322
+ ```
323
+
324
+ On Cloudflare they go in the `images`, `audio`, and `videos` fields. A
325
+ provider or model that doesn't read audio or video, including `clef` and
326
+ `clef-flash`, makes the client raise `RequestError` before sending.
327
+ Cloudflare documents up to 4 images, 4 audio clips, and 2 videos per request,
328
+ enforced server side.
329
+
330
+ The client checks only that each item is a data URL of the right kind
331
+ (`data:image/`, `data:audio/`, or `data:video/`). It does not decode the
332
+ payload or measure it, and `from_file` reads the whole file into memory.
333
+ Format, size, and count are the provider's to enforce, so check uploads you
334
+ don't control before you pass them on.
335
+
336
+ A retry sends the whole body again, so with the default policy a request
337
+ carrying large clips can upload up to three times. Build the client with
338
+ `retry: { max_retries: 0 }` to turn retries off for those calls.
339
+
293
340
  ## Retries
294
341
 
295
342
  Retry behaviour follows the official Typesafe SDKs and lives in
@@ -338,7 +385,7 @@ return is still accepted and treated as having no headers, which means no
338
385
  | Error | Meaning |
339
386
  | --- | --- |
340
387
  | `ConfigurationError` | No provider could be resolved, a missing key or setting (api_key, account_id, base_url), unknown provider, a `model:` Databricks can't take, or bad `retry:` value |
341
- | `RequestError` | Questions hash was empty, images were not data URLs or went to a provider that doesn't read them, or state or questions could not be encoded as JSON (original error on `#cause`) |
388
+ | `RequestError` | Questions hash was empty, images, audio, or videos were not data URLs or went to a provider or model that doesn't read them, or state or questions could not be encoded as JSON (original error on `#cause`) |
342
389
  | `TransportError` (`TimeoutError`) | Network or timeout failure after retries, carries `#cause_error` |
343
390
  | `ApiError` | Non-2xx response, carries `#status`, `#body`, and `#headers`. The message ends with the vendor's own reason when the body has one |
344
391
  | `Unauthorized` | 401 |
@@ -349,7 +396,7 @@ return is still accepted and treated as having no headers, which means no
349
396
  | `InvalidResponse` | Body wasn't JSON, wasn't a Hash, or an answer was malformed (including a noul outside 0..1 or an id answered twice) |
350
397
  | `MissingAnswers` | One or more question ids came back missing, wrong-typed, or refused. Carries `#missing`, `#refused` (the subset the provider declined), and `#answers` |
351
398
 
352
- Status: 0.2.0, API may change.
399
+ Status: 0.3.0, API may change.
353
400
 
354
401
  The companion gem `decide` builds decisions and verdicts on top of this client.
355
402
 
@@ -364,6 +411,8 @@ anywhere. To ship a version:
364
411
  request per configured provider and prints the answers. CI never runs it.
365
412
  `TARGETS=open_router:clef,open_router:luna` picks provider and model
366
413
  pairs, and `SMOKE_IMAGE=photo.png` adds an image where supported.
414
+ `SMOKE_AUDIO=clip.mp3` and `SMOKE_VIDEO=clip.mp4` add audio and video
415
+ where the model reads them, such as `TARGETS=cloudflare:clef-omni`.
367
416
  4. Merge to `main`. The Release workflow runs the suite, builds the gem with
368
417
  `gem build --strict`, checks the built gem carries every file under
369
418
  `lib/`, and pushes it. A version already on RubyGems is skipped, so the
@@ -60,12 +60,13 @@ module RubyDecisionModel
60
60
  @provider.base_url
61
61
  end
62
62
 
63
- # images: data URLs (see Images) for providers that read images. Each
64
- # provider places them where its API expects.
65
- def ask(state:, questions:, images: nil)
63
+ # images:, audio:, videos: data URLs (see Images and Media) for providers
64
+ # and models that read them. Each provider places them where its API
65
+ # expects.
66
+ def ask(state:, questions:, images: nil, audio: nil, videos: nil)
66
67
  raise RequestError, "questions must not be empty" if questions.nil? || questions.empty?
67
68
 
68
- body = build_body(state, questions, images)
69
+ body = build_body(state, questions, images: images, audio: audio, videos: videos)
69
70
  status, response_body, response_headers = perform_with_retry(
70
71
  url: request_url, headers: @provider.headers, body: body
71
72
  )
@@ -81,25 +82,44 @@ module RubyDecisionModel
81
82
  takes_model ? @provider.url(@model) : @provider.url
82
83
  end
83
84
 
84
- def build_body(state, questions, images)
85
- images = Array(images)
86
- return @provider.request_body(model: @model, state: state, questions: questions) if images.empty?
85
+ MEDIA_PREFIXES = { images: "data:image/", audio: "data:audio/", videos: "data:video/" }.freeze
86
+ private_constant :MEDIA_PREFIXES
87
87
 
88
- raise RequestError, "#{@provider.name} does not accept images" unless @provider.supports_images?
88
+ def build_body(state, questions, **media)
89
+ media = media.transform_values { |urls| Array(urls) }.reject { |_kind, urls| urls.empty? }
89
90
 
90
- images.each do |image|
91
- next if image.is_a?(String) && image.start_with?("data:image/")
91
+ media.each do |kind, urls|
92
+ raise RequestError, unsupported_message(kind) unless accepts?(kind)
92
93
 
93
- raise RequestError, "images must be data URLs (data:image/...); see RubyDecisionModel::Images"
94
+ prefix = MEDIA_PREFIXES.fetch(kind)
95
+ next if urls.all? { |url| url.is_a?(String) && url.start_with?(prefix) }
96
+
97
+ raise RequestError, "#{kind} must be data URLs (#{prefix}...); see RubyDecisionModel::#{kind == :images ? 'Images' : 'Media'}"
94
98
  end
95
99
 
96
- @provider.request_body(model: @model, state: state, questions: questions, images: images)
100
+ @provider.request_body(model: @model, state: state, questions: questions, **media)
97
101
  rescue JSON::GeneratorError, JSON::NestingError => e
98
102
  # The generator's message can quote the offending value, so it stays
99
103
  # on #cause rather than in a message that may be logged.
100
104
  raise RequestError, "state or questions could not be encoded as JSON (#{e.class})"
101
105
  end
102
106
 
107
+ # supports_images? takes no argument in 0.2.0 providers, so it is called
108
+ # bare; audio and videos can differ per model, so those get the model.
109
+ def accepts?(kind)
110
+ case kind
111
+ when :images then @provider.supports_images?
112
+ when :audio then @provider.supports_audio?(@model)
113
+ when :videos then @provider.supports_videos?(@model)
114
+ end
115
+ end
116
+
117
+ def unsupported_message(kind)
118
+ return "#{@provider.name} does not accept images" if kind == :images
119
+
120
+ "#{[@provider.name, @model].compact.join(' ')} does not accept #{kind}"
121
+ end
122
+
103
123
  def resolve_provider(provider, api_key:, base_url:)
104
124
  case provider
105
125
  when Providers::Base
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "images"
4
+
5
+ module RubyDecisionModel
6
+ # Builds the base64 data URLs that Client#ask takes as images:, audio:, and
7
+ # videos:. Images has the image-only version of the same helpers.
8
+ module Media
9
+ CONTENT_TYPES = Images::CONTENT_TYPES.merge(
10
+ ".wav" => "audio/wav",
11
+ ".mp3" => "audio/mpeg",
12
+ ".mp4" => "video/mp4",
13
+ ".webm" => "video/webm"
14
+ ).freeze
15
+
16
+ module_function
17
+
18
+ def data_url(bytes, content_type:)
19
+ unless content_type.to_s.start_with?("image/", "audio/", "video/")
20
+ raise ArgumentError, "content_type must be an image/*, audio/*, or video/* type, got #{content_type.inspect}"
21
+ end
22
+
23
+ # pack("m0") is strict base64 without the base64 gem, which leaves the
24
+ # default gems in Ruby 3.4.
25
+ "data:#{content_type};base64,#{[bytes].pack('m0')}"
26
+ end
27
+
28
+ # Reads a file and infers its type from the extension.
29
+ def from_file(path, content_type: nil)
30
+ content_type ||= CONTENT_TYPES[File.extname(path.to_s).downcase]
31
+ raise ArgumentError, "cannot infer a media type for #{path}; pass content_type:" if content_type.nil?
32
+
33
+ data_url(File.binread(path), content_type: content_type)
34
+ end
35
+ end
36
+ end
@@ -65,6 +65,18 @@ module RubyDecisionModel
65
65
  false
66
66
  end
67
67
 
68
+ # Whether requests may carry audio or videos. Client refuses audio: and
69
+ # videos: for a provider that says no, before anything goes over the
70
+ # wire. The model is passed because support can differ between one
71
+ # provider's models.
72
+ def supports_audio?(_model = nil)
73
+ false
74
+ end
75
+
76
+ def supports_videos?(_model = nil)
77
+ false
78
+ end
79
+
68
80
  # Response header carrying the provider's request id, or nil.
69
81
  # Typesafe's, which 0.1.0 read for every provider and System One
70
82
  # servers that follow Typesafe send too. Vendors with their own header
@@ -129,12 +141,14 @@ module RubyDecisionModel
129
141
  headers
130
142
  end
131
143
 
132
- # Client passes images: only when the caller supplied some, so a
133
- # subclass that does not take images can keep a three-keyword
134
- # signature.
135
- def request_body(model:, state:, questions:, images: nil)
144
+ # Client passes images:, audio:, and videos: only when the caller
145
+ # supplied some and the provider said it supports them, so a subclass
146
+ # that takes none can keep a three-keyword signature.
147
+ def request_body(model:, state:, questions:, images: nil, audio: nil, videos: nil)
136
148
  body = { "model" => model, "state" => state, "questions" => questions }
137
149
  body["images"] = images if images
150
+ body["audio"] = audio if audio
151
+ body["videos"] = videos if videos
138
152
  JSON.generate(body)
139
153
  end
140
154
 
@@ -4,8 +4,9 @@ require_relative "base"
4
4
 
5
5
  module RubyDecisionModel
6
6
  module Providers
7
- # Cloudflare's Clef and Clef-flash on Workers AI. The body is System One
8
- # with Cloudflare's images extension; the model also appears in the
7
+ # Cloudflare's Clef, Clef-flash, and Clef-omni on Workers AI. The body is
8
+ # System One with Cloudflare's images, audio, and videos extensions
9
+ # (audio and videos on Clef-omni only); the model also appears in the
9
10
  # path, and the account id is required. Responses arrive inside the
10
11
  # Workers AI envelope ({"result": ..., "success": true}), which is
11
12
  # unwrapped here.
@@ -14,9 +15,14 @@ module RubyDecisionModel
14
15
  "cloudflare/clef" => "clef",
15
16
  "cloudflare/clef-flash" => "clef-flash",
16
17
  "@cf/cloudflare/clef" => "clef",
17
- "@cf/cloudflare/clef-flash" => "clef-flash"
18
+ "@cf/cloudflare/clef-flash" => "clef-flash",
19
+ "cloudflare/clef-omni" => "clef-omni",
20
+ "@cf/cloudflare/clef-omni" => "clef-omni"
18
21
  }.freeze
19
22
 
23
+ # The models that read audio and videos as well as images.
24
+ OMNI_MODELS = %w[clef-omni].freeze
25
+
20
26
  # Cloudflare's own tooling reads CLOUDFLARE_API_TOKEN; the Clef docs
21
27
  # use CLOUDFLARE_AUTH_TOKEN. Either works, in that order.
22
28
  FALLBACK_ENV_VAR = "CLOUDFLARE_AUTH_TOKEN"
@@ -76,6 +82,14 @@ module RubyDecisionModel
76
82
  true
77
83
  end
78
84
 
85
+ def supports_audio?(model = nil)
86
+ OMNI_MODELS.include?(model.to_s)
87
+ end
88
+
89
+ def supports_videos?(model = nil)
90
+ OMNI_MODELS.include?(model.to_s)
91
+ end
92
+
79
93
  def request_id_header
80
94
  "cf-ray"
81
95
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyDecisionModel
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -7,6 +7,7 @@ require_relative "ruby_decision_model/version"
7
7
  require_relative "ruby_decision_model/errors"
8
8
  require_relative "ruby_decision_model/questions"
9
9
  require_relative "ruby_decision_model/images"
10
+ require_relative "ruby_decision_model/media"
10
11
  require_relative "ruby_decision_model/answers"
11
12
  require_relative "ruby_decision_model/response"
12
13
  require_relative "ruby_decision_model/providers"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_decision_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Obie Fernandez
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-07 00:00:00.000000000 Z
11
+ date: 2026-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -40,11 +40,11 @@ dependencies:
40
40
  version: '13.0'
41
41
  description: Decision models answer typed questions about a state instead of generating
42
42
  text. ruby_decision_model builds noul (yes/no probability), choice, and score questions,
43
- posts them with a state and optional images to a decision-model provider (OpenRouter,
44
- Typesafe, OpenAI, Cloudflare, Perplexity, Databricks, or any System One server such
45
- as Ollama), and returns the same normalized answers with probabilities, confidence,
46
- legends, and usage whichever vendor served them. Retries follow the official Typesafe
47
- SDKs.
43
+ posts them with a state and optional images, audio, and video to a decision-model
44
+ provider (OpenRouter, Typesafe, OpenAI, Cloudflare, Perplexity, Databricks, or any
45
+ System One server such as Ollama), and returns the same normalized answers with
46
+ probabilities, confidence, legends, and usage whichever vendor served them. Retries
47
+ follow the official Typesafe SDKs.
48
48
  email:
49
49
  - obiefernandez@gmail.com
50
50
  executables: []
@@ -59,6 +59,7 @@ files:
59
59
  - lib/ruby_decision_model/client.rb
60
60
  - lib/ruby_decision_model/errors.rb
61
61
  - lib/ruby_decision_model/images.rb
62
+ - lib/ruby_decision_model/media.rb
62
63
  - lib/ruby_decision_model/providers.rb
63
64
  - lib/ruby_decision_model/providers/base.rb
64
65
  - lib/ruby_decision_model/providers/cloudflare.rb