arcane-os 0.2.0 → 0.2.1
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 +18 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
- package/browser-runtime/ai/browser-speech-providers.mjs +330 -25
- package/docs/architecture.md +2 -2
- package/package.json +1 -1
- package/runtime/ARCANE_RUNTIME_RELEASE.json +11 -11
- package/runtime/arcane/components/chat.html +272 -1
- package/runtime/arcane/modules/AI.js +698 -17
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +93 -8
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +448 -24
- package/schemas/arcane-lock.schema.json +6 -4
- package/src/dev-server.mjs +29 -13
- package/src/doctor.mjs +1 -3
- package/src/import-map.mjs +134 -83
- package/src/packager/core.mjs +311 -39
- package/src/scaffold.mjs +45 -17
- package/src/templates/workspace-template.mjs +23 -4
- package/src/toolchain.mjs +10 -2
- package/src/workspace.mjs +177 -24
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import './DBOPFS.js';
|
|
2
2
|
import UserEntity from '../entities/User.js';
|
|
3
3
|
import {getAIPreferencesForRuntime} from './AIPreferenceRuntime.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
AI_MODEL_AUTHORITY_PROTOCOL,
|
|
6
|
+
AI_PROVIDER_PROTOCOL,
|
|
7
|
+
getAIProviderRuntime
|
|
8
|
+
} from './AIProviderRuntime.js';
|
|
5
9
|
import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
|
|
6
10
|
|
|
7
11
|
let credentials='include';
|
|
12
|
+
const LEGACY_TTS_RESPONSE_FORMAT='opus';
|
|
8
13
|
credentials='omit';
|
|
9
14
|
|
|
10
15
|
const LEGACY_AI_SERVICES=new Set(['OPENAI','OLLAMA','LOCAL_SPEACH']);
|
|
@@ -27,6 +32,148 @@ function normalizeAIRequestAbort(error){
|
|
|
27
32
|
return normalized;
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
function legacyAIProviderError(message,code,cause){
|
|
36
|
+
const error=cause===undefined
|
|
37
|
+
?new Error(message)
|
|
38
|
+
:new Error(message,{cause});
|
|
39
|
+
error.code=code;
|
|
40
|
+
return error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createLegacyAIStreamBridge(execute,sourceSignal){
|
|
44
|
+
const controller=new AbortController();
|
|
45
|
+
const queue=[];
|
|
46
|
+
const waiters=[];
|
|
47
|
+
let complete=false;
|
|
48
|
+
let failure=null;
|
|
49
|
+
let detached=false;
|
|
50
|
+
|
|
51
|
+
function forwardLegacyAIStreamAbort(){
|
|
52
|
+
if(!controller.signal.aborted){
|
|
53
|
+
controller.abort(sourceSignal?.reason);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function detachLegacyAIStreamAbort(){
|
|
58
|
+
if(detached){
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
detached=true;
|
|
62
|
+
sourceSignal?.removeEventListener?.(
|
|
63
|
+
'abort',
|
|
64
|
+
forwardLegacyAIStreamAbort
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if(sourceSignal?.aborted){
|
|
69
|
+
forwardLegacyAIStreamAbort();
|
|
70
|
+
}else{
|
|
71
|
+
sourceSignal?.addEventListener?.(
|
|
72
|
+
'abort',
|
|
73
|
+
forwardLegacyAIStreamAbort,
|
|
74
|
+
{once:true}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function emitLegacyAIStreamChunk(chunk){
|
|
79
|
+
if(complete){
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const waiter=waiters.shift();
|
|
83
|
+
if(waiter){
|
|
84
|
+
waiter.resolve({value:chunk,done:false});
|
|
85
|
+
}else{
|
|
86
|
+
queue.push(chunk);
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function finishLegacyAIStream(error){
|
|
92
|
+
if(complete){
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
complete=true;
|
|
96
|
+
failure=error||null;
|
|
97
|
+
detachLegacyAIStreamAbort();
|
|
98
|
+
while(waiters.length){
|
|
99
|
+
const waiter=waiters.shift();
|
|
100
|
+
if(failure){
|
|
101
|
+
waiter.reject(failure);
|
|
102
|
+
}else{
|
|
103
|
+
waiter.resolve({value:undefined,done:true});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result=Promise.resolve().then(
|
|
109
|
+
function executeLegacyAIStream(){
|
|
110
|
+
if(controller.signal.aborted){
|
|
111
|
+
throw normalizeAIRequestAbort(controller.signal.reason);
|
|
112
|
+
}
|
|
113
|
+
return execute({
|
|
114
|
+
emit:emitLegacyAIStreamChunk,
|
|
115
|
+
signal:controller.signal
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
).then(
|
|
119
|
+
function acceptLegacyAIStreamResult(value){
|
|
120
|
+
finishLegacyAIStream(null);
|
|
121
|
+
return value;
|
|
122
|
+
},
|
|
123
|
+
function rejectLegacyAIStreamResult(error){
|
|
124
|
+
const normalized=isAIRequestAbort(error,controller.signal)
|
|
125
|
+
?normalizeAIRequestAbort(error)
|
|
126
|
+
:error;
|
|
127
|
+
finishLegacyAIStream(normalized);
|
|
128
|
+
throw normalized;
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
result.catch(function retainLegacyAIStreamFailure() {});
|
|
132
|
+
|
|
133
|
+
async function cancelLegacyAIStream(reason){
|
|
134
|
+
if(!controller.signal.aborted){
|
|
135
|
+
controller.abort(reason);
|
|
136
|
+
}
|
|
137
|
+
await result.catch(function retainCancelledLegacyAIStream() {});
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handle={
|
|
142
|
+
result,
|
|
143
|
+
cancel:cancelLegacyAIStream,
|
|
144
|
+
next:function readLegacyAIStreamChunk(){
|
|
145
|
+
if(queue.length){
|
|
146
|
+
return Promise.resolve({value:queue.shift(),done:false});
|
|
147
|
+
}
|
|
148
|
+
if(complete){
|
|
149
|
+
return failure
|
|
150
|
+
?Promise.reject(failure)
|
|
151
|
+
:Promise.resolve({value:undefined,done:true});
|
|
152
|
+
}
|
|
153
|
+
return new Promise(function waitForLegacyAIStreamChunk(resolve,reject){
|
|
154
|
+
waiters.push({resolve,reject});
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
return:async function returnLegacyAIStream(value){
|
|
158
|
+
await cancelLegacyAIStream(
|
|
159
|
+
legacyAIProviderError(
|
|
160
|
+
'The legacy AI stream consumer stopped before completion.',
|
|
161
|
+
'ARCANE_AI_REQUEST_ABORTED'
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
return {value,done:true};
|
|
165
|
+
},
|
|
166
|
+
throw:async function throwLegacyAIStream(error){
|
|
167
|
+
await cancelLegacyAIStream(error);
|
|
168
|
+
throw error;
|
|
169
|
+
},
|
|
170
|
+
[Symbol.asyncIterator]:function iterateLegacyAIStream(){
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
return Object.freeze(handle);
|
|
175
|
+
}
|
|
176
|
+
|
|
30
177
|
function normalizeAIStartupOptions(options){
|
|
31
178
|
if(options===undefined){
|
|
32
179
|
return Object.freeze({startMuted:true,signal:null});
|
|
@@ -190,9 +337,21 @@ class AI {
|
|
|
190
337
|
this.setAI(
|
|
191
338
|
...preferences
|
|
192
339
|
);
|
|
340
|
+
|
|
341
|
+
const runtime=this;
|
|
342
|
+
globalThis.addEventListener?.(
|
|
343
|
+
'arcane-ollama-ready',
|
|
344
|
+
function reconcileLegacyOllamaReadiness(){
|
|
345
|
+
runtime.#retainLegacyLLMReadiness(
|
|
346
|
+
runtime.#reconcileLegacyLLMReadiness()
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
);
|
|
193
350
|
}
|
|
194
351
|
|
|
195
352
|
#providerRuntime=getAIProviderRuntime();
|
|
353
|
+
#legacyLLMProviders=new Map();
|
|
354
|
+
#legacyLLMReadiness=Promise.resolve(null);
|
|
196
355
|
#preferenceTuple=Object.freeze([
|
|
197
356
|
'OPENAI',
|
|
198
357
|
'OPENAI',
|
|
@@ -240,11 +399,311 @@ class AI {
|
|
|
240
399
|
|
|
241
400
|
set license(value){
|
|
242
401
|
this.#license=typeof value==='string' ? value.trim():'';
|
|
402
|
+
this.#retainLegacyLLMReadiness(
|
|
403
|
+
this.#reconcileLegacyLLMReadiness()
|
|
404
|
+
);
|
|
243
405
|
return this.#license;
|
|
244
406
|
}
|
|
245
407
|
|
|
408
|
+
#legacyLLMCapability(providerId){
|
|
409
|
+
if(providerId==='OPENAI'){
|
|
410
|
+
return this.llmService==='OPENAI'
|
|
411
|
+
&&Boolean(this.model)
|
|
412
|
+
&&Boolean(this.license)
|
|
413
|
+
&&typeof globalThis.fetch==='function';
|
|
414
|
+
}
|
|
415
|
+
if(providerId==='OLLAMA'){
|
|
416
|
+
return this.llmService==='OLLAMA'
|
|
417
|
+
&&Boolean(this.model)
|
|
418
|
+
&&Boolean(this.#nativeOllama());
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#legacyLLMInspection(providerId,selection){
|
|
424
|
+
const localOnly=providerId==='OLLAMA';
|
|
425
|
+
if(!selection
|
|
426
|
+
||selection.providerId!==providerId
|
|
427
|
+
||selection.modelId!==this.model
|
|
428
|
+
||selection.localOnly!==localOnly
|
|
429
|
+
||this.llmService!==providerId){
|
|
430
|
+
return Object.freeze({
|
|
431
|
+
available:false,
|
|
432
|
+
code:'ARCANE_AI_MODEL_AUTHORITY_REQUIRED',
|
|
433
|
+
message:'The selected legacy LLM route does not match the active AI configuration.'
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if(!this.#legacyLLMCapability(providerId)){
|
|
437
|
+
return Object.freeze({
|
|
438
|
+
available:false,
|
|
439
|
+
code:providerId==='OLLAMA'
|
|
440
|
+
?'AI_NATIVE_LOCAL_REQUIRED'
|
|
441
|
+
:'AI_PROVIDER_NOT_CONFIGURED',
|
|
442
|
+
message:providerId==='OLLAMA'
|
|
443
|
+
?'Local AI requires the capability-gated Arcane API.'
|
|
444
|
+
:'AI provider is not configured.'
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return Object.freeze({
|
|
448
|
+
available:true,
|
|
449
|
+
authority:Object.freeze({
|
|
450
|
+
protocol:AI_MODEL_AUTHORITY_PROTOCOL,
|
|
451
|
+
providerId,
|
|
452
|
+
modelId:selection.modelId,
|
|
453
|
+
admitted:true
|
|
454
|
+
})
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#createLegacyLLMProvider(providerId){
|
|
459
|
+
const runtime=this;
|
|
460
|
+
const localOnly=providerId==='OLLAMA';
|
|
461
|
+
let state='unloaded';
|
|
462
|
+
let busy=false;
|
|
463
|
+
|
|
464
|
+
function statusLegacyLLMProvider(){
|
|
465
|
+
if(state==='ready'
|
|
466
|
+
&&!busy
|
|
467
|
+
&&!runtime.#legacyLLMCapability(providerId)){
|
|
468
|
+
state='unloaded';
|
|
469
|
+
}
|
|
470
|
+
return Object.freeze({
|
|
471
|
+
state,
|
|
472
|
+
loaded:state==='ready',
|
|
473
|
+
busy
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function assertLegacyLLMSelection(selection){
|
|
478
|
+
const inspection=runtime.#legacyLLMInspection(
|
|
479
|
+
providerId,
|
|
480
|
+
selection
|
|
481
|
+
);
|
|
482
|
+
if(!inspection.available){
|
|
483
|
+
throw legacyAIProviderError(
|
|
484
|
+
inspection.message,
|
|
485
|
+
inspection.code
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
return inspection;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function releaseLegacyLLMRequest(){
|
|
492
|
+
busy=false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return Object.freeze({
|
|
496
|
+
protocol:AI_PROVIDER_PROTOCOL,
|
|
497
|
+
role:'llm',
|
|
498
|
+
id:providerId,
|
|
499
|
+
localOnly,
|
|
500
|
+
catalog:function catalogLegacyLLMProvider(){
|
|
501
|
+
if(runtime.llmService!==providerId||!runtime.model){
|
|
502
|
+
return Object.freeze([]);
|
|
503
|
+
}
|
|
504
|
+
return Object.freeze([
|
|
505
|
+
Object.freeze({id:runtime.model})
|
|
506
|
+
]);
|
|
507
|
+
},
|
|
508
|
+
inspect:function inspectLegacyLLMProvider(selection,{signal}={}){
|
|
509
|
+
if(signal?.aborted){
|
|
510
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
511
|
+
}
|
|
512
|
+
return runtime.#legacyLLMInspection(providerId,selection);
|
|
513
|
+
},
|
|
514
|
+
status:statusLegacyLLMProvider,
|
|
515
|
+
load:function loadLegacyLLMProvider(context={}){
|
|
516
|
+
if(context.signal?.aborted){
|
|
517
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
518
|
+
}
|
|
519
|
+
if(state==='disposed'){
|
|
520
|
+
throw legacyAIProviderError(
|
|
521
|
+
'The legacy LLM provider is disposed.',
|
|
522
|
+
'ARCANE_AI_PROVIDER_DISPOSED'
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
if(busy){
|
|
526
|
+
throw legacyAIProviderError(
|
|
527
|
+
'The legacy LLM provider owns an active request.',
|
|
528
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if(typeof context.progress!=='function'){
|
|
532
|
+
throw new TypeError(
|
|
533
|
+
'Legacy LLM provider load progress must be a function.'
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
const inspection=assertLegacyLLMSelection(context.selection);
|
|
537
|
+
state='loading';
|
|
538
|
+
context.progress({
|
|
539
|
+
phase:'capability',
|
|
540
|
+
completed:0,
|
|
541
|
+
total:1,
|
|
542
|
+
unit:'items',
|
|
543
|
+
heartbeat:false
|
|
544
|
+
});
|
|
545
|
+
if(context.signal?.aborted){
|
|
546
|
+
state='unloaded';
|
|
547
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
548
|
+
}
|
|
549
|
+
state='ready';
|
|
550
|
+
context.progress({
|
|
551
|
+
phase:'capability',
|
|
552
|
+
completed:1,
|
|
553
|
+
total:1,
|
|
554
|
+
unit:'items',
|
|
555
|
+
heartbeat:false
|
|
556
|
+
});
|
|
557
|
+
return Object.freeze({
|
|
558
|
+
authority:inspection.authority,
|
|
559
|
+
status:statusLegacyLLMProvider()
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
request:function requestLegacyLLMProvider(context={}){
|
|
563
|
+
if(context.signal?.aborted){
|
|
564
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
565
|
+
}
|
|
566
|
+
assertLegacyLLMSelection(context.selection);
|
|
567
|
+
const current=statusLegacyLLMProvider();
|
|
568
|
+
if(current.state!=='ready'||!current.loaded){
|
|
569
|
+
throw legacyAIProviderError(
|
|
570
|
+
'The legacy LLM provider is not ready.',
|
|
571
|
+
'ARCANE_AI_ROLE_NOT_READY'
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if(busy){
|
|
575
|
+
throw legacyAIProviderError(
|
|
576
|
+
'The legacy LLM provider owns an active request.',
|
|
577
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
busy=true;
|
|
581
|
+
if(context.operation==='chat'){
|
|
582
|
+
return Promise.resolve(
|
|
583
|
+
runtime.#requestLegacyLLMChat(
|
|
584
|
+
context.payload,
|
|
585
|
+
context.signal
|
|
586
|
+
)
|
|
587
|
+
).finally(releaseLegacyLLMRequest);
|
|
588
|
+
}
|
|
589
|
+
if(context.operation==='stream'){
|
|
590
|
+
const handle=createLegacyAIStreamBridge(
|
|
591
|
+
function executeLegacyLLMProviderStream(bridge){
|
|
592
|
+
return runtime.#requestLegacyLLMStream(
|
|
593
|
+
context.payload,
|
|
594
|
+
bridge
|
|
595
|
+
);
|
|
596
|
+
},
|
|
597
|
+
context.signal
|
|
598
|
+
);
|
|
599
|
+
handle.result.then(
|
|
600
|
+
releaseLegacyLLMRequest,
|
|
601
|
+
releaseLegacyLLMRequest
|
|
602
|
+
);
|
|
603
|
+
return handle;
|
|
604
|
+
}
|
|
605
|
+
busy=false;
|
|
606
|
+
throw legacyAIProviderError(
|
|
607
|
+
'The legacy LLM provider operation is unsupported.',
|
|
608
|
+
'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
|
|
609
|
+
);
|
|
610
|
+
},
|
|
611
|
+
unload:function unloadLegacyLLMProvider(context={}){
|
|
612
|
+
if(context.signal?.aborted){
|
|
613
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
614
|
+
}
|
|
615
|
+
state='unloaded';
|
|
616
|
+
busy=false;
|
|
617
|
+
return statusLegacyLLMProvider();
|
|
618
|
+
},
|
|
619
|
+
dispose:function disposeLegacyLLMProvider(context={}){
|
|
620
|
+
if(context.signal?.aborted){
|
|
621
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
622
|
+
}
|
|
623
|
+
state='disposed';
|
|
624
|
+
busy=false;
|
|
625
|
+
return statusLegacyLLMProvider();
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#ensureLegacyLLMProvider(providerId){
|
|
631
|
+
if(providerId!=='OPENAI'&&providerId!=='OLLAMA'){
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
if(this.#providerRuntime.hasProvider('llm',providerId)){
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
const provider=this.#createLegacyLLMProvider(providerId);
|
|
638
|
+
const unregister=this.#providerRuntime.register(provider);
|
|
639
|
+
this.#legacyLLMProviders.set(
|
|
640
|
+
providerId,
|
|
641
|
+
Object.freeze({provider,unregister})
|
|
642
|
+
);
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
#releaseInactiveLegacyLLMProviders(activeProviderId){
|
|
647
|
+
for(const [providerId,record] of this.#legacyLLMProviders){
|
|
648
|
+
if(providerId===activeProviderId){
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if(record.unregister()){
|
|
652
|
+
this.#legacyLLMProviders.delete(providerId);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#internalLegacyLLMSelection(localOnly=false){
|
|
658
|
+
const selection=this.#providerRuntime.selection(
|
|
659
|
+
'llm',
|
|
660
|
+
{localOnly}
|
|
661
|
+
);
|
|
662
|
+
if(!selection
|
|
663
|
+
||!this.#legacyLLMProviders.has(selection.providerId)
|
|
664
|
+
||selection.providerId!==this.llmService
|
|
665
|
+
||selection.modelId!==this.model){
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
return selection;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
#retainLegacyLLMReadiness(operation){
|
|
672
|
+
this.#legacyLLMReadiness=Promise.resolve(operation).catch(
|
|
673
|
+
function retainLegacyLLMReadinessFailure(){
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
);
|
|
677
|
+
return this.#legacyLLMReadiness;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
#reconcileLegacyLLMReadiness(){
|
|
681
|
+
const selection=this.#internalLegacyLLMSelection(false);
|
|
682
|
+
if(!selection){
|
|
683
|
+
return Promise.resolve(this.#providerRuntime.status('llm'));
|
|
684
|
+
}
|
|
685
|
+
const status=this.#providerRuntime.status('llm');
|
|
686
|
+
if(this.#legacyLLMCapability(selection.providerId)){
|
|
687
|
+
if(status.state==='ready'&&status.loaded===true){
|
|
688
|
+
return Promise.resolve(status);
|
|
689
|
+
}
|
|
690
|
+
return this.#providerRuntime.load('llm');
|
|
691
|
+
}
|
|
692
|
+
if(status.loaded===true
|
|
693
|
+
||status.busy===true
|
|
694
|
+
||status.state==='loading'
|
|
695
|
+
||status.state==='unloading'){
|
|
696
|
+
return this.#providerRuntime.unload('llm');
|
|
697
|
+
}
|
|
698
|
+
return Promise.resolve(status);
|
|
699
|
+
}
|
|
700
|
+
|
|
246
701
|
get configured(){
|
|
247
702
|
if(this.#usesProviderRuntime('llm',this.llmService)){
|
|
703
|
+
if(this.#internalLegacyLLMSelection(false)
|
|
704
|
+
&&!this.#legacyLLMCapability(this.llmService)){
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
248
707
|
const state=this.#providerRuntime.status('llm');
|
|
249
708
|
return state.state==='ready'&&state.loaded===true;
|
|
250
709
|
}
|
|
@@ -259,6 +718,19 @@ class AI {
|
|
|
259
718
|
|
|
260
719
|
#assertServiceConfigured(service=this.llmService,role='llm'){
|
|
261
720
|
if(this.#usesProviderRuntime(role,service)){
|
|
721
|
+
const internal=role==='llm'
|
|
722
|
+
?this.#internalLegacyLLMSelection(false)
|
|
723
|
+
:null;
|
|
724
|
+
if(internal&&!this.#legacyLLMCapability(internal.providerId)){
|
|
725
|
+
const inspection=this.#legacyLLMInspection(
|
|
726
|
+
internal.providerId,
|
|
727
|
+
internal
|
|
728
|
+
);
|
|
729
|
+
throw legacyAIProviderError(
|
|
730
|
+
inspection.message,
|
|
731
|
+
inspection.code
|
|
732
|
+
);
|
|
733
|
+
}
|
|
262
734
|
return true;
|
|
263
735
|
}
|
|
264
736
|
if(service==='OLLAMA'){
|
|
@@ -298,6 +770,11 @@ class AI {
|
|
|
298
770
|
}
|
|
299
771
|
|
|
300
772
|
#shouldUseProviderRuntime(role,service,localOnly=false){
|
|
773
|
+
// Legacy adapters publish lifecycle without replacing established
|
|
774
|
+
// public transport callbacks or their cancellation behavior.
|
|
775
|
+
if(role==='llm'&&this.#internalLegacyLLMSelection(localOnly)){
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
301
778
|
if(!localOnly){
|
|
302
779
|
return this.#usesProviderRuntime(role,service);
|
|
303
780
|
}
|
|
@@ -361,6 +838,19 @@ class AI {
|
|
|
361
838
|
}
|
|
362
839
|
}
|
|
363
840
|
|
|
841
|
+
#normalizedLLMModel(service,model){
|
|
842
|
+
if(service==='OLLAMA'){
|
|
843
|
+
const mappedModel=model==='OPENAI'?null:this.#models[model];
|
|
844
|
+
return mappedModel
|
|
845
|
+
||normalizeOllamaModelIdentifier(model)
|
|
846
|
+
||model;
|
|
847
|
+
}
|
|
848
|
+
if(service==='OPENAI'){
|
|
849
|
+
return this.#models.OPENAI;
|
|
850
|
+
}
|
|
851
|
+
return model;
|
|
852
|
+
}
|
|
853
|
+
|
|
364
854
|
#applyPreferenceTuple(tuple){
|
|
365
855
|
const [
|
|
366
856
|
llmService,
|
|
@@ -370,15 +860,10 @@ class AI {
|
|
|
370
860
|
modelTTS,
|
|
371
861
|
modelSTT
|
|
372
862
|
]=tuple;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
||normalizeOllamaModelIdentifier(model)
|
|
378
|
-
||model;
|
|
379
|
-
}else if(llmService==='OPENAI'){
|
|
380
|
-
normalizedLLMModel=this.#models.OPENAI;
|
|
381
|
-
}
|
|
863
|
+
const normalizedLLMModel=this.#normalizedLLMModel(
|
|
864
|
+
llmService,
|
|
865
|
+
model
|
|
866
|
+
);
|
|
382
867
|
this.llmService=llmService;
|
|
383
868
|
this.sttService=sttService;
|
|
384
869
|
this.ttsService=ttsService;
|
|
@@ -405,7 +890,10 @@ class AI {
|
|
|
405
890
|
|
|
406
891
|
#routesFromPreferenceTuple(tuple){
|
|
407
892
|
const roles={
|
|
408
|
-
llm:[
|
|
893
|
+
llm:[
|
|
894
|
+
tuple[0],
|
|
895
|
+
this.#normalizedLLMModel(tuple[0],tuple[3])
|
|
896
|
+
],
|
|
409
897
|
stt:[tuple[1],tuple[5]],
|
|
410
898
|
tts:[tuple[2],tuple[4]]
|
|
411
899
|
};
|
|
@@ -501,15 +989,30 @@ class AI {
|
|
|
501
989
|
modelSTT
|
|
502
990
|
]);
|
|
503
991
|
this.#assertValidProviderTuple(tuple);
|
|
992
|
+
this.#ensureLegacyLLMProvider(tuple[0]);
|
|
504
993
|
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
505
994
|
this.#applyPreferenceTuple(tuple);
|
|
995
|
+
this.#releaseInactiveLegacyLLMProviders(tuple[0]);
|
|
996
|
+
this.#retainLegacyLLMReadiness(
|
|
997
|
+
this.#reconcileLegacyLLMReadiness()
|
|
998
|
+
);
|
|
506
999
|
return true;
|
|
507
1000
|
}
|
|
508
1001
|
|
|
509
1002
|
configureProviders(selections){
|
|
510
|
-
this.#
|
|
511
|
-
|
|
1003
|
+
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
1004
|
+
this.#ensureLegacyLLMProvider(
|
|
1005
|
+
prepared.llm.default?.providerId
|
|
1006
|
+
);
|
|
1007
|
+
this.#assertRegisteredLegacyRoutes(prepared);
|
|
1008
|
+
const configured=this.#providerRuntime.configure(prepared);
|
|
512
1009
|
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
1010
|
+
this.#releaseInactiveLegacyLLMProviders(
|
|
1011
|
+
configured.llm.default?.providerId
|
|
1012
|
+
);
|
|
1013
|
+
this.#retainLegacyLLMReadiness(
|
|
1014
|
+
this.#reconcileLegacyLLMReadiness()
|
|
1015
|
+
);
|
|
513
1016
|
return configured;
|
|
514
1017
|
}
|
|
515
1018
|
|
|
@@ -532,18 +1035,28 @@ class AI {
|
|
|
532
1035
|
this.#assertValidProviderTuple(tuple);
|
|
533
1036
|
this.stopAudio();
|
|
534
1037
|
await this.#unloadProviderRolesForTransition();
|
|
1038
|
+
this.#ensureLegacyLLMProvider(tuple[0]);
|
|
535
1039
|
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
536
1040
|
this.#applyPreferenceTuple(tuple);
|
|
1041
|
+
this.#releaseInactiveLegacyLLMProviders(tuple[0]);
|
|
1042
|
+
await this.#reconcileLegacyLLMReadiness();
|
|
537
1043
|
return this.#providerRuntime.status();
|
|
538
1044
|
}
|
|
539
1045
|
|
|
540
1046
|
async transitionProviders(selections){
|
|
541
|
-
this.#assertRegisteredLegacyRoutes(selections);
|
|
542
1047
|
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
1048
|
+
this.#ensureLegacyLLMProvider(
|
|
1049
|
+
prepared.llm.default?.providerId
|
|
1050
|
+
);
|
|
1051
|
+
this.#assertRegisteredLegacyRoutes(prepared);
|
|
543
1052
|
this.stopAudio();
|
|
544
1053
|
await this.#unloadProviderRolesForTransition();
|
|
545
1054
|
const configured=this.#providerRuntime.configure(prepared);
|
|
546
1055
|
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
1056
|
+
this.#releaseInactiveLegacyLLMProviders(
|
|
1057
|
+
configured.llm.default?.providerId
|
|
1058
|
+
);
|
|
1059
|
+
await this.#reconcileLegacyLLMReadiness();
|
|
547
1060
|
return configured;
|
|
548
1061
|
}
|
|
549
1062
|
|
|
@@ -912,6 +1425,55 @@ class AI {
|
|
|
912
1425
|
return typeof content==='string'?content:completion;
|
|
913
1426
|
}
|
|
914
1427
|
|
|
1428
|
+
#requestLegacyLLMChat(payload={},signal=null){
|
|
1429
|
+
return this.#fetchLegacy(
|
|
1430
|
+
payload.messages??[],
|
|
1431
|
+
function ignoreLegacyLLMProviderResponse(){},
|
|
1432
|
+
payload.structuredOutput??false,
|
|
1433
|
+
payload.tools??[],
|
|
1434
|
+
payload.toolChoice??'auto',
|
|
1435
|
+
payload.parallelToolCalls??true,
|
|
1436
|
+
payload.id??Date.now(),
|
|
1437
|
+
function ignoreLegacyLLMProviderRequest(){},
|
|
1438
|
+
signal
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
#requestLegacyLLMStream(payload={},bridge){
|
|
1443
|
+
function emitLegacyLLMStreamText(text,id,thinking){
|
|
1444
|
+
if(typeof text!=='string'||!text){
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
bridge.emit(
|
|
1448
|
+
thinking
|
|
1449
|
+
?{thinking:text}
|
|
1450
|
+
:{content:text}
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function emitLegacyLLMStreamTool(name){
|
|
1455
|
+
if(typeof name==='string'&&name){
|
|
1456
|
+
bridge.emit({toolCalls:[{name}]});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
return this.#streamLegacyMessage(
|
|
1461
|
+
payload.messages??[],
|
|
1462
|
+
emitLegacyLLMStreamText,
|
|
1463
|
+
function ignoreLegacyLLMProviderCompletion(){},
|
|
1464
|
+
payload.tools??[],
|
|
1465
|
+
payload.toolChoice??'auto',
|
|
1466
|
+
emitLegacyLLMStreamTool,
|
|
1467
|
+
payload.parallelToolCalls??true,
|
|
1468
|
+
payload.id??Date.now(),
|
|
1469
|
+
payload.seeThinking??false,
|
|
1470
|
+
bridge.signal,
|
|
1471
|
+
function ignoreLegacyLLMProviderRequest(){},
|
|
1472
|
+
payload.structuredOutput??false,
|
|
1473
|
+
false
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
915
1477
|
#assertRequiredOllamaToolCall(toolCalls=[],toolChoice='auto'){
|
|
916
1478
|
const requiredName=toolChoice?.function?.name;
|
|
917
1479
|
|
|
@@ -1152,6 +1714,38 @@ class AI {
|
|
|
1152
1714
|
signal
|
|
1153
1715
|
});
|
|
1154
1716
|
}
|
|
1717
|
+
|
|
1718
|
+
return this.#streamLegacyMessage(
|
|
1719
|
+
messages,
|
|
1720
|
+
streamHandler,
|
|
1721
|
+
streamComplete,
|
|
1722
|
+
tools,
|
|
1723
|
+
tool_choice,
|
|
1724
|
+
earlyFunctionTrigger,
|
|
1725
|
+
parallel_tool_calls,
|
|
1726
|
+
id,
|
|
1727
|
+
seeThinking,
|
|
1728
|
+
signal,
|
|
1729
|
+
requestHandler,
|
|
1730
|
+
structuredOutput
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
async #streamLegacyMessage(
|
|
1735
|
+
messages=[],
|
|
1736
|
+
streamHandler=function ignoreStreamChunk(){},
|
|
1737
|
+
streamComplete=function finishIgnoredStream(){},
|
|
1738
|
+
tools=[],
|
|
1739
|
+
tool_choice='auto',
|
|
1740
|
+
earlyFunctionTrigger=function ignoreEarlyFunction(){},
|
|
1741
|
+
parallel_tool_calls=true,
|
|
1742
|
+
id=Date.now(),
|
|
1743
|
+
seeThinking=false,
|
|
1744
|
+
signal=null,
|
|
1745
|
+
requestHandler=function ignoreStreamRequest(){},
|
|
1746
|
+
structuredOutput=false,
|
|
1747
|
+
finishSpeech=true
|
|
1748
|
+
){
|
|
1155
1749
|
let speechTurnCompleted=false;
|
|
1156
1750
|
|
|
1157
1751
|
try{
|
|
@@ -1198,6 +1792,13 @@ class AI {
|
|
|
1198
1792
|
|
|
1199
1793
|
const nativeOllama=this.#nativeOllama();
|
|
1200
1794
|
|
|
1795
|
+
if(this.llmService==='OLLAMA'&&!nativeOllama){
|
|
1796
|
+
throw legacyAIProviderError(
|
|
1797
|
+
'Local AI requires the capability-gated Arcane API.',
|
|
1798
|
+
'AI_NATIVE_LOCAL_REQUIRED'
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1201
1802
|
if(nativeOllama){
|
|
1202
1803
|
let nativeContent='';
|
|
1203
1804
|
const nativeToolCalls={};
|
|
@@ -1294,7 +1895,9 @@ class AI {
|
|
|
1294
1895
|
if(Object.keys(nativeToolCalls).length&&!nativeContent){
|
|
1295
1896
|
streamHandler('',`M-${id}`,false);
|
|
1296
1897
|
}
|
|
1297
|
-
|
|
1898
|
+
if(finishSpeech){
|
|
1899
|
+
this.finishTTS();
|
|
1900
|
+
}
|
|
1298
1901
|
await streamComplete(nativeResult,`M-${id}`,isThinking);
|
|
1299
1902
|
speechTurnCompleted=true;
|
|
1300
1903
|
return nativeResult;
|
|
@@ -1496,7 +2099,9 @@ class AI {
|
|
|
1496
2099
|
if(Object.keys(tool_funcs).length&&!chunkString){
|
|
1497
2100
|
streamHandler('',`M-${id}`,false);
|
|
1498
2101
|
}
|
|
1499
|
-
|
|
2102
|
+
if(finishSpeech){
|
|
2103
|
+
this.finishTTS();
|
|
2104
|
+
}
|
|
1500
2105
|
await streamComplete(streamResult, `M-${id}`,isThinking);
|
|
1501
2106
|
|
|
1502
2107
|
//sync
|
|
@@ -1609,6 +2214,31 @@ class AI {
|
|
|
1609
2214
|
onResponse:responseHandler
|
|
1610
2215
|
});
|
|
1611
2216
|
}
|
|
2217
|
+
|
|
2218
|
+
return this.#fetchLegacy(
|
|
2219
|
+
messages,
|
|
2220
|
+
responseHandler,
|
|
2221
|
+
structuredOutput,
|
|
2222
|
+
tools,
|
|
2223
|
+
tool_choice,
|
|
2224
|
+
parallel_tool_calls,
|
|
2225
|
+
id,
|
|
2226
|
+
requestHandler,
|
|
2227
|
+
signal
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
async #fetchLegacy(
|
|
2232
|
+
messages=[],
|
|
2233
|
+
responseHandler=function ignoreFetchResponse(){},
|
|
2234
|
+
structuredOutput=false,
|
|
2235
|
+
tools=[],
|
|
2236
|
+
tool_choice='auto',
|
|
2237
|
+
parallel_tool_calls=true,
|
|
2238
|
+
id=Date.now(),
|
|
2239
|
+
requestHandler=function ignoreFetchRequest(){},
|
|
2240
|
+
signal=null,
|
|
2241
|
+
){
|
|
1612
2242
|
this.#assertServiceConfigured(this.llmService);
|
|
1613
2243
|
if(signal&&(
|
|
1614
2244
|
typeof signal.aborted!=='boolean'
|
|
@@ -1645,6 +2275,13 @@ class AI {
|
|
|
1645
2275
|
|
|
1646
2276
|
const nativeOllama=this.#nativeOllama();
|
|
1647
2277
|
|
|
2278
|
+
if(this.llmService==='OLLAMA'&&!nativeOllama){
|
|
2279
|
+
throw legacyAIProviderError(
|
|
2280
|
+
'Local AI requires the capability-gated Arcane API.',
|
|
2281
|
+
'AI_NATIVE_LOCAL_REQUIRED'
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
1648
2285
|
if(nativeOllama){
|
|
1649
2286
|
const ollamaTools=this.#ollamaTools(tools,tool_choice);
|
|
1650
2287
|
const ollamaMessages=this.#ollamaMessages(messages,tool_choice);
|
|
@@ -1931,6 +2568,7 @@ class AI {
|
|
|
1931
2568
|
async #requestSpeechAudio(job){
|
|
1932
2569
|
if(this.#usesProviderRuntime('tts',this.ttsService)){
|
|
1933
2570
|
job.abortController=new AbortController();
|
|
2571
|
+
const responseFormat=this.#providerSpeechResponseFormat();
|
|
1934
2572
|
const response=await this.#providerRuntime.request(
|
|
1935
2573
|
'tts',
|
|
1936
2574
|
{
|
|
@@ -1939,7 +2577,7 @@ class AI {
|
|
|
1939
2577
|
model:this.#providerRuntime.selection('tts')?.modelId,
|
|
1940
2578
|
voice:String(window.user?.AI_voice||'af_heart'),
|
|
1941
2579
|
input:job.text,
|
|
1942
|
-
responseFormat
|
|
2580
|
+
responseFormat,
|
|
1943
2581
|
speed:this.voiceSpeed
|
|
1944
2582
|
},
|
|
1945
2583
|
localOnly:false,
|
|
@@ -2028,6 +2666,49 @@ class AI {
|
|
|
2028
2666
|
return {chunks,type:this.audioType};
|
|
2029
2667
|
}
|
|
2030
2668
|
|
|
2669
|
+
#providerSpeechResponseFormat(){
|
|
2670
|
+
const selection=this.#providerRuntime.selection('tts');
|
|
2671
|
+
if(!selection){
|
|
2672
|
+
return this.audioFormat;
|
|
2673
|
+
}
|
|
2674
|
+
const provider=this.#providerRuntime.catalog('tts').find(
|
|
2675
|
+
entry=>entry.providerId===selection.providerId
|
|
2676
|
+
);
|
|
2677
|
+
const model=provider?.models.find(entry=>entry?.id===selection.modelId);
|
|
2678
|
+
const speech=model?.speech;
|
|
2679
|
+
if(speech===undefined){
|
|
2680
|
+
return this.audioFormat;
|
|
2681
|
+
}
|
|
2682
|
+
const prototype=speech&&typeof speech==='object'
|
|
2683
|
+
?Object.getPrototypeOf(speech)
|
|
2684
|
+
:null;
|
|
2685
|
+
const descriptors=prototype===Object.prototype||prototype===null
|
|
2686
|
+
?Object.getOwnPropertyDescriptors(speech)
|
|
2687
|
+
:null;
|
|
2688
|
+
const formats=descriptors?.responseFormats?.value;
|
|
2689
|
+
const defaultFormat=descriptors?.defaultResponseFormat?.value;
|
|
2690
|
+
if(!Array.isArray(formats)
|
|
2691
|
+
||formats.length<1
|
|
2692
|
+
||!formats.every(format=>typeof format==='string'&&format.trim()===format&&format)
|
|
2693
|
+
||typeof defaultFormat!=='string'
|
|
2694
|
+
||!formats.includes(defaultFormat)){
|
|
2695
|
+
throw legacyAIProviderError(
|
|
2696
|
+
'The selected TTS provider returned an invalid speech format catalog.',
|
|
2697
|
+
'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2700
|
+
if(formats.includes(this.audioFormat)){
|
|
2701
|
+
return this.audioFormat;
|
|
2702
|
+
}
|
|
2703
|
+
if(this.audioFormat===LEGACY_TTS_RESPONSE_FORMAT){
|
|
2704
|
+
return defaultFormat;
|
|
2705
|
+
}
|
|
2706
|
+
throw legacyAIProviderError(
|
|
2707
|
+
`The selected TTS provider does not support ${this.audioFormat}.`,
|
|
2708
|
+
'ARCANE_AI_UNSUPPORTED_RESPONSE_FORMAT'
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2031
2712
|
async #normalizeProviderSpeechAudio(response){
|
|
2032
2713
|
if(response instanceof Blob){
|
|
2033
2714
|
return {
|