homura-runtime 0.3.8 → 0.3.10
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 +4 -4
- data/CHANGELOG.md +15 -0
- data/exe/auto-await +23 -23
- data/exe/compile-assets +68 -61
- data/exe/compile-erb +263 -255
- data/exe/homura-build +74 -21
- data/lib/homura/runtime/ai.rb +104 -85
- data/lib/homura/runtime/async_registry.rb +124 -109
- data/lib/homura/runtime/auto_await/analyzer.rb +28 -15
- data/lib/homura/runtime/auto_await/transformer.rb +1 -0
- data/lib/homura/runtime/build_support.rb +90 -11
- data/lib/homura/runtime/cache.rb +21 -18
- data/lib/homura/runtime/durable_object.rb +27 -17
- data/lib/homura/runtime/email.rb +14 -14
- data/lib/homura/runtime/http.rb +4 -3
- data/lib/homura/runtime/multipart.rb +11 -4
- data/lib/homura/runtime/queue.rb +53 -23
- data/lib/homura/runtime/scheduled.rb +12 -14
- data/lib/homura/runtime/stream.rb +18 -14
- data/lib/homura/runtime/version.rb +1 -1
- data/lib/homura/runtime.rb +129 -93
- data/lib/homura_vendor_tempfile.rb +4 -2
- data/lib/homura_vendor_tilt.rb +5 -3
- data/lib/homura_vendor_zlib.rb +3 -0
- data/lib/literal/opal_compat.rb +141 -0
- data/lib/opal_patches.rb +83 -66
- data/lib/phlex/opal_compat.rb +403 -0
- data/lib/zeitwerk/opal_compat.rb +91 -0
- metadata +4 -1
data/exe/homura-build
CHANGED
|
@@ -8,6 +8,7 @@ require "fileutils"
|
|
|
8
8
|
require "open3"
|
|
9
9
|
require "optparse"
|
|
10
10
|
require "pathname"
|
|
11
|
+
|
|
11
12
|
require_relative "../lib/homura/runtime/build_support"
|
|
12
13
|
|
|
13
14
|
module HomuraRuntimeBuild
|
|
@@ -55,6 +56,7 @@ OptionParser
|
|
|
55
56
|
o.on("--root PATH", "Project root (default: cwd)") do |p|
|
|
56
57
|
options[:root] = p
|
|
57
58
|
end
|
|
59
|
+
|
|
58
60
|
o.on(
|
|
59
61
|
"--standalone",
|
|
60
62
|
"Consumer app (skip inline-routes; use Gemfile-resolved load paths)"
|
|
@@ -62,6 +64,7 @@ OptionParser
|
|
|
62
64
|
o.on("--with-db", "Include sequel-d1 on Opal load path (standalone)") do
|
|
63
65
|
options[:with_db] = true
|
|
64
66
|
end
|
|
67
|
+
|
|
65
68
|
o.on("--input PATH", "Opal entry .rb") { |p| options[:opal_input] = p }
|
|
66
69
|
o.on("--output PATH", "Opal bundle .mjs") { |p| options[:opal_output] = p }
|
|
67
70
|
o.on(
|
|
@@ -71,9 +74,11 @@ OptionParser
|
|
|
71
74
|
o.on("--setup-import PATH", "setup-node-crypto import in entrypoint") do |p|
|
|
72
75
|
options[:setup_import] = p
|
|
73
76
|
end
|
|
77
|
+
|
|
74
78
|
o.on("--bundle-import PATH", "Opal bundle import in entrypoint") do |p|
|
|
75
79
|
options[:bundle_import] = p
|
|
76
80
|
end
|
|
81
|
+
|
|
77
82
|
o.on(
|
|
78
83
|
"--worker-module-import PATH",
|
|
79
84
|
"worker_module.mjs import in entrypoint"
|
|
@@ -81,6 +86,7 @@ OptionParser
|
|
|
81
86
|
o.on("--entrypoint-out PATH", "Where to write worker.entrypoint.mjs") do |p|
|
|
82
87
|
options[:entrypoint_out] = p
|
|
83
88
|
end
|
|
89
|
+
|
|
84
90
|
o.on(
|
|
85
91
|
"--templates-namespace NAME",
|
|
86
92
|
"Standalone templates module name (default: project-derived)"
|
|
@@ -95,16 +101,13 @@ OptionParser
|
|
|
95
101
|
options[:standalone] = true if options[:with_db]
|
|
96
102
|
|
|
97
103
|
root = Pathname(options[:root]).expand_path
|
|
98
|
-
options[
|
|
99
|
-
:templates_namespace
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
root,
|
|
106
|
-
"Assets"
|
|
107
|
-
) if options[:standalone]
|
|
104
|
+
if options[:standalone]
|
|
105
|
+
options[:templates_namespace] ||= HomuraRuntime::BuildSupport.standalone_namespace(root, "Templates")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if options[:standalone]
|
|
109
|
+
options[:assets_namespace] ||= HomuraRuntime::BuildSupport.standalone_namespace(root, "Assets")
|
|
110
|
+
end
|
|
108
111
|
|
|
109
112
|
if options[:standalone]
|
|
110
113
|
Dir.chdir(root) { require "bundler/setup" }
|
|
@@ -175,7 +178,7 @@ def run_opal_homura!(root, opal_input, opal_output)
|
|
|
175
178
|
]
|
|
176
179
|
stderr_log = root.join("build/opal.stderr.log")
|
|
177
180
|
FileUtils.mkdir_p(root.join("build"))
|
|
178
|
-
env = {
|
|
181
|
+
env = {"OPAL_PREFORK_DISABLE" => "1"}
|
|
179
182
|
out_err, status = Open3.capture2e(env, *argv, chdir: root.to_s)
|
|
180
183
|
File.write(stderr_log, out_err)
|
|
181
184
|
abort("homura build: opal failed (see #{stderr_log})") unless status.success?
|
|
@@ -186,18 +189,19 @@ def homura_vendor_from_gemfile(project_root)
|
|
|
186
189
|
end
|
|
187
190
|
|
|
188
191
|
def run_opal_standalone!(root, opal_input, opal_output, with_db:)
|
|
189
|
-
load_paths =
|
|
190
|
-
|
|
192
|
+
load_paths = HomuraRuntime::BuildSupport.standalone_load_paths(root, with_db: with_db)
|
|
193
|
+
opal_gems_prelude = root.join("build", "homura_opal_gems.rb")
|
|
191
194
|
|
|
192
195
|
argv = %w[bundle exec opal -c -E --esm --no-source-map]
|
|
193
196
|
load_paths.each { |p| argv.push("-I", p) }
|
|
194
|
-
argv +=
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
argv += %w[-r opal_patches -r homura/runtime -r app_templates -r app_assets]
|
|
198
|
+
argv += %w[-r homura_opal_gems] if opal_gems_prelude.file?
|
|
199
|
+
argv += %w[-o] +
|
|
200
|
+
[opal_output, opal_input]
|
|
197
201
|
|
|
198
202
|
stderr_log = root.join("build/opal.stderr.log")
|
|
199
203
|
FileUtils.mkdir_p(root.join("build"))
|
|
200
|
-
env = {
|
|
204
|
+
env = {"OPAL_PREFORK_DISABLE" => "1"}
|
|
201
205
|
out_err, status = Open3.capture2e(env, *argv, chdir: root.to_s)
|
|
202
206
|
File.write(stderr_log, out_err)
|
|
203
207
|
abort("homura build: opal failed (see #{stderr_log})") unless status.success?
|
|
@@ -206,11 +210,58 @@ end
|
|
|
206
210
|
def write_entrypoint!(root, out_path, setup:, bundle:, worker_mod:)
|
|
207
211
|
out_file = root.join(out_path)
|
|
208
212
|
out_dir = out_file.dirname
|
|
213
|
+
bundle_spec = relative_import_spec(out_dir, root.join(bundle))
|
|
214
|
+
worker_spec = relative_import_spec(out_dir, root.join(worker_mod))
|
|
209
215
|
body = <<~JS
|
|
210
216
|
// GENERATED by homura build — do not edit by hand.
|
|
211
217
|
import "#{relative_import_spec(out_dir, root.join(setup))}";
|
|
212
|
-
import "#{
|
|
213
|
-
|
|
218
|
+
import runtimeDefault, { HomuraCounterDO as RuntimeHomuraCounterDO } from "#{worker_spec}";
|
|
219
|
+
|
|
220
|
+
let opalBundleReady;
|
|
221
|
+
|
|
222
|
+
function ensureOpalBundle() {
|
|
223
|
+
opalBundleReady ||= import("#{bundle_spec}");
|
|
224
|
+
return opalBundleReady;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default {
|
|
228
|
+
async fetch(request, env, ctx) {
|
|
229
|
+
await ensureOpalBundle();
|
|
230
|
+
return runtimeDefault.fetch(request, env, ctx);
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
async scheduled(event, env, ctx) {
|
|
234
|
+
await ensureOpalBundle();
|
|
235
|
+
return runtimeDefault.scheduled(event, env, ctx);
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
async queue(batch, env, ctx) {
|
|
239
|
+
await ensureOpalBundle();
|
|
240
|
+
return runtimeDefault.queue(batch, env, ctx);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export class HomuraCounterDO extends RuntimeHomuraCounterDO {
|
|
245
|
+
async fetch(request) {
|
|
246
|
+
await ensureOpalBundle();
|
|
247
|
+
return super.fetch(request);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async webSocketMessage(ws, message) {
|
|
251
|
+
await ensureOpalBundle();
|
|
252
|
+
return super.webSocketMessage(ws, message);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async webSocketClose(ws, code, reason, wasClean) {
|
|
256
|
+
await ensureOpalBundle();
|
|
257
|
+
return super.webSocketClose(ws, code, reason, wasClean);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async webSocketError(ws, err) {
|
|
261
|
+
await ensureOpalBundle();
|
|
262
|
+
return super.webSocketError(ws, err);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
214
265
|
JS
|
|
215
266
|
File.write(out_file, body)
|
|
216
267
|
end
|
|
@@ -367,7 +418,7 @@ else
|
|
|
367
418
|
chdir: root
|
|
368
419
|
)
|
|
369
420
|
else
|
|
370
|
-
warn
|
|
421
|
+
warn("homura build: no app/ directory or top-level app.rb — skipping auto-await")
|
|
371
422
|
end
|
|
372
423
|
|
|
373
424
|
# Also run auto-await over every gem we ship to Opal:
|
|
@@ -399,6 +450,8 @@ else
|
|
|
399
450
|
end
|
|
400
451
|
end
|
|
401
452
|
|
|
453
|
+
HomuraRuntime::BuildSupport.write_opal_gems_prelude(root)
|
|
454
|
+
|
|
402
455
|
opal_in = Pathname(resolve_opal_input(root, options[:opal_input]))
|
|
403
456
|
opal_out = Pathname(options[:opal_output])
|
|
404
457
|
opal_in = root.join(opal_in) unless opal_in.absolute?
|
|
@@ -435,4 +488,4 @@ write_entrypoint!(
|
|
|
435
488
|
worker_mod: options[:worker_module_import]
|
|
436
489
|
)
|
|
437
490
|
|
|
438
|
-
$stderr.puts
|
|
491
|
+
$stderr.puts("homura build: ok")
|
data/lib/homura/runtime/ai.rb
CHANGED
|
@@ -56,15 +56,21 @@ module Cloudflare
|
|
|
56
56
|
|
|
57
57
|
def run(model, inputs = nil, options: nil, **input_options)
|
|
58
58
|
payload = inputs || input_options
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
if inputs.is_a?(Hash) &&
|
|
60
|
+
!input_options.empty?
|
|
61
|
+
payload = payload.merge(input_options)
|
|
62
|
+
end
|
|
63
|
+
|
|
61
64
|
Cloudflare::AI.run(model, payload, binding: @js, options: options)
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
def run_stream(model, inputs = nil, **input_options)
|
|
65
68
|
payload = inputs || input_options
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
if inputs.is_a?(Hash) &&
|
|
70
|
+
!input_options.empty?
|
|
71
|
+
payload = payload.merge(input_options)
|
|
72
|
+
end
|
|
73
|
+
|
|
68
74
|
run(model, payload.merge(stream: true))
|
|
69
75
|
end
|
|
70
76
|
|
|
@@ -78,12 +84,11 @@ module Cloudflare
|
|
|
78
84
|
)
|
|
79
85
|
chat_options = Cloudflare::AI.chat_input_options(model, input_options)
|
|
80
86
|
payload = {
|
|
81
|
-
messages:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
+
messages: Cloudflare::AI.build_messages(
|
|
88
|
+
prompt,
|
|
89
|
+
messages: messages,
|
|
90
|
+
system: system
|
|
91
|
+
)
|
|
87
92
|
}.merge(chat_options)
|
|
88
93
|
run(model, payload, options: options)
|
|
89
94
|
end
|
|
@@ -96,15 +101,14 @@ module Cloudflare
|
|
|
96
101
|
options: nil,
|
|
97
102
|
**input_options
|
|
98
103
|
)
|
|
99
|
-
response =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
104
|
+
response = chat(
|
|
105
|
+
prompt,
|
|
106
|
+
messages: messages,
|
|
107
|
+
system: system,
|
|
108
|
+
model: model,
|
|
109
|
+
options: options,
|
|
110
|
+
**input_options
|
|
111
|
+
)
|
|
108
112
|
response = response.__await__ if Cloudflare.js_promise?(response)
|
|
109
113
|
Cloudflare::AI.extract_text(response)
|
|
110
114
|
end
|
|
@@ -115,7 +119,7 @@ module Cloudflare
|
|
|
115
119
|
options: nil,
|
|
116
120
|
**input_options
|
|
117
121
|
)
|
|
118
|
-
payload = {
|
|
122
|
+
payload = {audio: Cloudflare::AI.audio_input(audio)}.merge(
|
|
119
123
|
input_options
|
|
120
124
|
)
|
|
121
125
|
run(model, payload, options: options)
|
|
@@ -127,16 +131,14 @@ module Cloudflare
|
|
|
127
131
|
options: nil,
|
|
128
132
|
**input_options
|
|
129
133
|
)
|
|
130
|
-
response =
|
|
131
|
-
transcribe(audio, model: model, options: options, **input_options)
|
|
134
|
+
response = transcribe(audio, model: model, options: options, **input_options)
|
|
132
135
|
response = response.__await__ if Cloudflare.js_promise?(response)
|
|
133
136
|
Cloudflare::AI.extract_text(response)
|
|
134
137
|
end
|
|
135
138
|
|
|
136
139
|
def speak(text, model: DEFAULT_SPEAK_MODEL, options: nil, **input_options)
|
|
137
|
-
payload = {
|
|
138
|
-
response =
|
|
139
|
-
Cloudflare::AI.speak(model, payload, binding: @js, options: options)
|
|
140
|
+
payload = {text: text.to_s}.merge(input_options)
|
|
141
|
+
response = Cloudflare::AI.speak(model, payload, binding: @js, options: options)
|
|
140
142
|
response = response.__await__ if Cloudflare.js_promise?(response)
|
|
141
143
|
response
|
|
142
144
|
end
|
|
@@ -147,14 +149,13 @@ module Cloudflare
|
|
|
147
149
|
options: nil,
|
|
148
150
|
**input_options
|
|
149
151
|
)
|
|
150
|
-
payload = {
|
|
151
|
-
response =
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)
|
|
152
|
+
payload = {text: text.to_s}.merge(input_options)
|
|
153
|
+
response = Cloudflare::AI.speak_data_url(
|
|
154
|
+
model,
|
|
155
|
+
payload,
|
|
156
|
+
binding: @js,
|
|
157
|
+
options: options
|
|
158
|
+
)
|
|
158
159
|
response = response.__await__ if Cloudflare.js_promise?(response)
|
|
159
160
|
response.to_s
|
|
160
161
|
end
|
|
@@ -169,14 +170,17 @@ module Cloudflare
|
|
|
169
170
|
# @param binding [JS object] env.AI binding (required)
|
|
170
171
|
# @param options [Hash] gateway / extra options forwarded as the 3rd arg
|
|
171
172
|
def self.run(model, inputs, binding: nil, options: nil, raw_response: false)
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
if defined?(Binding) &&
|
|
174
|
+
`(#{binding} != null && #{binding}.$$class === #{Binding})`
|
|
175
|
+
binding = binding.js
|
|
176
|
+
end
|
|
174
177
|
# Use a JS-side null check because `binding` may be a raw JS object
|
|
175
178
|
# (env.AI), which has no Ruby `#nil?` method on the prototype.
|
|
176
179
|
bound = !`(#{binding} == null)`
|
|
177
180
|
unless bound
|
|
178
181
|
raise AIError.new("AI binding not bound (env.AI is null)", model: model)
|
|
179
182
|
end
|
|
183
|
+
|
|
180
184
|
js_inputs = ruby_to_js(inputs)
|
|
181
185
|
js_options = options ? ruby_to_js(options) : `({})`
|
|
182
186
|
ai_binding = binding
|
|
@@ -186,15 +190,10 @@ module Cloudflare
|
|
|
186
190
|
# newer Workers AI shape) or `options: { stream: true }` (the
|
|
187
191
|
# 3rd-arg "options" contract). Accept both so callers can use
|
|
188
192
|
# whichever idiom matches the model docs they're following.
|
|
189
|
-
streaming =
|
|
190
|
-
(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
) ||
|
|
194
|
-
(
|
|
195
|
-
options.is_a?(Hash) &&
|
|
196
|
-
(options[:stream] == true || options["stream"] == true)
|
|
197
|
-
)
|
|
193
|
+
streaming = (inputs.is_a?(Hash) &&
|
|
194
|
+
(inputs[:stream] == true || inputs["stream"] == true)) ||
|
|
195
|
+
(options.is_a?(Hash) &&
|
|
196
|
+
(options[:stream] == true || options["stream"] == true))
|
|
198
197
|
cf = Cloudflare
|
|
199
198
|
|
|
200
199
|
# NOTE: multi-line backtick → Promise works HERE because the
|
|
@@ -204,8 +203,7 @@ module Cloudflare
|
|
|
204
203
|
# or the Promise will be silently dropped (same pitfall
|
|
205
204
|
# documented in lib/homura/runtime/{cache,queue}.rb —
|
|
206
205
|
# Phase 11B audit).
|
|
207
|
-
js_promise =
|
|
208
|
-
`
|
|
206
|
+
js_promise = `
|
|
209
207
|
(async function() {
|
|
210
208
|
var out;
|
|
211
209
|
try {
|
|
@@ -231,8 +229,11 @@ module Cloudflare
|
|
|
231
229
|
end
|
|
232
230
|
|
|
233
231
|
def self.speak(model, inputs, binding: nil, options: nil)
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
if defined?(Binding) &&
|
|
233
|
+
`(#{binding} != null && #{binding}.$$class === #{Binding})`
|
|
234
|
+
binding = binding.js
|
|
235
|
+
end
|
|
236
|
+
|
|
236
237
|
bound = !`(#{binding} == null)`
|
|
237
238
|
unless bound
|
|
238
239
|
raise AIError.new("AI binding not bound (env.AI is null)", model: model)
|
|
@@ -243,8 +244,7 @@ module Cloudflare
|
|
|
243
244
|
ai_binding = binding
|
|
244
245
|
err_klass = Cloudflare::AIError
|
|
245
246
|
|
|
246
|
-
js_response =
|
|
247
|
-
`
|
|
247
|
+
js_response = `
|
|
248
248
|
(async function() {
|
|
249
249
|
try {
|
|
250
250
|
return await #{ai_binding}.run(#{model}, #{js_inputs}, #{js_options});
|
|
@@ -252,17 +252,20 @@ module Cloudflare
|
|
|
252
252
|
#{Kernel}.$raise(#{err_klass}.$new(e && e.message ? e.message : String(e), Opal.hash({ model: #{model}, operation: 'speak' })));
|
|
253
253
|
}
|
|
254
254
|
})()
|
|
255
|
-
|
|
255
|
+
`
|
|
256
|
+
.__await__
|
|
256
257
|
|
|
257
|
-
content_type =
|
|
258
|
-
`#{js_response}.headers.get("content-type") || "application/octet-stream"`
|
|
258
|
+
content_type = `#{js_response}.headers.get("content-type") || "application/octet-stream"`
|
|
259
259
|
cache_control = `#{js_response}.headers.get("cache-control")`
|
|
260
260
|
BinaryBody.new(`#{js_response}.body`, content_type, cache_control)
|
|
261
261
|
end
|
|
262
262
|
|
|
263
263
|
def self.speak_data_url(model, inputs, binding: nil, options: nil)
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
if defined?(Binding) &&
|
|
265
|
+
`(#{binding} != null && #{binding}.$$class === #{Binding})`
|
|
266
|
+
binding = binding.js
|
|
267
|
+
end
|
|
268
|
+
|
|
266
269
|
bound = !`(#{binding} == null)`
|
|
267
270
|
unless bound
|
|
268
271
|
raise AIError.new("AI binding not bound (env.AI is null)", model: model)
|
|
@@ -273,8 +276,7 @@ module Cloudflare
|
|
|
273
276
|
ai_binding = binding
|
|
274
277
|
err_klass = Cloudflare::AIError
|
|
275
278
|
|
|
276
|
-
js_response =
|
|
277
|
-
`
|
|
279
|
+
js_response = `
|
|
278
280
|
(async function() {
|
|
279
281
|
try {
|
|
280
282
|
return await #{ai_binding}.run(#{model}, #{js_inputs}, #{js_options});
|
|
@@ -282,10 +284,10 @@ module Cloudflare
|
|
|
282
284
|
#{Kernel}.$raise(#{err_klass}.$new(e && e.message ? e.message : String(e), Opal.hash({ model: #{model}, operation: 'speak_data_url' })));
|
|
283
285
|
}
|
|
284
286
|
})()
|
|
285
|
-
|
|
287
|
+
`
|
|
288
|
+
.__await__
|
|
286
289
|
|
|
287
|
-
content_type =
|
|
288
|
-
`#{js_response}.headers.get("content-type") || "application/octet-stream"`
|
|
290
|
+
content_type = `#{js_response}.headers.get("content-type") || "application/octet-stream"`
|
|
289
291
|
`
|
|
290
292
|
(async function(resp, ct) {
|
|
291
293
|
var buf = await resp.arrayBuffer();
|
|
@@ -294,19 +296,22 @@ module Cloudflare
|
|
|
294
296
|
for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
295
297
|
return 'data:' + ct + ';base64,' + globalThis.btoa(bin);
|
|
296
298
|
})(#{js_response}, #{content_type})
|
|
297
|
-
|
|
299
|
+
`
|
|
300
|
+
.__await__
|
|
298
301
|
end
|
|
299
302
|
|
|
300
303
|
def self.build_messages(prompt = nil, messages: nil, system: nil)
|
|
301
304
|
out = []
|
|
302
|
-
out << {
|
|
305
|
+
out << {role: "system", content: system.to_s} if system
|
|
303
306
|
if messages
|
|
304
307
|
unless messages.is_a?(Array)
|
|
305
308
|
raise ArgumentError, "messages must be an Array of chat messages"
|
|
306
309
|
end
|
|
310
|
+
|
|
307
311
|
out.concat(messages)
|
|
308
312
|
end
|
|
309
|
-
|
|
313
|
+
|
|
314
|
+
out << {role: "user", content: prompt.to_s} unless prompt.nil?
|
|
310
315
|
raise ArgumentError, "chat requires a prompt or messages" if out.empty?
|
|
311
316
|
out
|
|
312
317
|
end
|
|
@@ -316,10 +321,12 @@ module Cloudflare
|
|
|
316
321
|
uint8 = audio.to_uint8_array
|
|
317
322
|
return `Array.from(#{uint8})`
|
|
318
323
|
end
|
|
324
|
+
|
|
319
325
|
return audio if audio.is_a?(Array)
|
|
320
326
|
if `typeof #{audio} !== 'undefined' && #{audio} instanceof Uint8Array`
|
|
321
327
|
return `Array.from(#{audio})`
|
|
322
328
|
end
|
|
329
|
+
|
|
323
330
|
audio
|
|
324
331
|
end
|
|
325
332
|
|
|
@@ -327,12 +334,12 @@ module Cloudflare
|
|
|
327
334
|
return input_options unless model.to_s == DEFAULT_CHAT_MODEL
|
|
328
335
|
|
|
329
336
|
options = input_options.dup
|
|
330
|
-
key =
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
337
|
+
key = if options.key?(:chat_template_kwargs)
|
|
338
|
+
:chat_template_kwargs
|
|
339
|
+
elsif options.key?("chat_template_kwargs")
|
|
340
|
+
"chat_template_kwargs"
|
|
341
|
+
end
|
|
342
|
+
|
|
336
343
|
template_kwargs = key ? options[key] : nil
|
|
337
344
|
if template_kwargs && !template_kwargs.is_a?(Hash)
|
|
338
345
|
raise ArgumentError, "chat_template_kwargs must be a Hash"
|
|
@@ -354,18 +361,21 @@ module Cloudflare
|
|
|
354
361
|
text = message_hash_text(msg)
|
|
355
362
|
return text unless text.empty?
|
|
356
363
|
end
|
|
364
|
+
|
|
357
365
|
if out["messages"].is_a?(Array) && !out["messages"].empty?
|
|
358
|
-
msg =
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
366
|
+
msg = out["messages"].find do |entry|
|
|
367
|
+
entry.is_a?(Hash) && entry["role"].to_s == "assistant"
|
|
368
|
+
end ||
|
|
369
|
+
out["messages"][0]
|
|
362
370
|
text = message_hash_text(msg)
|
|
363
371
|
return text unless text.empty?
|
|
364
372
|
end
|
|
373
|
+
|
|
365
374
|
%w[text transcription response result output].each do |key|
|
|
366
375
|
value = message_content_text(out[key])
|
|
367
376
|
return value unless value.empty?
|
|
368
377
|
end
|
|
378
|
+
|
|
369
379
|
nested = out["result"]
|
|
370
380
|
return extract_text(nested) if nested.is_a?(Hash)
|
|
371
381
|
""
|
|
@@ -396,6 +406,7 @@ module Cloudflare
|
|
|
396
406
|
text = message_content_text(value[key])
|
|
397
407
|
return text unless text.empty?
|
|
398
408
|
end
|
|
409
|
+
|
|
399
410
|
""
|
|
400
411
|
end
|
|
401
412
|
|
|
@@ -405,6 +416,7 @@ module Cloudflare
|
|
|
405
416
|
if `#{val} != null && typeof #{val} === 'object' && #{val}.$$class == null`
|
|
406
417
|
return val
|
|
407
418
|
end
|
|
419
|
+
|
|
408
420
|
if val.is_a?(Hash)
|
|
409
421
|
obj = `({})`
|
|
410
422
|
val.each do |k, v|
|
|
@@ -412,6 +424,7 @@ module Cloudflare
|
|
|
412
424
|
jv = ruby_to_js(v)
|
|
413
425
|
`#{obj}[#{ks}] = #{jv}`
|
|
414
426
|
end
|
|
427
|
+
|
|
415
428
|
obj
|
|
416
429
|
elsif val.is_a?(Array)
|
|
417
430
|
arr = `([])`
|
|
@@ -419,6 +432,7 @@ module Cloudflare
|
|
|
419
432
|
jv = ruby_to_js(v)
|
|
420
433
|
`#{arr}.push(#{jv})`
|
|
421
434
|
end
|
|
435
|
+
|
|
422
436
|
arr
|
|
423
437
|
elsif val.is_a?(Symbol)
|
|
424
438
|
val.to_s
|
|
@@ -457,23 +471,23 @@ module Cloudflare
|
|
|
457
471
|
# this file still loads if stream.rb hasn't been required yet
|
|
458
472
|
# (Phase 11A load-order flip: ai.rb currently loads first).
|
|
459
473
|
def response_headers
|
|
460
|
-
defaults =
|
|
461
|
-
(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
)
|
|
474
|
+
defaults = (
|
|
475
|
+
if defined?(::Cloudflare::SSEStream)
|
|
476
|
+
::Cloudflare::SSEStream::DEFAULT_HEADERS
|
|
477
|
+
else
|
|
478
|
+
{
|
|
479
|
+
"content-type" => "text/event-stream; charset=utf-8",
|
|
480
|
+
"cache-control" => "no-cache, no-transform",
|
|
481
|
+
"x-accel-buffering" => "no"
|
|
482
|
+
}
|
|
483
|
+
end
|
|
484
|
+
)
|
|
472
485
|
defaults.merge(@extra_headers)
|
|
473
486
|
end
|
|
474
487
|
|
|
475
488
|
def each
|
|
476
489
|
end
|
|
490
|
+
|
|
477
491
|
def close
|
|
478
492
|
end
|
|
479
493
|
end
|
|
@@ -487,6 +501,7 @@ module Cloudflare
|
|
|
487
501
|
if `typeof #{js_val} === 'string' || typeof #{js_val} === 'number' || typeof #{js_val} === 'boolean'`
|
|
488
502
|
return js_val
|
|
489
503
|
end
|
|
504
|
+
|
|
490
505
|
if `Array.isArray(#{js_val})`
|
|
491
506
|
out = []
|
|
492
507
|
len = `#{js_val}.length`
|
|
@@ -495,8 +510,10 @@ module Cloudflare
|
|
|
495
510
|
out << js_to_ruby(`#{js_val}[#{i}]`)
|
|
496
511
|
i += 1
|
|
497
512
|
end
|
|
513
|
+
|
|
498
514
|
return out
|
|
499
515
|
end
|
|
516
|
+
|
|
500
517
|
if `typeof #{js_val} === 'object'`
|
|
501
518
|
h = {}
|
|
502
519
|
keys = `Object.keys(#{js_val})`
|
|
@@ -507,8 +524,10 @@ module Cloudflare
|
|
|
507
524
|
h[k] = js_to_ruby(`#{js_val}[#{k}]`)
|
|
508
525
|
i += 1
|
|
509
526
|
end
|
|
527
|
+
|
|
510
528
|
return h
|
|
511
529
|
end
|
|
530
|
+
|
|
512
531
|
js_val
|
|
513
532
|
end
|
|
514
533
|
end
|