Module: Cloudflare::AI

Defined in:
lib/cloudflare_workers/ai.rb

Defined Under Namespace

Classes: Stream

Constant Summary collapse

DEFAULT_OPTIONS =

Default REST options forwarded to env.AI.run as the third argument.

{}.freeze

Class Method Summary collapse

Class Method Details

.ruby_to_js(val) ⇒ Object

Convert a Ruby value (Hash / Array / String / Numeric / true / false / nil) into a plain JS object suitable for env.AI.run inputs.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/cloudflare_workers/ai.rb', line 101

def self.ruby_to_js(val)
  if val.is_a?(Hash)
    obj = `({})`
    val.each do |k, v|
      ks = k.to_s
      jv = ruby_to_js(v)
      `#{obj}[#{ks}] = #{jv}`
    end
    obj
  elsif val.is_a?(Array)
    arr = `([])`
    val.each do |v|
      jv = ruby_to_js(v)
      `#{arr}.push(#{jv})`
    end
    arr
  elsif val.is_a?(Symbol)
    val.to_s
  else
    val
  end
end

.run(model, inputs, binding: nil, options: nil) ⇒ Object

Run a Workers AI model. Returns a JS Promise that resolves to a Ruby Hash for non-streaming calls, or to a Cloudflare::AI::Stream wrapping the JS ReadableStream for streaming calls.

Parameters:

  • model (String)

    catalog model id, e.g. β€˜@cf/google/gemma-4-26b-a4b-it’

  • inputs (Hash)

    model inputs (messages / prompt / max_tokens / etc.)

  • binding (JS object) (defaults to: nil)

    env.AI binding (required)

  • options (Hash) (defaults to: nil)

    gateway / extra options forwarded as the 3rd arg

Raises:



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/cloudflare_workers/ai.rb', line 51

def self.run(model, inputs, binding: nil, options: nil)
  # Use a JS-side null check because `binding` may be a raw JS object
  # (env.AI), which has no Ruby `#nil?` method on the prototype.
  bound = !`(#{binding} == null)`
  raise AIError.new('AI binding not bound (env.AI is null)', model: model) unless bound
  js_inputs = ruby_to_js(inputs)
  js_options = options ? ruby_to_js(options) : `({})`
  ai_binding = binding
  err_klass = Cloudflare::AIError
  stream_klass = Cloudflare::AI::Stream
  # Streaming may be requested either via `inputs[:stream]` (the
   # newer Workers AI shape) or `options: { stream: true }` (the
   # 3rd-arg "options" contract). Accept both so callers can use
   # whichever idiom matches the model docs they're following.
  streaming = (inputs.is_a?(Hash) && (inputs[:stream] == true || inputs['stream'] == true)) ||
              (options.is_a?(Hash) && (options[:stream] == true || options['stream'] == true))
  cf = Cloudflare

  # NOTE: multi-line backtick β†’ Promise works HERE because the
  # value is assigned to `js_promise` (Opal emits the statement AND
  # keeps the returned value alive through the local). Do NOT
  # refactor this so the backtick is the method's last expression
  # or the Promise will be silently dropped (same pitfall
  # documented in lib/cloudflare_workers/{cache,queue}.rb β€”
  # Phase 11B audit).
  js_promise = `
    (async function() {
      var out;
      try {
        out = await #{ai_binding}.run(#{model}, #{js_inputs}, #{js_options});
      } catch (e) {
        #{Kernel}.$raise(#{err_klass}.$new(e && e.message ? e.message : String(e), Opal.hash({ model: #{model}, operation: 'run' })));
      }
      return out;
    })()
  `

  js_result = js_promise.__await__

  if streaming
    # Workers AI returns a ReadableStream<Uint8Array> when stream:true.
    # Wrap it so the Sinatra route can return it as an SSE body.
    stream_klass.new(js_result)
  else
    cf.js_to_ruby(js_result)
  end
end