arcane-os 0.77.0 → 0.79.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.
@@ -13,6 +13,10 @@ function cancellation(message) {
13
13
 
14
14
  function reportCleanupFailure(error) { console.error('Native decision cleanup failed.', error); }
15
15
 
16
+ function sameExecutionTarget(left, right) {
17
+ return left === right || (left != null && right != null && left.deviceId !== undefined && left.deviceId === right.deviceId);
18
+ }
19
+
16
20
  /**
17
21
  * One explicitly activated Laya FP32 model using an existing native ONNX owner.
18
22
  * It does not create another inference runtime or load on construction.
@@ -20,7 +24,7 @@ function reportCleanupFailure(error) { console.error('Native decision cleanup fa
20
24
  export function createNativeDecisionModel({
21
25
  onnx, modelAssets, workingDirectory, paths,
22
26
  model = 'onnx-community/laya-typed-decisions-ONNX', revision = 'main',
23
- dtype = 'fp32', sessionOptions, executionPreference = 'gpu', signal
27
+ dtype = 'fp32', sessionOptions, executionPreference = 'gpu', executionTarget, signal
24
28
  } = {}) {
25
29
  if (dtype !== 'fp32') throw new TypeError('This native Laya graph selection requires dtype fp32.');
26
30
  const owner = {};
@@ -33,14 +37,18 @@ export function createNativeDecisionModel({
33
37
  let progress = null;
34
38
  let lastError = null;
35
39
  let execution = null;
40
+ let selectedTarget = executionTarget;
41
+ let pendingActivation;
36
42
 
37
43
  function current() {
44
+ const pending = pendingActivation && (state !== 'ready' || pendingActivation.entry !== activation) ? pendingActivation : null;
38
45
  return {
39
46
  family: 'laya', model, revision, dtype, state,
40
- loaded: state === 'ready', busy: state === 'loading' || Boolean(activation?.jobs.size),
47
+ loaded: state === 'ready', busy: state === 'loading' || Boolean(pending) || Boolean(activation?.jobs.size),
41
48
  activeRequests: activation?.jobs.size ?? 0, progress,
42
49
  error: lastError ? serializeCoreError(lastError) : null,
43
- execution
50
+ execution,
51
+ pendingActivation: pending ? {executionTarget: pending.executionTarget} : null
44
52
  };
45
53
  }
46
54
 
@@ -141,8 +149,8 @@ export function createNativeDecisionModel({
141
149
  return {model: files['onnx/model.onnx'], tokenizer: files['tokenizer.json'], tokenizerConfig: files['tokenizer_config.json']};
142
150
  }
143
151
 
144
- function beginActivation() {
145
- const entry = {id: randomUUID(), controller: new AbortController(), jobs: new Set(), prepared: false, onnxLoadStarted: false};
152
+ function beginActivation(target) {
153
+ const entry = {id: randomUUID(), controller: new AbortController(), jobs: new Set(), prepared: false, onnxLoadStarted: false, executionTarget: target};
146
154
  activation = entry;
147
155
  state = 'loading';
148
156
  progress = {phase: 'preparing-model'};
@@ -161,7 +169,7 @@ export function createNativeDecisionModel({
161
169
  });
162
170
  entry.onnxLoadStarted = true;
163
171
  const loaded = onnx.load({
164
- id: entry.id, model: files.model, sessionOptions, executionPreference,
172
+ id: entry.id, model: files.model, sessionOptions, executionPreference, executionTarget: entry.executionTarget,
165
173
  signal: entry.controller.signal
166
174
  });
167
175
  const results = await Promise.allSettled([loaded, entry.tokenizer.ready]);
@@ -193,19 +201,76 @@ export function createNativeDecisionModel({
193
201
  return entry;
194
202
  }
195
203
 
196
- async function load({signal: operationSignal} = {}) {
197
- assertOpen(operationSignal);
198
- if (releaseTask && !activation) await releaseTask;
199
- assertOpen(operationSignal);
200
- const entry = activation ?? beginActivation();
204
+ async function awaitActivation(entry, operationSignal) {
201
205
  const detach = observeCancellation(entry, operationSignal);
202
206
  try {
203
207
  await entry.loading;
204
208
  entry.controller.signal.throwIfAborted();
209
+ if (activation !== entry) throw cancellation('The decision activation was replaced.');
205
210
  return current();
206
211
  } finally { detach(); }
207
212
  }
208
213
 
214
+ function observeLoad(request, operationSignal) {
215
+ function abort() { request.controller.abort(operationSignal.reason); }
216
+ operationSignal?.addEventListener('abort', abort, {once: true});
217
+ if (operationSignal?.aborted) abort();
218
+ return request.task.then(function loadedActivation() {
219
+ request.controller.signal.throwIfAborted();
220
+ request.entry.controller.signal.throwIfAborted();
221
+ if (activation !== request.entry) throw cancellation('The decision activation was replaced.');
222
+ return current();
223
+ }).finally(function detachLoadCaller() { operationSignal?.removeEventListener('abort', abort); });
224
+ }
225
+
226
+ async function load({signal: operationSignal, executionTarget: requestedTarget} = {}) {
227
+ assertOpen(operationSignal);
228
+ const target = requestedTarget === undefined ? selectedTarget : requestedTarget;
229
+ if (pendingActivation && !pendingActivation.controller.signal.aborted && sameExecutionTarget(pendingActivation.executionTarget, target)) {
230
+ return observeLoad(pendingActivation, operationSignal);
231
+ }
232
+ if (!pendingActivation && activation && sameExecutionTarget(activation.executionTarget, target)) {
233
+ return awaitActivation(activation, operationSignal);
234
+ }
235
+ const previous = pendingActivation;
236
+ const request = {executionTarget: target, controller: new AbortController()};
237
+ let precedingRelease;
238
+ // Retain the replacement before synchronous abort/state observers run.
239
+ // Only this model's retiring activation must finish before its successor.
240
+ request.task = Promise.resolve().then(async function loadSelectedTarget() {
241
+ request.controller.signal.throwIfAborted();
242
+ if (precedingRelease) await precedingRelease;
243
+ assertOpen(request.controller.signal);
244
+ if (pendingActivation !== request) throw cancellation('The decision activation request was replaced.');
245
+ request.entry ??= activation ?? beginActivation(request.executionTarget);
246
+ return awaitActivation(request.entry, request.controller.signal);
247
+ });
248
+ function settled(error) {
249
+ request.detach?.();
250
+ if (pendingActivation !== request) return;
251
+ pendingActivation = undefined;
252
+ if (!request.entry && !activation && !disposed && state === 'loading') {
253
+ state = request.controller.signal.aborted ? 'unloaded' : 'error';
254
+ progress = null;
255
+ lastError = request.controller.signal.aborted ? null : error;
256
+ }
257
+ publish();
258
+ }
259
+ request.task.then(function loaded() { settled(); }, function loadFailed(error) { settled(error); }).catch(reportCleanupFailure);
260
+ pendingActivation = request;
261
+ selectedTarget = target;
262
+ const reason = cancellation('The decision execution target was replaced.');
263
+ previous?.controller.abort(reason);
264
+ if (activation && !sameExecutionTarget(activation.executionTarget, target)) stop(activation, reason);
265
+ precedingRelease = !activation ? releaseTask : undefined;
266
+ if (!activation && !precedingRelease) {
267
+ request.entry = beginActivation(target);
268
+ request.detach = observeCancellation(request.entry, request.controller.signal);
269
+ }
270
+ publish();
271
+ return observeLoad(request, operationSignal);
272
+ }
273
+
209
274
  async function evaluate(rows, {signal: operationSignal, runOptions} = {}) {
210
275
  assertOpen(operationSignal);
211
276
  const entry = activation;
@@ -247,21 +312,34 @@ export function createNativeDecisionModel({
247
312
  }
248
313
 
249
314
  function unload() {
250
- return activation ? stop(activation, cancellation('The native decision model was unloaded.'))
251
- : releaseTask ?? Promise.resolve(current());
315
+ const reason = cancellation('The native decision model was unloaded.');
316
+ const pending = pendingActivation;
317
+ pending?.controller.abort(reason);
318
+ const release = activation ? stop(activation, reason) : releaseTask ?? Promise.resolve(current());
319
+ if (!pending) return release;
320
+ return Promise.allSettled([release, pending.task]).then(function releasedActivation(results) {
321
+ if (results[0].status === 'rejected') throw results[0].reason;
322
+ return current();
323
+ });
252
324
  }
253
325
 
254
326
  function dispose() {
255
327
  if (disposal) return disposal;
256
328
  disposed = true;
257
329
  signal?.removeEventListener('abort', lifetimeAborted);
330
+ const pending = pendingActivation;
331
+ const reason = cancellation('The native decision model was disposed.');
258
332
  let release;
259
333
  disposal = Promise.resolve().then(async function disposeNativeModel() {
260
- try { await release; }
334
+ try {
335
+ const results = await Promise.allSettled([release, pending?.task]);
336
+ if (results[0].status === 'rejected') throw results[0].reason;
337
+ }
261
338
  finally { state = 'disposed'; publish(); events.dispose(); }
262
339
  return current();
263
340
  });
264
- release = activation ? stop(activation, cancellation('The native decision model was disposed.')) : releaseTask;
341
+ pending?.controller.abort(reason);
342
+ release = activation ? stop(activation, reason) : releaseTask;
265
343
  if (!activation && !release) { state = 'disposed'; publish(); }
266
344
  return disposal;
267
345
  }
@@ -4,19 +4,23 @@ import {createLocalAIService} from '../core/services/local-ai.mjs';
4
4
  import {createLocalImageService} from '../core/services/image.mjs';
5
5
  import {createModelAssetService} from '../core/services/model-assets.mjs';
6
6
  import {createSpeechService} from '../core/services/speech.mjs';
7
+ import {createExecutionDeviceService} from '../core/services/execution-devices.mjs';
7
8
  import {normalizeLocalAIConfig} from './config.mjs';
8
9
  import {discoverLocalAIRuntimes} from './discover.mjs';
9
10
  import {ensureLocalAIRuntimes} from './install.mjs';
10
11
  import {createWhisperRuntime} from './whisper/index.mjs';
12
+ import {createExecutionDeviceCatalog} from './execution-devices.mjs';
11
13
 
12
14
  /** Local engines share the ordinary app-service development transport. */
13
15
  export function createDevelopmentLocalAI({
14
16
  config, appRoot, directory, application, version, signal, onEvent,
15
17
  services = [], serviceModules = [], context = {}
16
18
  } = {}) {
19
+ const selected = normalizeLocalAIConfig(config) ?? {runtimes: []};
17
20
  const lifetime = new AbortController();
18
21
  signal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
19
- const selected = normalizeLocalAIConfig(config) ?? {runtimes: []};
22
+ // Discovery starts on demand, independently of model runtime preparation.
23
+ const executionDevices = createExecutionDeviceCatalog({signal});
20
24
  const localConfig = {
21
25
  ...selected,
22
26
  runtimes: selected.runtimes.filter(function selectedChatRuntime(requirement) {
@@ -26,7 +30,7 @@ export function createDevelopmentLocalAI({
26
30
  const service = createLocalAIService(
27
31
  localConfig,
28
32
  {
29
- appRoot, signal, onEvent,
33
+ appRoot, signal, onEvent, executionDevices,
30
34
  async prepare({signal, onEvent: report}) {
31
35
  const discovered = await discoverLocalAIRuntimes({config: localConfig, appRoot, signal});
32
36
  const installed = await ensureLocalAIRuntimes(
@@ -36,7 +40,7 @@ export function createDevelopmentLocalAI({
36
40
  }
37
41
  }
38
42
  );
39
- const definitions = [service, ...services];
43
+ const definitions = [createExecutionDeviceService({catalog: executionDevices}), service, ...services];
40
44
  let modelAssets;
41
45
  let imageService;
42
46
  let speechService;