arcane-os 0.43.0 → 0.44.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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/browser-runtime/ai/twin-cloud.mjs +234 -0
- package/docs/reference/ai/twin-cloud.md +124 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.44.0
|
|
4
|
+
|
|
5
|
+
- Add stateless `generateImages` through `arcane-os/ai/twin-cloud` with
|
|
6
|
+
caller-selected FAL Flux Schnell or Stable Diffusion 3.5 Large. Preserve
|
|
7
|
+
complete prompts and provider parameters, and return every image as a Blob
|
|
8
|
+
in provider order with declared media metadata.
|
|
9
|
+
- Expose semantic progress and complete diagnostic callbacks, asynchronous
|
|
10
|
+
job polling, and cancellation across credentials, callbacks, requests,
|
|
11
|
+
response bodies and media downloads. Submit a paid generation request once;
|
|
12
|
+
never automatically replay an ambiguous submission.
|
|
13
|
+
- Keep image model selection, credentials, display and storage with the
|
|
14
|
+
application. No global model default, saved-history migration, durable media
|
|
15
|
+
storage or change to existing chat and speech behavior is introduced.
|
|
16
|
+
|
|
3
17
|
## 0.43.0
|
|
4
18
|
|
|
5
19
|
- Add reusable rendered printing through `createPrintView`, preserving the
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles share the theme, packaging, event, cancellation,
|
|
20
20
|
and browser run contracts while retaining their selected application layout.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.
|
|
22
|
+
This checkout defines the `0.44.0` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -105,6 +105,240 @@ export function normalizeAIRequestAbort(error){
|
|
|
105
105
|
return normalized;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** Stateless image generation; model selection and provider parameters belong to the caller. */
|
|
109
|
+
export async function generateImages({
|
|
110
|
+
model,
|
|
111
|
+
prompt,
|
|
112
|
+
parameters = {},
|
|
113
|
+
twinKey,
|
|
114
|
+
getApiKey,
|
|
115
|
+
signal = null,
|
|
116
|
+
id = Date.now(),
|
|
117
|
+
onRequest = function observeImageRequest(){},
|
|
118
|
+
onResponse = function observeImageResponse(){},
|
|
119
|
+
onProgress = function observeImageProgress(){}
|
|
120
|
+
} = {}) {
|
|
121
|
+
if (signal?.aborted) throw normalizeAIRequestAbort(signal.reason);
|
|
122
|
+
const asynchronous = model === 'fal-ai/flux/schnell';
|
|
123
|
+
if (!asynchronous && model !== 'stable-diffusion-3.5-large') {
|
|
124
|
+
const error = new TypeError('TWiN generateImages requires a supported explicit image model.');
|
|
125
|
+
error.code = 'ARCANE_AI_IMAGE_MODEL_UNSUPPORTED';
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
if (!is.string(prompt)) throw new TypeError('TWiN generateImages requires a prompt string.');
|
|
129
|
+
if (!parameters || !is.object(parameters) || is.array(parameters)) {
|
|
130
|
+
throw new TypeError('Image parameters must be an object of additional provider fields.');
|
|
131
|
+
}
|
|
132
|
+
if (Object.hasOwn(parameters, 'prompt') || (!asynchronous && Object.hasOwn(parameters, 'model'))) {
|
|
133
|
+
throw new TypeError('Supply the image prompt and model through their named arguments.');
|
|
134
|
+
}
|
|
135
|
+
const destination = asynchronous
|
|
136
|
+
? 'https://inference.do-ai.run/v1/async-invoke'
|
|
137
|
+
: 'https://inference.do-ai.run/v1/images/generations';
|
|
138
|
+
const request = asynchronous
|
|
139
|
+
? {model_id: model, input: {...parameters, prompt}}
|
|
140
|
+
: {...parameters, model, prompt};
|
|
141
|
+
// Observe the complete request without allowing a later callback to rewrite its payload.
|
|
142
|
+
const body = JSON.stringify(request);
|
|
143
|
+
const outputFormat = parameters.output_format;
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const operationSignal = controller.signal;
|
|
146
|
+
function cancelImageRequest() {controller.abort(signal.reason);}
|
|
147
|
+
signal?.addEventListener('abort', cancelImageRequest, {once: true});
|
|
148
|
+
if (signal?.aborted) cancelImageRequest();
|
|
149
|
+
|
|
150
|
+
function progress(stage, requestId) {
|
|
151
|
+
return settleImageOperation(
|
|
152
|
+
function publishImageProgress() {
|
|
153
|
+
return onProgress({stage, model, id, ...(requestId === undefined ? {} : {requestId})});
|
|
154
|
+
},
|
|
155
|
+
operationSignal
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function inference(url, options) {
|
|
160
|
+
const response = await settleImageOperation(
|
|
161
|
+
function requestImageInference() {return fetch(url, {...options, credentials: 'omit', signal: operationSignal});},
|
|
162
|
+
operationSignal
|
|
163
|
+
);
|
|
164
|
+
const result = await settleImageOperation(
|
|
165
|
+
function readImageInference() {
|
|
166
|
+
if (response.ok || response.headers.get('content-type')?.includes('application/json')) {
|
|
167
|
+
return response.json();
|
|
168
|
+
}
|
|
169
|
+
return response.text();
|
|
170
|
+
},
|
|
171
|
+
operationSignal
|
|
172
|
+
);
|
|
173
|
+
if (!response.ok) throw result;
|
|
174
|
+
const retryAfter = response.headers.get('retry-after');
|
|
175
|
+
const seconds = retryAfter?.trim() ? Number(retryAfter) : NaN;
|
|
176
|
+
const date = retryAfter?.trim() ? Date.parse(retryAfter) : NaN;
|
|
177
|
+
const delay = is.finite(seconds) && seconds >= 0 ? seconds * 1000
|
|
178
|
+
: is.finite(date) ? Math.max(0, date - Date.now()) : 1000;
|
|
179
|
+
return {result, delay};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await progress('credentials');
|
|
184
|
+
const key = twinKey === undefined && is.function(getApiKey)
|
|
185
|
+
? await settleImageOperation(getApiKey, operationSignal) : twinKey;
|
|
186
|
+
if (!is.string(key) || !key) {
|
|
187
|
+
const error = new Error('AI provider is not configured.');
|
|
188
|
+
error.code = 'AI_PROVIDER_NOT_CONFIGURED';
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
const headers = {'Content-Type': 'application/json', Authorization: `Bearer ${key}`};
|
|
192
|
+
await settleImageOperation(
|
|
193
|
+
function observeImageSubmission() {
|
|
194
|
+
return onRequest(request, id, {operation: 'images', transport: 'http', destination});
|
|
195
|
+
},
|
|
196
|
+
operationSignal
|
|
197
|
+
);
|
|
198
|
+
await progress('requesting');
|
|
199
|
+
// A lost POST response can represent an accepted paid job. Never replay it.
|
|
200
|
+
let response = await inference(destination, {method: 'POST', headers, body});
|
|
201
|
+
let result = response.result;
|
|
202
|
+
if (asynchronous) {
|
|
203
|
+
const requestId = result?.request_id;
|
|
204
|
+
while (result?.status === 'QUEUED' || result?.status === 'IN_PROGRESS') {
|
|
205
|
+
if (!is.string(requestId) || !requestId) {
|
|
206
|
+
throw imageResultError('Image generation returned a pending job without a request_id.', result);
|
|
207
|
+
}
|
|
208
|
+
await progress(result.status === 'QUEUED' ? 'queued' : 'generating', requestId);
|
|
209
|
+
await waitForImageStatus(response.delay, operationSignal);
|
|
210
|
+
response = await inference(
|
|
211
|
+
`${destination}/${encodeURIComponent(requestId)}/status`,
|
|
212
|
+
{method: 'GET', headers}
|
|
213
|
+
);
|
|
214
|
+
result = response.result;
|
|
215
|
+
}
|
|
216
|
+
if (result?.status === 'FAILED') {
|
|
217
|
+
throw imageResultError('TWiN Cloud image generation failed.', result);
|
|
218
|
+
}
|
|
219
|
+
if (result?.status !== 'COMPLETED' && result?.status !== 'COMPLETE') {
|
|
220
|
+
throw imageResultError('TWiN Cloud returned an unsupported image job status.', result);
|
|
221
|
+
}
|
|
222
|
+
if (!is.array(result.output?.images)) {
|
|
223
|
+
if (!is.string(requestId) || !requestId) {
|
|
224
|
+
throw imageResultError('Image generation returned no result or request_id.', result);
|
|
225
|
+
}
|
|
226
|
+
response = await inference(`${destination}/${encodeURIComponent(requestId)}`, {method: 'GET', headers});
|
|
227
|
+
result = response.result;
|
|
228
|
+
if (result?.status !== 'COMPLETED' && result?.status !== 'COMPLETE') {
|
|
229
|
+
throw imageResultError('TWiN Cloud image generation did not return a completed result.', result);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const returnedImages = asynchronous ? result?.output?.images : result?.data;
|
|
234
|
+
if (!is.array(returnedImages)) {
|
|
235
|
+
throw imageResultError('TWiN Cloud returned no image collection.', result);
|
|
236
|
+
}
|
|
237
|
+
const images = returnedImages.map(function retainImageDescriptor(image) {return {...image};});
|
|
238
|
+
const format = result.output_format ?? outputFormat;
|
|
239
|
+
const requestId = asynchronous ? result.request_id : undefined;
|
|
240
|
+
await settleImageOperation(
|
|
241
|
+
function observeImageResult() {return onResponse(result, id, false);},
|
|
242
|
+
operationSignal
|
|
243
|
+
);
|
|
244
|
+
await progress('downloading', requestId);
|
|
245
|
+
const completed = await Promise.all(images.map(async function materializeImage(image) {
|
|
246
|
+
let blob;
|
|
247
|
+
const mediaType = image.content_type
|
|
248
|
+
?? ({png: 'image/png', jpeg: 'image/jpeg', jpg: 'image/jpeg', webp: 'image/webp'}[format])
|
|
249
|
+
?? 'application/octet-stream';
|
|
250
|
+
if (is.string(image.b64_json)) {
|
|
251
|
+
blob = await settleImageOperation(function decodeImageBase64() {
|
|
252
|
+
return new Blob([Uint8Array.from(atob(image.b64_json), function imageCode(character) {
|
|
253
|
+
return character.charCodeAt(0);
|
|
254
|
+
})], {type: mediaType});
|
|
255
|
+
}, operationSignal);
|
|
256
|
+
} else if (is.string(image.url) && image.url) {
|
|
257
|
+
// Media URLs have their own delivery authority. Never forward the inference key.
|
|
258
|
+
const download = await settleImageOperation(function downloadGeneratedImage() {
|
|
259
|
+
return fetch(image.url, {credentials: 'omit', signal: operationSignal});
|
|
260
|
+
}, operationSignal);
|
|
261
|
+
if (!download.ok) {
|
|
262
|
+
throw await settleImageOperation(function readImageDownloadError() {
|
|
263
|
+
return download.headers.get('content-type')?.includes('application/json')
|
|
264
|
+
? download.json() : download.text();
|
|
265
|
+
}, operationSignal);
|
|
266
|
+
}
|
|
267
|
+
blob = await settleImageOperation(function readGeneratedImage() {return download.blob();}, operationSignal);
|
|
268
|
+
if (!blob.type) blob = new Blob([blob], {type: mediaType});
|
|
269
|
+
} else {
|
|
270
|
+
throw imageResultError('TWiN Cloud returned an image without data or a URL.', image);
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
blob,
|
|
274
|
+
mediaType: blob.type || mediaType,
|
|
275
|
+
...(image.width === undefined ? {} : {width: image.width}),
|
|
276
|
+
...(image.height === undefined ? {} : {height: image.height})
|
|
277
|
+
};
|
|
278
|
+
}));
|
|
279
|
+
await progress('complete', requestId);
|
|
280
|
+
if (operationSignal.aborted) throw normalizeAIRequestAbort(operationSignal.reason);
|
|
281
|
+
return {images: completed};
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (isAIRequestAbort(error, operationSignal)) throw normalizeAIRequestAbort(error);
|
|
284
|
+
throw error;
|
|
285
|
+
} finally {
|
|
286
|
+
signal?.removeEventListener('abort', cancelImageRequest);
|
|
287
|
+
controller.abort('Image operation settled.');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function imageResultError(message, result) {
|
|
292
|
+
const error = new Error(message, {cause: result});
|
|
293
|
+
error.code = 'ARCANE_AI_INVALID_PROVIDER_RESULT';
|
|
294
|
+
return error;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Abort a wait even when a credential, callback, or host Fetch ignores the signal. */
|
|
298
|
+
function settleImageOperation(operation, signal) {
|
|
299
|
+
return new Promise(function settleCloudImageOperation(resolve, reject) {
|
|
300
|
+
let settled = false;
|
|
301
|
+
function finish(error, value, failed) {
|
|
302
|
+
if (settled) return;
|
|
303
|
+
settled = true;
|
|
304
|
+
signal.removeEventListener('abort', cancelled);
|
|
305
|
+
if (failed) reject(error);
|
|
306
|
+
else resolve(value);
|
|
307
|
+
}
|
|
308
|
+
function cancelled() {finish(normalizeAIRequestAbort(signal.reason), undefined, true);}
|
|
309
|
+
signal.addEventListener('abort', cancelled, {once: true});
|
|
310
|
+
if (signal.aborted) {
|
|
311
|
+
cancelled();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
Promise.resolve(operation()).then(
|
|
316
|
+
function accepted(value) {finish(undefined, value, false);},
|
|
317
|
+
function rejected(error) {finish(error, undefined, true);}
|
|
318
|
+
);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
finish(error, undefined, true);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function waitForImageStatus(delay, signal) {
|
|
326
|
+
let remaining = delay;
|
|
327
|
+
do {
|
|
328
|
+
// Split only at the host timer range; retain the provider's entire delay.
|
|
329
|
+
const milliseconds = Math.min(remaining, 2147483647);
|
|
330
|
+
let timer;
|
|
331
|
+
try {
|
|
332
|
+
await settleImageOperation(function waitForImagePoll() {
|
|
333
|
+
return new Promise(function scheduleImagePoll(finish) {timer = setTimeout(finish, milliseconds);});
|
|
334
|
+
}, signal);
|
|
335
|
+
} finally {
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
}
|
|
338
|
+
remaining -= milliseconds;
|
|
339
|
+
} while (remaining > 0);
|
|
340
|
+
}
|
|
341
|
+
|
|
108
342
|
export function structuredOutputFormat(value = false){
|
|
109
343
|
if(value === false || value === null || value === undefined){
|
|
110
344
|
return null;
|
|
@@ -136,6 +136,130 @@ options; they do not add a key, model, browser state, or retained conversation.
|
|
|
136
136
|
Retry warnings and observer failures use the shared console logger and preserve
|
|
137
137
|
the complete original error.
|
|
138
138
|
|
|
139
|
+
## Generate images with an application-selected model
|
|
140
|
+
|
|
141
|
+
`generateImages` is a separate stateless operation on the same public import.
|
|
142
|
+
The application owns the selected model, labels, prompt, provider parameters,
|
|
143
|
+
credential source, display, and any saved image. It does not instantiate the
|
|
144
|
+
browser AI singleton or change chat, speech, model preferences, or history.
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
import {generateImages} from 'arcane-os/ai/twin-cloud';
|
|
148
|
+
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const {images} = await generateImages({
|
|
151
|
+
model: 'stable-diffusion-3.5-large',
|
|
152
|
+
prompt: 'A moon-powered toaster launches croissants over purple craters.',
|
|
153
|
+
parameters: {n: 2, size: '1024x1024', output_format: 'png'},
|
|
154
|
+
getApiKey: applicationRuntime.getTwinKey,
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
onProgress({stage}) {
|
|
157
|
+
console.log(stage);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
for (const image of images) {
|
|
162
|
+
await applicationRuntime.saveGeneratedImage(image.blob, image.mediaType);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`applicationRuntime.getTwinKey` and `saveGeneratedImage` are caller-owned
|
|
167
|
+
functions in this example, not SDK APIs. Pass the credential function with any
|
|
168
|
+
required receiver already bound. Use `twinKey` instead when the application
|
|
169
|
+
already has its key. `getApiKey()` may return a key or a promise and is called
|
|
170
|
+
only when `twinKey` is `undefined`. An explicitly supplied empty key fails with
|
|
171
|
+
`AI_PROVIDER_NOT_CONFIGURED`; it does not select another credential source.
|
|
172
|
+
The example's model and parameters are application choices, not SDK defaults.
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
generateImages({
|
|
176
|
+
model,
|
|
177
|
+
prompt,
|
|
178
|
+
parameters,
|
|
179
|
+
twinKey,
|
|
180
|
+
getApiKey,
|
|
181
|
+
signal,
|
|
182
|
+
id,
|
|
183
|
+
onRequest,
|
|
184
|
+
onResponse,
|
|
185
|
+
onProgress
|
|
186
|
+
})
|
|
187
|
+
// Promise<{images: [{blob, mediaType, width?, height?}]}>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`parameters` defaults to `{}`, `signal` to `null`, and `id` to `Date.now()`.
|
|
191
|
+
All diagnostic and progress callbacks are optional.
|
|
192
|
+
|
|
193
|
+
The SDK owns the route for these exact supported identifiers:
|
|
194
|
+
|
|
195
|
+
| Explicit model | Submission and result |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `fal-ai/flux/schnell` | POST `/v1/async-invoke` with `{model_id:model,input:{...parameters,prompt}}`; pending jobs use GET `/v1/async-invoke/{request_id}/status`, then GET `/v1/async-invoke/{request_id}` when the completed status lacks `output.images`. |
|
|
198
|
+
| `stable-diffusion-3.5-large` | POST `/v1/images/generations` with `{...parameters,model,prompt}`; images come from the completed response's `data` array. |
|
|
199
|
+
|
|
200
|
+
Both routes use `https://inference.do-ai.run`. Missing or unsupported model
|
|
201
|
+
identifiers reject with `ARCANE_AI_IMAGE_MODEL_UNSUPPORTED`; there is no SDK
|
|
202
|
+
image model default. The prompt must be a string and is sent unchanged,
|
|
203
|
+
including whitespace. `parameters` is an object of additional provider fields;
|
|
204
|
+
the SDK neither supplies nor changes image counts, dimensions, quality, seeds,
|
|
205
|
+
or output limits. Supply the prompt only through `prompt`, and the synchronous
|
|
206
|
+
model only through `model`; duplicate reserved fields in `parameters` throw
|
|
207
|
+
`TypeError` instead of silently replacing one input. Provider-supported
|
|
208
|
+
parameter names and values remain that selected model's contract.
|
|
209
|
+
|
|
210
|
+
Every returned image is materialized in provider order. Base64 image data
|
|
211
|
+
becomes a `Blob`; returned URLs are fetched concurrently without an inference
|
|
212
|
+
Authorization header or browser credentials. The output's `mediaType` uses the
|
|
213
|
+
downloaded Blob's declared type, provider `content_type`, or a declared
|
|
214
|
+
`output_format` of `png`, `jpeg`/`jpg`, or `webp`. With no declared type or known
|
|
215
|
+
format, it is `application/octet-stream`; the SDK does not guess dimensions or
|
|
216
|
+
image metadata. `width` and `height` are included only when the provider supplies
|
|
217
|
+
them. The SDK saves nothing and creates no object URLs; the caller owns any
|
|
218
|
+
storage and the lifetime of display URLs it creates.
|
|
219
|
+
|
|
220
|
+
`onRequest(request,id,metadata)` runs once before submission with the complete
|
|
221
|
+
provider request. Metadata is `{operation:'images',transport:'http',destination}`.
|
|
222
|
+
`onResponse(response,id,false)` runs once with the complete final provider JSON
|
|
223
|
+
after successful generation and before image materialization. Intermediate
|
|
224
|
+
poll replies do not call it. These are explicit diagnostic callbacks: keep raw
|
|
225
|
+
protocol outside ordinary user status and durable chat history. Credentials are
|
|
226
|
+
transport-only and are absent from these payloads. The request and image
|
|
227
|
+
descriptors are captured before their diagnostic callbacks so observer edits do
|
|
228
|
+
not rewrite the operation's content.
|
|
229
|
+
|
|
230
|
+
`onProgress({stage,model,id,requestId?})` reports semantic stages: `credentials`,
|
|
231
|
+
`requesting`, `queued`, `generating`, `downloading`, and `complete`. Pending
|
|
232
|
+
stages can repeat; immediately completed jobs skip them. Async job stages
|
|
233
|
+
include `requestId` when present. Progress contains no image data or protocol
|
|
234
|
+
body. All three callbacks may return a promise; their settlement is awaited
|
|
235
|
+
and their failure rejects the operation. `onResponse` means generation returned
|
|
236
|
+
a final result, not that media downloads have finished. The resolved promise
|
|
237
|
+
delivers all materialized images; a failed download rejects instead of returning
|
|
238
|
+
an incomplete collection.
|
|
239
|
+
|
|
240
|
+
The image operation submits once and performs no automatic retry, including
|
|
241
|
+
after a network loss, HTTP `429`, body-read failure, or callback failure. A lost
|
|
242
|
+
POST response may correspond to an accepted paid job. This image-specific rule
|
|
243
|
+
does not alter chat or speech retry behavior. While a FAL job is `QUEUED` or
|
|
244
|
+
`IN_PROGRESS`, polling uses the provider's `Retry-After` delay when supplied,
|
|
245
|
+
otherwise one second. Completed jobs accept `COMPLETED` and the guide's
|
|
246
|
+
`COMPLETE` spelling. Failed or unusable job results reject with
|
|
247
|
+
`ARCANE_AI_INVALID_PROVIDER_RESULT` and the complete result in `error.cause`.
|
|
248
|
+
HTTP failures throw their complete parsed JSON or text body; transport,
|
|
249
|
+
decoding, and callback failures remain their original errors.
|
|
250
|
+
|
|
251
|
+
Calling `controller.abort()` cancels local credential/callback waits, requests,
|
|
252
|
+
body reads, polling timers, and downloads. It rejects with
|
|
253
|
+
`ARCANE_AI_REQUEST_ABORTED` and ignores later settlement from an operation that
|
|
254
|
+
does not honor the signal. A callback already running remains caller-owned;
|
|
255
|
+
the SDK invokes no subsequent callbacks after cancellation. Local cancellation
|
|
256
|
+
does not claim to cancel a remote paid job. Dispose the controller with the
|
|
257
|
+
owning page or operation; no global startup wait is required.
|
|
258
|
+
|
|
259
|
+
The route and response contracts follow DigitalOcean's official
|
|
260
|
+
[FAL inference guide](https://docs.digitalocean.com/products/inference/how-to/use-fal-models/)
|
|
261
|
+
and [multimodal inference guide](https://docs.digitalocean.com/products/inference/how-to/use-multimodal-inference/).
|
|
262
|
+
|
|
139
263
|
## Existing browser AI interface
|
|
140
264
|
|
|
141
265
|
The following browser example uses the same managed imports as the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
87
87
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
88
88
|
"test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
|
|
89
|
-
"test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
89
|
+
"test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
90
90
|
"test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
|
|
91
91
|
"test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
|
|
92
92
|
"check": "node tools/check-source.mjs && npm test",
|