arcane-os 0.1.2 → 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 +35 -0
- package/NOTICE +5 -3
- package/README.md +73 -24
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +67 -18
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +16 -5
- package/browser-runtime/ai/browser-kokoro-worker.mjs +3 -0
- package/browser-runtime/ai/browser-speech-artifacts.mjs +1108 -0
- package/browser-runtime/ai/browser-speech-providers.mjs +780 -0
- package/browser-runtime/ai/browser-speech.mjs +9 -0
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1537 -167
- package/browser-runtime/ai/browser-wasm.mjs +46 -1
- package/browser-runtime/ai/browser-whisper-worker.mjs +3 -0
- package/browser-runtime/ai/browser-wllama-runtime.mjs +677 -132
- package/browser-runtime/ai/model-controller.mjs +138 -12
- package/browser-runtime/ai/speech-worker-client.mjs +207 -0
- package/browser-runtime/ai/speech-worker-runtime.mjs +516 -0
- package/browser-runtime/ai/wllama/index.mjs +389 -0
- package/docs/architecture.md +132 -22
- package/docs/reference/README.md +1 -1
- package/docs/reference/ai/browser-wasm.md +101 -42
- package/docs/reference/availability-and-normalization.md +19 -5
- package/docs/reference/behavioral-testing.md +18 -5
- package/docs/reference/cli.md +2 -2
- package/docs/reference/inventory/package-api.json +14 -14
- package/docs/reference/protocols.md +4 -4
- package/docs/reference/sdk-api.md +68 -38
- package/docs/work-amplification.md +8 -4
- package/package.json +7 -3
- package/runtime/ARCANE_RUNTIME_RELEASE.json +50 -20
- package/runtime/arcane/components/chat.html +551 -62
- package/runtime/arcane/components/speech.html +1113 -265
- package/runtime/arcane/entities/Chat.js +246 -43
- package/runtime/arcane/modules/AI.js +1394 -162
- package/runtime/arcane/modules/AIProviderRuntime.js +2289 -0
- package/runtime/arcane/modules/AIRuntimeState.js +872 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +382 -31
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +1106 -0
- package/runtime/arcane/modules/DocumentLexicalSearch.js +292 -0
- package/runtime/arcane/modules/PersistentAIChatSession.js +268 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +25 -206
- package/schemas/arcane-lock.schema.json +10 -6
- package/src/cli/main.mjs +14 -2
- package/src/constants.mjs +1 -1
- package/src/dev-server.mjs +273 -26
- package/src/doctor.mjs +1 -3
- package/src/import-map.mjs +193 -84
- package/src/packager/core.mjs +313 -41
- package/src/runtime.mjs +14 -4
- package/src/scaffold.mjs +45 -17
- package/src/sdk-browser-runtime.mjs +28 -75
- package/src/templates/workspace-template.mjs +27 -8
- package/src/toolchain.mjs +13 -2
- package/src/workspace-runtime.mjs +1 -1
- package/src/workspace.mjs +178 -25
|
@@ -1,79 +1,219 @@
|
|
|
1
1
|
import './DBOPFS.js';
|
|
2
2
|
import UserEntity from '../entities/User.js';
|
|
3
3
|
import {getAIPreferencesForRuntime} from './AIPreferenceRuntime.js';
|
|
4
|
+
import {
|
|
5
|
+
AI_MODEL_AUTHORITY_PROTOCOL,
|
|
6
|
+
AI_PROVIDER_PROTOCOL,
|
|
7
|
+
getAIProviderRuntime
|
|
8
|
+
} from './AIProviderRuntime.js';
|
|
4
9
|
import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
|
|
5
10
|
|
|
6
11
|
let credentials='include';
|
|
12
|
+
const LEGACY_TTS_RESPONSE_FORMAT='opus';
|
|
7
13
|
credentials='omit';
|
|
8
14
|
|
|
9
|
-
const
|
|
10
|
-
'[Arcane AI] exact outbound inference request';
|
|
11
|
-
const ARCANE_AI_RESPONSE_DIAGNOSTIC_LABEL=
|
|
12
|
-
'[Arcane AI] exact inbound inference response';
|
|
13
|
-
const ARCANE_AI_DIAGNOSTIC_WARNING=
|
|
14
|
-
'may contain private conversation or document content.';
|
|
15
|
-
|
|
16
|
-
function snapshotAIConsolePayload(payload){
|
|
17
|
-
const serialized=JSON.stringify(payload);
|
|
18
|
-
return serialized===undefined?payload:JSON.parse(serialized);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function reportAIExchangeToConsole(label,{
|
|
22
|
-
id,
|
|
23
|
-
operation='',
|
|
24
|
-
service='',
|
|
25
|
-
transport='',
|
|
26
|
-
destination='',
|
|
27
|
-
payload
|
|
28
|
-
}={}){
|
|
29
|
-
try{
|
|
30
|
-
console.info(
|
|
31
|
-
`${label}; ${ARCANE_AI_DIAGNOSTIC_WARNING}`,
|
|
32
|
-
{
|
|
33
|
-
id,
|
|
34
|
-
operation,
|
|
35
|
-
service,
|
|
36
|
-
transport,
|
|
37
|
-
destination,
|
|
38
|
-
payload:snapshotAIConsolePayload(payload)
|
|
39
|
-
}
|
|
40
|
-
);
|
|
41
|
-
}catch(error){
|
|
42
|
-
console.warn('Arcane AI console instrumentation failed:',error);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function reportAIRequestToConsole({request={},...metadata}={}){
|
|
47
|
-
reportAIExchangeToConsole(
|
|
48
|
-
ARCANE_AI_REQUEST_DIAGNOSTIC_LABEL,
|
|
49
|
-
{...metadata,payload:request}
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function reportAIResponseToConsole({response,...metadata}={}){
|
|
54
|
-
reportAIExchangeToConsole(
|
|
55
|
-
ARCANE_AI_RESPONSE_DIAGNOSTIC_LABEL,
|
|
56
|
-
{...metadata,payload:response}
|
|
57
|
-
);
|
|
58
|
-
}
|
|
15
|
+
const LEGACY_AI_SERVICES=new Set(['OPENAI','OLLAMA','LOCAL_SPEACH']);
|
|
59
16
|
|
|
60
17
|
function isAIRequestAbort(error,signal){
|
|
61
18
|
return signal?.aborted
|
|
62
19
|
||error?.name==='AbortError'
|
|
63
20
|
||error?.code==='ARCANE_REQUEST_ABORTED'
|
|
21
|
+
||error?.code==='ARCANE_AI_REQUEST_ABORTED'
|
|
64
22
|
||error?.code==='AI_REQUEST_ABORTED';
|
|
65
23
|
}
|
|
66
24
|
|
|
67
25
|
function normalizeAIRequestAbort(error){
|
|
68
|
-
if(error?.code==='
|
|
26
|
+
if(error?.code==='ARCANE_AI_REQUEST_ABORTED'){
|
|
69
27
|
return error;
|
|
70
28
|
}
|
|
71
29
|
const normalized=new Error('The AI request was cancelled.',{cause:error});
|
|
72
30
|
normalized.name='AbortError';
|
|
73
|
-
normalized.code='
|
|
31
|
+
normalized.code='ARCANE_AI_REQUEST_ABORTED';
|
|
74
32
|
return normalized;
|
|
75
33
|
}
|
|
76
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
|
+
|
|
177
|
+
function normalizeAIStartupOptions(options){
|
|
178
|
+
if(options===undefined){
|
|
179
|
+
return Object.freeze({startMuted:true,signal:null});
|
|
180
|
+
}
|
|
181
|
+
if(!options||typeof options!=='object'||Array.isArray(options)){
|
|
182
|
+
throw new TypeError('AI startup options must be a plain object.');
|
|
183
|
+
}
|
|
184
|
+
const prototype=Object.getPrototypeOf(options);
|
|
185
|
+
if(prototype!==Object.prototype&&prototype!==null){
|
|
186
|
+
throw new TypeError('AI startup options must be a plain object.');
|
|
187
|
+
}
|
|
188
|
+
const descriptors=Object.getOwnPropertyDescriptors(options);
|
|
189
|
+
for(const key of Reflect.ownKeys(descriptors)){
|
|
190
|
+
if(typeof key==='symbol'||(key!=='startMuted'&&key!=='signal')){
|
|
191
|
+
throw new TypeError('AI startup options contain an unknown option.');
|
|
192
|
+
}
|
|
193
|
+
if(!Object.hasOwn(descriptors[key],'value')){
|
|
194
|
+
throw new TypeError(`AI startup options.${key} must be a data property.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const startMuted=Object.hasOwn(descriptors,'startMuted')
|
|
198
|
+
?descriptors.startMuted.value
|
|
199
|
+
:true;
|
|
200
|
+
const signal=Object.hasOwn(descriptors,'signal')
|
|
201
|
+
?descriptors.signal.value
|
|
202
|
+
:null;
|
|
203
|
+
if(typeof startMuted!=='boolean'){
|
|
204
|
+
throw new TypeError('AI startup startMuted must be a boolean.');
|
|
205
|
+
}
|
|
206
|
+
if(signal!==null&&signal!==undefined&&(
|
|
207
|
+
typeof signal!=='object'
|
|
208
|
+
||typeof signal.aborted!=='boolean'
|
|
209
|
+
||typeof signal.addEventListener!=='function'
|
|
210
|
+
||typeof signal.removeEventListener!=='function'
|
|
211
|
+
)){
|
|
212
|
+
throw new TypeError('AI startup signal must be an AbortSignal.');
|
|
213
|
+
}
|
|
214
|
+
return Object.freeze({startMuted,signal});
|
|
215
|
+
}
|
|
216
|
+
|
|
77
217
|
class AI {
|
|
78
218
|
// This is the enum section for inference configuration
|
|
79
219
|
#service = {
|
|
@@ -155,7 +295,7 @@ class AI {
|
|
|
155
295
|
}
|
|
156
296
|
|
|
157
297
|
ready=false;
|
|
158
|
-
muted=
|
|
298
|
+
muted=true;
|
|
159
299
|
|
|
160
300
|
|
|
161
301
|
llmService = '';
|
|
@@ -186,16 +326,45 @@ class AI {
|
|
|
186
326
|
return window.ai;
|
|
187
327
|
}
|
|
188
328
|
|
|
329
|
+
const preferences=[
|
|
330
|
+
llmService||'OPENAI',
|
|
331
|
+
sttService||'OPENAI',
|
|
332
|
+
ttsService||'OPENAI',
|
|
333
|
+
model||'OPENAI',
|
|
334
|
+
modelTTS||'OPENAI',
|
|
335
|
+
modelSTT||'OPENAI'
|
|
336
|
+
];
|
|
189
337
|
this.setAI(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
338
|
+
...preferences
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const runtime=this;
|
|
342
|
+
globalThis.addEventListener?.(
|
|
343
|
+
'arcane-ollama-ready',
|
|
344
|
+
function reconcileLegacyOllamaReadiness(){
|
|
345
|
+
runtime.#retainLegacyLLMReadiness(
|
|
346
|
+
runtime.#reconcileLegacyLLMReadiness()
|
|
347
|
+
);
|
|
348
|
+
}
|
|
196
349
|
);
|
|
197
350
|
}
|
|
198
351
|
|
|
352
|
+
#providerRuntime=getAIProviderRuntime();
|
|
353
|
+
#legacyLLMProviders=new Map();
|
|
354
|
+
#legacyLLMReadiness=Promise.resolve(null);
|
|
355
|
+
#preferenceTuple=Object.freeze([
|
|
356
|
+
'OPENAI',
|
|
357
|
+
'OPENAI',
|
|
358
|
+
'OPENAI',
|
|
359
|
+
'OPENAI',
|
|
360
|
+
'OPENAI',
|
|
361
|
+
'OPENAI'
|
|
362
|
+
]);
|
|
363
|
+
|
|
364
|
+
get providerRuntime(){
|
|
365
|
+
return this.#providerRuntime;
|
|
366
|
+
}
|
|
367
|
+
|
|
199
368
|
get url() {
|
|
200
369
|
return `${this.#service.baseURL[this.llmService]}${this.#paths.chat[this.llmService]}`
|
|
201
370
|
}
|
|
@@ -230,10 +399,314 @@ class AI {
|
|
|
230
399
|
|
|
231
400
|
set license(value){
|
|
232
401
|
this.#license=typeof value==='string' ? value.trim():'';
|
|
402
|
+
this.#retainLegacyLLMReadiness(
|
|
403
|
+
this.#reconcileLegacyLLMReadiness()
|
|
404
|
+
);
|
|
233
405
|
return this.#license;
|
|
234
406
|
}
|
|
235
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
|
+
|
|
236
701
|
get configured(){
|
|
702
|
+
if(this.#usesProviderRuntime('llm',this.llmService)){
|
|
703
|
+
if(this.#internalLegacyLLMSelection(false)
|
|
704
|
+
&&!this.#legacyLLMCapability(this.llmService)){
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
const state=this.#providerRuntime.status('llm');
|
|
708
|
+
return state.state==='ready'&&state.loaded===true;
|
|
709
|
+
}
|
|
237
710
|
if(this.llmService==='OLLAMA'){
|
|
238
711
|
return Boolean(this.model)&&Boolean(this.#nativeOllama());
|
|
239
712
|
}
|
|
@@ -243,9 +716,25 @@ class AI {
|
|
|
243
716
|
&&Boolean(this.license);
|
|
244
717
|
}
|
|
245
718
|
|
|
246
|
-
#assertServiceConfigured(service=this.llmService){
|
|
719
|
+
#assertServiceConfigured(service=this.llmService,role='llm'){
|
|
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
|
+
}
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
247
736
|
if(service==='OLLAMA'){
|
|
248
|
-
if(this.#nativeOllama()){
|
|
737
|
+
if(role==='llm'&&this.#nativeOllama()){
|
|
249
738
|
return true;
|
|
250
739
|
}
|
|
251
740
|
|
|
@@ -255,8 +744,16 @@ class AI {
|
|
|
255
744
|
error.code='AI_NATIVE_LOCAL_REQUIRED';
|
|
256
745
|
throw error;
|
|
257
746
|
}
|
|
258
|
-
if(service
|
|
259
|
-
|
|
747
|
+
if(service==='LOCAL_SPEACH'){
|
|
748
|
+
if(this.#nativeSpeech(service,role)){
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const error=new Error(
|
|
753
|
+
`Local ${role.toUpperCase()} requires the capability-gated Arcane API.`
|
|
754
|
+
);
|
|
755
|
+
error.code='AI_NATIVE_LOCAL_REQUIRED';
|
|
756
|
+
throw error;
|
|
260
757
|
}
|
|
261
758
|
|
|
262
759
|
if(service==='OPENAI'&&this.license){
|
|
@@ -268,6 +765,40 @@ class AI {
|
|
|
268
765
|
throw error;
|
|
269
766
|
}
|
|
270
767
|
|
|
768
|
+
#usesProviderRuntime(role,service){
|
|
769
|
+
return Boolean(this.#providerRuntime.selection(role));
|
|
770
|
+
}
|
|
771
|
+
|
|
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
|
+
}
|
|
778
|
+
if(!localOnly){
|
|
779
|
+
return this.#usesProviderRuntime(role,service);
|
|
780
|
+
}
|
|
781
|
+
const selection=this.#providerRuntime.selection(
|
|
782
|
+
role,
|
|
783
|
+
{localOnly:true}
|
|
784
|
+
);
|
|
785
|
+
return Boolean(selection);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
#hasLocalRoute(role,service){
|
|
789
|
+
const selection=this.#providerRuntime.selection(
|
|
790
|
+
role,
|
|
791
|
+
{localOnly:true}
|
|
792
|
+
);
|
|
793
|
+
if(selection){
|
|
794
|
+
return selection.localOnly===true;
|
|
795
|
+
}
|
|
796
|
+
if(this.#providerRuntime.selection(role)){
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
return role==='llm'&&service==='OLLAMA';
|
|
800
|
+
}
|
|
801
|
+
|
|
271
802
|
audioMessageChunks='';
|
|
272
803
|
sourceNodes=[];
|
|
273
804
|
isSpeaking=false;
|
|
@@ -282,6 +813,151 @@ class AI {
|
|
|
282
813
|
speechSynthesisTail=Promise.resolve();
|
|
283
814
|
speechUnlockHandler=null;
|
|
284
815
|
|
|
816
|
+
#nextPreferenceTuple(values){
|
|
817
|
+
const current=this.#preferenceTuple;
|
|
818
|
+
const next=values.map(function normalizeAIPreference(value,index){
|
|
819
|
+
if(value===undefined||value===null||value===''){
|
|
820
|
+
return current[index];
|
|
821
|
+
}
|
|
822
|
+
if(typeof value!=='string'||value.trim()!==value||!value){
|
|
823
|
+
throw new TypeError('AI preferences must be nonempty trimmed strings.');
|
|
824
|
+
}
|
|
825
|
+
return value;
|
|
826
|
+
});
|
|
827
|
+
return Object.freeze(next);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
#assertValidProviderTuple(tuple){
|
|
831
|
+
if(tuple[0]==='OLLAMA'){
|
|
832
|
+
const mappedModel=tuple[3]==='OPENAI'?null:this.#models[tuple[3]];
|
|
833
|
+
if(!mappedModel&&!normalizeOllamaModelIdentifier(tuple[3])){
|
|
834
|
+
const error=new TypeError('The Ollama model preference is invalid.');
|
|
835
|
+
error.code='AI_MODEL_INVALID';
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
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
|
+
|
|
854
|
+
#applyPreferenceTuple(tuple){
|
|
855
|
+
const [
|
|
856
|
+
llmService,
|
|
857
|
+
sttService,
|
|
858
|
+
ttsService,
|
|
859
|
+
model,
|
|
860
|
+
modelTTS,
|
|
861
|
+
modelSTT
|
|
862
|
+
]=tuple;
|
|
863
|
+
const normalizedLLMModel=this.#normalizedLLMModel(
|
|
864
|
+
llmService,
|
|
865
|
+
model
|
|
866
|
+
);
|
|
867
|
+
this.llmService=llmService;
|
|
868
|
+
this.sttService=sttService;
|
|
869
|
+
this.ttsService=ttsService;
|
|
870
|
+
this.model=normalizedLLMModel;
|
|
871
|
+
this.modelTTS=this.#ttsModels[modelTTS]||modelTTS;
|
|
872
|
+
this.modelSTT=this.#sttModels[modelSTT]||modelSTT;
|
|
873
|
+
this.reasoningEffort='';
|
|
874
|
+
this.#preferenceTuple=Object.freeze(tuple.slice());
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
#tupleFromProviderRoutes(selections){
|
|
878
|
+
const llm=selections.llm.default;
|
|
879
|
+
const stt=selections.stt.default;
|
|
880
|
+
const tts=selections.tts.default;
|
|
881
|
+
return Object.freeze([
|
|
882
|
+
llm?.providerId||'',
|
|
883
|
+
stt?.providerId||'',
|
|
884
|
+
tts?.providerId||'',
|
|
885
|
+
llm?.modelId||'',
|
|
886
|
+
tts?.modelId||'',
|
|
887
|
+
stt?.modelId||''
|
|
888
|
+
]);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
#routesFromPreferenceTuple(tuple){
|
|
892
|
+
const roles={
|
|
893
|
+
llm:[
|
|
894
|
+
tuple[0],
|
|
895
|
+
this.#normalizedLLMModel(tuple[0],tuple[3])
|
|
896
|
+
],
|
|
897
|
+
stt:[tuple[1],tuple[5]],
|
|
898
|
+
tts:[tuple[2],tuple[4]]
|
|
899
|
+
};
|
|
900
|
+
const selections={};
|
|
901
|
+
for(const role of ['llm','stt','tts']){
|
|
902
|
+
const [providerId,modelId]=roles[role];
|
|
903
|
+
const identity=providerId&&modelId
|
|
904
|
+
?this.#providerRuntime.providerIdentity(role,providerId)
|
|
905
|
+
:null;
|
|
906
|
+
const pendingNonLegacy=Boolean(
|
|
907
|
+
providerId
|
|
908
|
+
&&modelId
|
|
909
|
+
&&!LEGACY_AI_SERVICES.has(providerId)
|
|
910
|
+
);
|
|
911
|
+
if(!identity&&!pendingNonLegacy){
|
|
912
|
+
selections[role]={default:null,localOnly:null};
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const selection={
|
|
916
|
+
providerId,
|
|
917
|
+
modelId,
|
|
918
|
+
localOnly:identity?.localOnly??null
|
|
919
|
+
};
|
|
920
|
+
selections[role]={
|
|
921
|
+
default:selection,
|
|
922
|
+
localOnly:identity?.localOnly===true
|
|
923
|
+
?{...selection,localOnly:true}
|
|
924
|
+
:null
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
return selections;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
#assertRegisteredLegacyRoutes(selections){
|
|
931
|
+
for(const role of ['llm','stt','tts']){
|
|
932
|
+
for(const routeName of ['default','localOnly']){
|
|
933
|
+
const selection=selections?.[role]?.[routeName];
|
|
934
|
+
if(selection
|
|
935
|
+
&&LEGACY_AI_SERVICES.has(selection.providerId)
|
|
936
|
+
&&!this.#providerRuntime.hasProvider(role,selection.providerId)){
|
|
937
|
+
const error=new Error(
|
|
938
|
+
`Legacy AI provider ${selection.providerId} requires an explicit ${role} adapter before routing.`
|
|
939
|
+
);
|
|
940
|
+
error.code='ARCANE_AI_PROVIDER_UNAVAILABLE';
|
|
941
|
+
throw error;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
async #unloadProviderRolesForTransition(){
|
|
948
|
+
const settlements=await Promise.allSettled([
|
|
949
|
+
this.#providerRuntime.unload('llm'),
|
|
950
|
+
this.#providerRuntime.unload('stt'),
|
|
951
|
+
this.#providerRuntime.unload('tts')
|
|
952
|
+
]);
|
|
953
|
+
const failure=settlements.find(function findAITransitionCleanupFailure(result){
|
|
954
|
+
return result.status==='rejected';
|
|
955
|
+
});
|
|
956
|
+
if(failure){
|
|
957
|
+
throw failure.reason;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
285
961
|
// Set models to be used by the AI.
|
|
286
962
|
// Note: Only those that are defined are set.
|
|
287
963
|
setAI(
|
|
@@ -304,29 +980,107 @@ class AI {
|
|
|
304
980
|
) {
|
|
305
981
|
return false;
|
|
306
982
|
}
|
|
983
|
+
const tuple=this.#nextPreferenceTuple([
|
|
984
|
+
llmService,
|
|
985
|
+
sttService,
|
|
986
|
+
ttsService,
|
|
987
|
+
model,
|
|
988
|
+
modelTTS,
|
|
989
|
+
modelSTT
|
|
990
|
+
]);
|
|
991
|
+
this.#assertValidProviderTuple(tuple);
|
|
992
|
+
this.#ensureLegacyLLMProvider(tuple[0]);
|
|
993
|
+
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
994
|
+
this.#applyPreferenceTuple(tuple);
|
|
995
|
+
this.#releaseInactiveLegacyLLMProviders(tuple[0]);
|
|
996
|
+
this.#retainLegacyLLMReadiness(
|
|
997
|
+
this.#reconcileLegacyLLMReadiness()
|
|
998
|
+
);
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
307
1001
|
|
|
308
|
-
|
|
309
|
-
this.
|
|
310
|
-
this
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
1002
|
+
configureProviders(selections){
|
|
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);
|
|
1009
|
+
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
1010
|
+
this.#releaseInactiveLegacyLLMProviders(
|
|
1011
|
+
configured.llm.default?.providerId
|
|
1012
|
+
);
|
|
1013
|
+
this.#retainLegacyLLMReadiness(
|
|
1014
|
+
this.#reconcileLegacyLLMReadiness()
|
|
1015
|
+
);
|
|
1016
|
+
return configured;
|
|
1017
|
+
}
|
|
314
1018
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1019
|
+
async transitionAI(
|
|
1020
|
+
llmService,
|
|
1021
|
+
sttService,
|
|
1022
|
+
ttsService,
|
|
1023
|
+
model,
|
|
1024
|
+
modelTTS,
|
|
1025
|
+
modelSTT
|
|
1026
|
+
){
|
|
1027
|
+
const tuple=this.#nextPreferenceTuple([
|
|
1028
|
+
llmService,
|
|
1029
|
+
sttService,
|
|
1030
|
+
ttsService,
|
|
1031
|
+
model,
|
|
1032
|
+
modelTTS,
|
|
1033
|
+
modelSTT
|
|
1034
|
+
]);
|
|
1035
|
+
this.#assertValidProviderTuple(tuple);
|
|
1036
|
+
this.stopAudio();
|
|
1037
|
+
await this.#unloadProviderRolesForTransition();
|
|
1038
|
+
this.#ensureLegacyLLMProvider(tuple[0]);
|
|
1039
|
+
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
1040
|
+
this.#applyPreferenceTuple(tuple);
|
|
1041
|
+
this.#releaseInactiveLegacyLLMProviders(tuple[0]);
|
|
1042
|
+
await this.#reconcileLegacyLLMReadiness();
|
|
1043
|
+
return this.#providerRuntime.status();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async transitionProviders(selections){
|
|
1047
|
+
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
1048
|
+
this.#ensureLegacyLLMProvider(
|
|
1049
|
+
prepared.llm.default?.providerId
|
|
1050
|
+
);
|
|
1051
|
+
this.#assertRegisteredLegacyRoutes(prepared);
|
|
1052
|
+
this.stopAudio();
|
|
1053
|
+
await this.#unloadProviderRolesForTransition();
|
|
1054
|
+
const configured=this.#providerRuntime.configure(prepared);
|
|
1055
|
+
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
1056
|
+
this.#releaseInactiveLegacyLLMProviders(
|
|
1057
|
+
configured.llm.default?.providerId
|
|
1058
|
+
);
|
|
1059
|
+
await this.#reconcileLegacyLLMReadiness();
|
|
1060
|
+
return configured;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
async startProviders(options){
|
|
1064
|
+
const normalized=normalizeAIStartupOptions(options);
|
|
1065
|
+
this.muted=normalized.startMuted;
|
|
1066
|
+
if(normalized.startMuted){
|
|
1067
|
+
this.stopAudio();
|
|
324
1068
|
}
|
|
325
|
-
this.
|
|
326
|
-
|
|
327
|
-
this.modelSTT=this.#sttModels[modelSTT];
|
|
328
|
-
this.reasoningEffort='';
|
|
1069
|
+
return this.#providerRuntime.start(normalized);
|
|
1070
|
+
}
|
|
329
1071
|
|
|
1072
|
+
async setSpeechMuted(muted){
|
|
1073
|
+
if(typeof muted!=='boolean'){
|
|
1074
|
+
throw new TypeError('AI speech muted state must be a boolean.');
|
|
1075
|
+
}
|
|
1076
|
+
this.muted=muted;
|
|
1077
|
+
if(muted){
|
|
1078
|
+
this.stopAudio();
|
|
1079
|
+
}
|
|
1080
|
+
if(!this.#usesProviderRuntime('tts',this.ttsService)){
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
await this.#providerRuntime.setSpeechMuted(muted);
|
|
330
1084
|
return true;
|
|
331
1085
|
}
|
|
332
1086
|
|
|
@@ -376,14 +1130,19 @@ class AI {
|
|
|
376
1130
|
:null;
|
|
377
1131
|
}
|
|
378
1132
|
|
|
379
|
-
#nativeSpeech(service){
|
|
1133
|
+
#nativeSpeech(service,role){
|
|
380
1134
|
const client=globalThis.Arcane?.speech;
|
|
381
1135
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
1136
|
+
if(service!=='LOCAL_SPEACH'){
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
if(role==='tts'&&typeof client?.synthesize==='function'){
|
|
1140
|
+
return client;
|
|
1141
|
+
}
|
|
1142
|
+
if(role==='stt'&&typeof client?.transcribe==='function'){
|
|
1143
|
+
return client;
|
|
1144
|
+
}
|
|
1145
|
+
return null;
|
|
387
1146
|
}
|
|
388
1147
|
|
|
389
1148
|
async #androidNativeHost(){
|
|
@@ -589,34 +1348,130 @@ class AI {
|
|
|
589
1348
|
return null;
|
|
590
1349
|
}
|
|
591
1350
|
|
|
592
|
-
#reportRequest(requestHandler,request,id
|
|
1351
|
+
async #reportRequest(requestHandler,request,id){
|
|
593
1352
|
if(typeof requestHandler!=='function'){
|
|
594
|
-
throw new TypeError('AI
|
|
1353
|
+
throw new TypeError('AI onRequest callback must be a function.');
|
|
595
1354
|
}
|
|
1355
|
+
await requestHandler(request,id);
|
|
1356
|
+
}
|
|
596
1357
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1358
|
+
#providerStreamEmissions(chunk,seeThinking){
|
|
1359
|
+
const chunks=[];
|
|
1360
|
+
const toolNames=[];
|
|
1361
|
+
const choices=Array.isArray(chunk?.choices)?chunk.choices:[];
|
|
1362
|
+
for(const choice of choices){
|
|
1363
|
+
const delta=choice?.delta||{};
|
|
1364
|
+
if(seeThinking&&typeof delta.reasoning_content==='string'){
|
|
1365
|
+
chunks.push({text:delta.reasoning_content,thinking:true});
|
|
1366
|
+
}
|
|
1367
|
+
if(typeof delta.content==='string'){
|
|
1368
|
+
chunks.push({text:delta.content,thinking:false});
|
|
1369
|
+
}
|
|
1370
|
+
for(const call of Array.isArray(delta.tool_calls)?delta.tool_calls:[]){
|
|
1371
|
+
const name=call?.function?.name;
|
|
1372
|
+
if(typeof name==='string'&&name){
|
|
1373
|
+
toolNames.push(name);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if(!choices.length){
|
|
1378
|
+
if(seeThinking&&typeof chunk?.thinking==='string'){
|
|
1379
|
+
chunks.push({text:chunk.thinking,thinking:true});
|
|
1380
|
+
}
|
|
1381
|
+
const text=typeof chunk?.text==='string'
|
|
1382
|
+
?chunk.text
|
|
1383
|
+
:typeof chunk?.content==='string'
|
|
1384
|
+
?chunk.content
|
|
1385
|
+
:'';
|
|
1386
|
+
if(text){
|
|
1387
|
+
chunks.push({text,thinking:false});
|
|
1388
|
+
}
|
|
1389
|
+
const calls=Array.isArray(chunk?.toolCalls)
|
|
1390
|
+
?chunk.toolCalls
|
|
1391
|
+
:Array.isArray(chunk?.tool_calls)
|
|
1392
|
+
?chunk.tool_calls
|
|
1393
|
+
:[];
|
|
1394
|
+
for(const call of calls){
|
|
1395
|
+
const name=call?.function?.name||call?.name;
|
|
1396
|
+
if(typeof name==='string'&&name){
|
|
1397
|
+
toolNames.push(name);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
603
1400
|
}
|
|
1401
|
+
return {chunks,toolNames};
|
|
1402
|
+
}
|
|
604
1403
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1404
|
+
#providerCompletionOutput(completion){
|
|
1405
|
+
if(typeof completion==='string'){
|
|
1406
|
+
return completion;
|
|
1407
|
+
}
|
|
1408
|
+
const toolRecord={};
|
|
1409
|
+
let toolCount=0;
|
|
1410
|
+
for(const choice of Array.isArray(completion?.choices)?completion.choices:[]){
|
|
1411
|
+
for(const call of Array.isArray(choice?.message?.tool_calls)
|
|
1412
|
+
?choice.message.tool_calls
|
|
1413
|
+
:[]){
|
|
1414
|
+
const name=call?.function?.name;
|
|
1415
|
+
if(typeof name==='string'&&name){
|
|
1416
|
+
toolRecord[name]=call.function.arguments;
|
|
1417
|
+
toolCount+=1;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
if(toolCount){
|
|
1422
|
+
return toolRecord;
|
|
1423
|
+
}
|
|
1424
|
+
const content=completion?.choices?.[0]?.message?.content;
|
|
1425
|
+
return typeof content==='string'?content:completion;
|
|
611
1426
|
}
|
|
612
1427
|
|
|
613
|
-
#
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
+
);
|
|
620
1475
|
}
|
|
621
1476
|
|
|
622
1477
|
#assertRequiredOllamaToolCall(toolCalls=[],toolChoice='auto'){
|
|
@@ -701,13 +1556,116 @@ class AI {
|
|
|
701
1556
|
if(localOnly!==true&&localOnly!==false){
|
|
702
1557
|
throw new TypeError('AI localOnly must be a boolean.');
|
|
703
1558
|
}
|
|
704
|
-
if(localOnly
|
|
1559
|
+
if(localOnly&&!this.#hasLocalRoute('llm',this.llmService)){
|
|
705
1560
|
const error=new Error(
|
|
706
1561
|
'This AI request requires a configured local model.'
|
|
707
1562
|
);
|
|
708
1563
|
error.code='AI_LOCAL_MODEL_REQUIRED';
|
|
709
1564
|
throw error;
|
|
710
1565
|
}
|
|
1566
|
+
if(this.#shouldUseProviderRuntime('llm',this.llmService,localOnly)){
|
|
1567
|
+
const request={
|
|
1568
|
+
messages,
|
|
1569
|
+
structuredOutput,
|
|
1570
|
+
tools,
|
|
1571
|
+
toolChoice,
|
|
1572
|
+
parallelToolCalls,
|
|
1573
|
+
id,
|
|
1574
|
+
seeThinking
|
|
1575
|
+
};
|
|
1576
|
+
const displayId=`M-${id}`;
|
|
1577
|
+
const announcedTools=new Set();
|
|
1578
|
+
let handle=null;
|
|
1579
|
+
try{
|
|
1580
|
+
if(signal?.aborted){
|
|
1581
|
+
throw normalizeAIRequestAbort();
|
|
1582
|
+
}
|
|
1583
|
+
await this.#reportRequest(onRequest,request,id);
|
|
1584
|
+
if(signal?.aborted){
|
|
1585
|
+
throw normalizeAIRequestAbort();
|
|
1586
|
+
}
|
|
1587
|
+
handle=await this.#providerRuntime.request(
|
|
1588
|
+
'llm',
|
|
1589
|
+
{
|
|
1590
|
+
operation:'stream',
|
|
1591
|
+
payload:request,
|
|
1592
|
+
localOnly,
|
|
1593
|
+
signal
|
|
1594
|
+
}
|
|
1595
|
+
);
|
|
1596
|
+
for await(const chunk of handle){
|
|
1597
|
+
if(signal?.aborted){
|
|
1598
|
+
throw normalizeAIRequestAbort();
|
|
1599
|
+
}
|
|
1600
|
+
const emissions=this.#providerStreamEmissions(chunk,seeThinking);
|
|
1601
|
+
for(const emission of emissions.chunks){
|
|
1602
|
+
if(signal?.aborted){
|
|
1603
|
+
throw normalizeAIRequestAbort();
|
|
1604
|
+
}
|
|
1605
|
+
await onChunk(
|
|
1606
|
+
emission.text,
|
|
1607
|
+
displayId,
|
|
1608
|
+
emission.thinking
|
|
1609
|
+
);
|
|
1610
|
+
if(signal?.aborted){
|
|
1611
|
+
throw normalizeAIRequestAbort();
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
for(const name of emissions.toolNames){
|
|
1615
|
+
if(signal?.aborted){
|
|
1616
|
+
throw normalizeAIRequestAbort();
|
|
1617
|
+
}
|
|
1618
|
+
if(!announcedTools.has(name)){
|
|
1619
|
+
announcedTools.add(name);
|
|
1620
|
+
await onToolCall(name);
|
|
1621
|
+
if(signal?.aborted){
|
|
1622
|
+
throw normalizeAIRequestAbort();
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const completion=await handle.result;
|
|
1628
|
+
if(signal?.aborted){
|
|
1629
|
+
throw normalizeAIRequestAbort();
|
|
1630
|
+
}
|
|
1631
|
+
for(const choice of Array.isArray(completion?.choices)
|
|
1632
|
+
?completion.choices
|
|
1633
|
+
:[]){
|
|
1634
|
+
for(const call of Array.isArray(choice?.message?.tool_calls)
|
|
1635
|
+
?choice.message.tool_calls
|
|
1636
|
+
:[]){
|
|
1637
|
+
const name=call?.function?.name;
|
|
1638
|
+
if(typeof name==='string'&&name&&!announcedTools.has(name)){
|
|
1639
|
+
if(signal?.aborted){
|
|
1640
|
+
throw normalizeAIRequestAbort();
|
|
1641
|
+
}
|
|
1642
|
+
announcedTools.add(name);
|
|
1643
|
+
await onToolCall(name);
|
|
1644
|
+
if(signal?.aborted){
|
|
1645
|
+
throw normalizeAIRequestAbort();
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
const result=this.#providerCompletionOutput(completion);
|
|
1651
|
+
await onComplete(result,displayId,false);
|
|
1652
|
+
if(signal?.aborted){
|
|
1653
|
+
throw normalizeAIRequestAbort();
|
|
1654
|
+
}
|
|
1655
|
+
this.finishTTS();
|
|
1656
|
+
return result;
|
|
1657
|
+
}catch(error){
|
|
1658
|
+
if(handle){
|
|
1659
|
+
await handle.cancel(error).catch(
|
|
1660
|
+
function retainProviderStreamCleanupFailure() {}
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
this.stopAudio();
|
|
1664
|
+
throw isAIRequestAbort(error,signal)
|
|
1665
|
+
?normalizeAIRequestAbort(error)
|
|
1666
|
+
:error;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
711
1669
|
|
|
712
1670
|
return this.streamMessage(
|
|
713
1671
|
messages,
|
|
@@ -738,6 +1696,55 @@ class AI {
|
|
|
738
1696
|
signal=null,
|
|
739
1697
|
requestHandler=function ignoreStreamRequest(){},
|
|
740
1698
|
structuredOutput=false
|
|
1699
|
+
){
|
|
1700
|
+
if(this.#shouldUseProviderRuntime('llm',this.llmService,false)){
|
|
1701
|
+
return this.streamRequest({
|
|
1702
|
+
messages,
|
|
1703
|
+
structuredOutput,
|
|
1704
|
+
localOnly:false,
|
|
1705
|
+
onChunk:streamHandler,
|
|
1706
|
+
onComplete:streamComplete,
|
|
1707
|
+
tools,
|
|
1708
|
+
toolChoice:tool_choice,
|
|
1709
|
+
onToolCall:earlyFunctionTrigger,
|
|
1710
|
+
onRequest:requestHandler,
|
|
1711
|
+
parallelToolCalls:parallel_tool_calls,
|
|
1712
|
+
id,
|
|
1713
|
+
seeThinking,
|
|
1714
|
+
signal
|
|
1715
|
+
});
|
|
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
|
|
741
1748
|
){
|
|
742
1749
|
let speechTurnCompleted=false;
|
|
743
1750
|
|
|
@@ -785,10 +1792,16 @@ class AI {
|
|
|
785
1792
|
|
|
786
1793
|
const nativeOllama=this.#nativeOllama();
|
|
787
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
|
+
|
|
788
1802
|
if(nativeOllama){
|
|
789
1803
|
let nativeContent='';
|
|
790
1804
|
const nativeToolCalls={};
|
|
791
|
-
const nativeResponseChunks=[];
|
|
792
1805
|
const triggeredTools=new Set();
|
|
793
1806
|
const ollamaTools=this.#ollamaTools(tools,tool_choice);
|
|
794
1807
|
const ollamaMessages=this.#ollamaMessages(messages,tool_choice);
|
|
@@ -802,7 +1815,7 @@ class AI {
|
|
|
802
1815
|
};
|
|
803
1816
|
|
|
804
1817
|
function reportEarlyFunctionFailure(error){
|
|
805
|
-
console.error('Early tool trigger failed
|
|
1818
|
+
console.error('Early tool trigger failed.');
|
|
806
1819
|
}
|
|
807
1820
|
|
|
808
1821
|
function receiveNativeToolCalls(message={}){
|
|
@@ -832,7 +1845,7 @@ class AI {
|
|
|
832
1845
|
}
|
|
833
1846
|
}
|
|
834
1847
|
|
|
835
|
-
this.#reportRequest(requestHandler,ollamaRequest,id,{
|
|
1848
|
+
await this.#reportRequest(requestHandler,ollamaRequest,id,{
|
|
836
1849
|
operation:'stream',
|
|
837
1850
|
transport:'native',
|
|
838
1851
|
destination:'Arcane.ollama.chat'
|
|
@@ -844,7 +1857,6 @@ class AI {
|
|
|
844
1857
|
if(signal?.aborted){
|
|
845
1858
|
return;
|
|
846
1859
|
}
|
|
847
|
-
nativeResponseChunks.push(chunk);
|
|
848
1860
|
const message=chunk?.message||{};
|
|
849
1861
|
const thinking=seeThinking
|
|
850
1862
|
?String(message.thinking||'')
|
|
@@ -866,15 +1878,6 @@ class AI {
|
|
|
866
1878
|
signal
|
|
867
1879
|
}
|
|
868
1880
|
);
|
|
869
|
-
this.#reportResponse(
|
|
870
|
-
{chunks:nativeResponseChunks,final:nativeResponse},
|
|
871
|
-
id,
|
|
872
|
-
{
|
|
873
|
-
operation:'stream',
|
|
874
|
-
transport:'native',
|
|
875
|
-
destination:'Arcane.ollama.chat'
|
|
876
|
-
}
|
|
877
|
-
);
|
|
878
1881
|
if(signal?.aborted){
|
|
879
1882
|
throw normalizeAIRequestAbort();
|
|
880
1883
|
}
|
|
@@ -892,13 +1895,15 @@ class AI {
|
|
|
892
1895
|
if(Object.keys(nativeToolCalls).length&&!nativeContent){
|
|
893
1896
|
streamHandler('',`M-${id}`,false);
|
|
894
1897
|
}
|
|
895
|
-
|
|
1898
|
+
if(finishSpeech){
|
|
1899
|
+
this.finishTTS();
|
|
1900
|
+
}
|
|
896
1901
|
await streamComplete(nativeResult,`M-${id}`,isThinking);
|
|
897
1902
|
speechTurnCompleted=true;
|
|
898
1903
|
return nativeResult;
|
|
899
1904
|
}
|
|
900
1905
|
|
|
901
|
-
this.#reportRequest(requestHandler,request,id,{
|
|
1906
|
+
await this.#reportRequest(requestHandler,request,id,{
|
|
902
1907
|
operation:'stream',
|
|
903
1908
|
transport:'http',
|
|
904
1909
|
destination:this.url
|
|
@@ -921,7 +1926,7 @@ class AI {
|
|
|
921
1926
|
if(signal?.aborted||err?.name==='AbortError'){
|
|
922
1927
|
const error=new Error('The AI request was cancelled.',{cause:err});
|
|
923
1928
|
error.name='AbortError';
|
|
924
|
-
error.code='
|
|
1929
|
+
error.code='ARCANE_AI_REQUEST_ABORTED';
|
|
925
1930
|
throw error;
|
|
926
1931
|
}
|
|
927
1932
|
const error=new Error(
|
|
@@ -936,7 +1941,6 @@ class AI {
|
|
|
936
1941
|
|
|
937
1942
|
let chunkString='';
|
|
938
1943
|
let chunkCache='';
|
|
939
|
-
const responseEvents=[];
|
|
940
1944
|
const streamedToolCalls=new Map();
|
|
941
1945
|
const triggeredTools=new Set();
|
|
942
1946
|
const decoder = new TextDecoder('utf-8');
|
|
@@ -969,7 +1973,7 @@ class AI {
|
|
|
969
1973
|
Promise.resolve(
|
|
970
1974
|
earlyFunctionTrigger(record.name)
|
|
971
1975
|
).catch(
|
|
972
|
-
|
|
1976
|
+
()=>console.error('Early tool trigger failed.')
|
|
973
1977
|
);
|
|
974
1978
|
}
|
|
975
1979
|
}
|
|
@@ -1007,14 +2011,12 @@ class AI {
|
|
|
1007
2011
|
chunkCache+=delta;
|
|
1008
2012
|
|
|
1009
2013
|
if (chunkCache.trim() === '[DONE]') {
|
|
1010
|
-
responseEvents.push('[DONE]');
|
|
1011
2014
|
chunkCache = '';
|
|
1012
2015
|
return;
|
|
1013
2016
|
}
|
|
1014
2017
|
|
|
1015
2018
|
try{
|
|
1016
2019
|
const resp=JSON.parse(chunkCache)||{};
|
|
1017
|
-
responseEvents.push(resp);
|
|
1018
2020
|
//console.log(JSON.stringify(resp));
|
|
1019
2021
|
//console.log(resp)
|
|
1020
2022
|
const choice = resp.choices?.[0] || {};
|
|
@@ -1058,7 +2060,7 @@ class AI {
|
|
|
1058
2060
|
receiveStreamedToolCalls(tool_calls);
|
|
1059
2061
|
}
|
|
1060
2062
|
} catch(err) {
|
|
1061
|
-
console.warn(
|
|
2063
|
+
console.warn('AI stream callback failed.');
|
|
1062
2064
|
}
|
|
1063
2065
|
}
|
|
1064
2066
|
);
|
|
@@ -1072,12 +2074,6 @@ class AI {
|
|
|
1072
2074
|
reader.releaseLock();
|
|
1073
2075
|
}
|
|
1074
2076
|
|
|
1075
|
-
this.#reportResponse(responseEvents,id,{
|
|
1076
|
-
operation:'stream',
|
|
1077
|
-
transport:'http',
|
|
1078
|
-
destination:this.url
|
|
1079
|
-
});
|
|
1080
|
-
|
|
1081
2077
|
const tool_funcs={};
|
|
1082
2078
|
const orderedToolCalls=[...streamedToolCalls.values()].sort(
|
|
1083
2079
|
function sortStreamedToolCalls(a,b){
|
|
@@ -1103,7 +2099,9 @@ class AI {
|
|
|
1103
2099
|
if(Object.keys(tool_funcs).length&&!chunkString){
|
|
1104
2100
|
streamHandler('',`M-${id}`,false);
|
|
1105
2101
|
}
|
|
1106
|
-
|
|
2102
|
+
if(finishSpeech){
|
|
2103
|
+
this.finishTTS();
|
|
2104
|
+
}
|
|
1107
2105
|
await streamComplete(streamResult, `M-${id}`,isThinking);
|
|
1108
2106
|
|
|
1109
2107
|
//sync
|
|
@@ -1136,13 +2134,47 @@ class AI {
|
|
|
1136
2134
|
if(localOnly!==true&&localOnly!==false){
|
|
1137
2135
|
throw new TypeError('AI localOnly must be a boolean.');
|
|
1138
2136
|
}
|
|
1139
|
-
if(localOnly
|
|
2137
|
+
if(localOnly&&!this.#hasLocalRoute('llm',this.llmService)){
|
|
1140
2138
|
const error=new Error(
|
|
1141
2139
|
'This AI request requires a configured local model.'
|
|
1142
2140
|
);
|
|
1143
2141
|
error.code='AI_LOCAL_MODEL_REQUIRED';
|
|
1144
2142
|
throw error;
|
|
1145
2143
|
}
|
|
2144
|
+
if(this.#shouldUseProviderRuntime('llm',this.llmService,localOnly)){
|
|
2145
|
+
if(signal?.aborted){
|
|
2146
|
+
throw normalizeAIRequestAbort();
|
|
2147
|
+
}
|
|
2148
|
+
const request={
|
|
2149
|
+
messages,
|
|
2150
|
+
structuredOutput,
|
|
2151
|
+
tools,
|
|
2152
|
+
toolChoice,
|
|
2153
|
+
parallelToolCalls,
|
|
2154
|
+
id
|
|
2155
|
+
};
|
|
2156
|
+
await this.#reportRequest(onRequest,request,id);
|
|
2157
|
+
if(signal?.aborted){
|
|
2158
|
+
throw normalizeAIRequestAbort();
|
|
2159
|
+
}
|
|
2160
|
+
const response=await this.#providerRuntime.request(
|
|
2161
|
+
'llm',
|
|
2162
|
+
{
|
|
2163
|
+
operation:'chat',
|
|
2164
|
+
payload:request,
|
|
2165
|
+
localOnly,
|
|
2166
|
+
signal
|
|
2167
|
+
}
|
|
2168
|
+
);
|
|
2169
|
+
if(signal?.aborted){
|
|
2170
|
+
throw normalizeAIRequestAbort();
|
|
2171
|
+
}
|
|
2172
|
+
await onResponse(response,id,false);
|
|
2173
|
+
if(signal?.aborted){
|
|
2174
|
+
throw normalizeAIRequestAbort();
|
|
2175
|
+
}
|
|
2176
|
+
return response;
|
|
2177
|
+
}
|
|
1146
2178
|
|
|
1147
2179
|
return this.fetch(
|
|
1148
2180
|
messages,
|
|
@@ -1167,6 +2199,45 @@ class AI {
|
|
|
1167
2199
|
id=Date.now(),
|
|
1168
2200
|
requestHandler=function ignoreFetchRequest(){},
|
|
1169
2201
|
signal=null,
|
|
2202
|
+
){
|
|
2203
|
+
if(this.#shouldUseProviderRuntime('llm',this.llmService,false)){
|
|
2204
|
+
return this.fetchRequest({
|
|
2205
|
+
messages,
|
|
2206
|
+
structuredOutput,
|
|
2207
|
+
localOnly:false,
|
|
2208
|
+
tools,
|
|
2209
|
+
toolChoice:tool_choice,
|
|
2210
|
+
parallelToolCalls:parallel_tool_calls,
|
|
2211
|
+
id,
|
|
2212
|
+
signal,
|
|
2213
|
+
onRequest:requestHandler,
|
|
2214
|
+
onResponse:responseHandler
|
|
2215
|
+
});
|
|
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,
|
|
1170
2241
|
){
|
|
1171
2242
|
this.#assertServiceConfigured(this.llmService);
|
|
1172
2243
|
if(signal&&(
|
|
@@ -1204,6 +2275,13 @@ class AI {
|
|
|
1204
2275
|
|
|
1205
2276
|
const nativeOllama=this.#nativeOllama();
|
|
1206
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
|
+
|
|
1207
2285
|
if(nativeOllama){
|
|
1208
2286
|
const ollamaTools=this.#ollamaTools(tools,tool_choice);
|
|
1209
2287
|
const ollamaMessages=this.#ollamaMessages(messages,tool_choice);
|
|
@@ -1215,7 +2293,7 @@ class AI {
|
|
|
1215
2293
|
...(structuredOutputFormat?{format:structuredOutputFormat}:{}),
|
|
1216
2294
|
...(ollamaTools.length?{tools:ollamaTools}:{})
|
|
1217
2295
|
};
|
|
1218
|
-
this.#reportRequest(requestHandler,nativeRequest,id,{
|
|
2296
|
+
await this.#reportRequest(requestHandler,nativeRequest,id,{
|
|
1219
2297
|
operation:'fetch',
|
|
1220
2298
|
transport:'native',
|
|
1221
2299
|
destination:'Arcane.ollama.chat'
|
|
@@ -1232,11 +2310,6 @@ class AI {
|
|
|
1232
2310
|
}
|
|
1233
2311
|
throw error;
|
|
1234
2312
|
}
|
|
1235
|
-
this.#reportResponse(nativeResponse,id,{
|
|
1236
|
-
operation:'fetch',
|
|
1237
|
-
transport:'native',
|
|
1238
|
-
destination:'Arcane.ollama.chat'
|
|
1239
|
-
});
|
|
1240
2313
|
if(signal?.aborted){
|
|
1241
2314
|
throw normalizeAIRequestAbort();
|
|
1242
2315
|
}
|
|
@@ -1258,7 +2331,7 @@ class AI {
|
|
|
1258
2331
|
return responseJSON;
|
|
1259
2332
|
}
|
|
1260
2333
|
|
|
1261
|
-
this.#reportRequest(requestHandler,request,id,{
|
|
2334
|
+
await this.#reportRequest(requestHandler,request,id,{
|
|
1262
2335
|
operation:'fetch',
|
|
1263
2336
|
transport:'http',
|
|
1264
2337
|
destination:this.url
|
|
@@ -1309,11 +2382,6 @@ class AI {
|
|
|
1309
2382
|
}
|
|
1310
2383
|
throw error;
|
|
1311
2384
|
}
|
|
1312
|
-
this.#reportResponse(responseJSON,id,{
|
|
1313
|
-
operation:'fetch',
|
|
1314
|
-
transport:'http',
|
|
1315
|
-
destination:this.url
|
|
1316
|
-
});
|
|
1317
2385
|
if(signal?.aborted){
|
|
1318
2386
|
throw normalizeAIRequestAbort();
|
|
1319
2387
|
}
|
|
@@ -1348,9 +2416,9 @@ class AI {
|
|
|
1348
2416
|
}
|
|
1349
2417
|
|
|
1350
2418
|
try{
|
|
1351
|
-
this.#assertServiceConfigured(this.ttsService);
|
|
2419
|
+
this.#assertServiceConfigured(this.ttsService,'tts');
|
|
1352
2420
|
}catch(error){
|
|
1353
|
-
console.warn('
|
|
2421
|
+
console.warn('AI speech provider is unavailable.');
|
|
1354
2422
|
return Promise.resolve(false);
|
|
1355
2423
|
}
|
|
1356
2424
|
|
|
@@ -1498,7 +2566,28 @@ class AI {
|
|
|
1498
2566
|
}
|
|
1499
2567
|
|
|
1500
2568
|
async #requestSpeechAudio(job){
|
|
1501
|
-
|
|
2569
|
+
if(this.#usesProviderRuntime('tts',this.ttsService)){
|
|
2570
|
+
job.abortController=new AbortController();
|
|
2571
|
+
const responseFormat=this.#providerSpeechResponseFormat();
|
|
2572
|
+
const response=await this.#providerRuntime.request(
|
|
2573
|
+
'tts',
|
|
2574
|
+
{
|
|
2575
|
+
operation:'synthesize',
|
|
2576
|
+
payload:{
|
|
2577
|
+
model:this.#providerRuntime.selection('tts')?.modelId,
|
|
2578
|
+
voice:String(window.user?.AI_voice||'af_heart'),
|
|
2579
|
+
input:job.text,
|
|
2580
|
+
responseFormat,
|
|
2581
|
+
speed:this.voiceSpeed
|
|
2582
|
+
},
|
|
2583
|
+
localOnly:false,
|
|
2584
|
+
signal:job.abortController.signal
|
|
2585
|
+
}
|
|
2586
|
+
);
|
|
2587
|
+
return this.#normalizeProviderSpeechAudio(response);
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
const nativeSpeech=this.#nativeSpeech(this.ttsService,'tts');
|
|
1502
2591
|
|
|
1503
2592
|
if(nativeSpeech){
|
|
1504
2593
|
const response=await nativeSpeech.synthesize({
|
|
@@ -1577,6 +2666,109 @@ class AI {
|
|
|
1577
2666
|
return {chunks,type:this.audioType};
|
|
1578
2667
|
}
|
|
1579
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
|
+
|
|
2712
|
+
async #normalizeProviderSpeechAudio(response){
|
|
2713
|
+
if(response instanceof Blob){
|
|
2714
|
+
return {
|
|
2715
|
+
chunks:[new Uint8Array(await response.arrayBuffer())],
|
|
2716
|
+
type:response.type||this.audioType
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
if(response instanceof ArrayBuffer||ArrayBuffer.isView(response)){
|
|
2721
|
+
const bytes=response instanceof ArrayBuffer
|
|
2722
|
+
?new Uint8Array(response)
|
|
2723
|
+
:new Uint8Array(
|
|
2724
|
+
response.buffer,
|
|
2725
|
+
response.byteOffset,
|
|
2726
|
+
response.byteLength
|
|
2727
|
+
);
|
|
2728
|
+
return {
|
|
2729
|
+
chunks:[bytes],
|
|
2730
|
+
type:this.audioType
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
if(response&&typeof response==='object'){
|
|
2735
|
+
if(typeof response.audioBase64==='string'){
|
|
2736
|
+
return {
|
|
2737
|
+
chunks:[this.#base64ToBytes(response.audioBase64)],
|
|
2738
|
+
type:typeof response.contentType==='string'
|
|
2739
|
+
?response.contentType
|
|
2740
|
+
:this.audioType
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
if(response.audio instanceof Blob){
|
|
2744
|
+
return {
|
|
2745
|
+
chunks:[new Uint8Array(await response.audio.arrayBuffer())],
|
|
2746
|
+
type:response.audio.type
|
|
2747
|
+
||response.contentType
|
|
2748
|
+
||this.audioType
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
if(response.audio instanceof ArrayBuffer
|
|
2752
|
+
||ArrayBuffer.isView(response.audio)){
|
|
2753
|
+
const bytes=response.audio instanceof ArrayBuffer
|
|
2754
|
+
?new Uint8Array(response.audio)
|
|
2755
|
+
:new Uint8Array(
|
|
2756
|
+
response.audio.buffer,
|
|
2757
|
+
response.audio.byteOffset,
|
|
2758
|
+
response.audio.byteLength
|
|
2759
|
+
);
|
|
2760
|
+
return {
|
|
2761
|
+
chunks:[bytes],
|
|
2762
|
+
type:typeof response.contentType==='string'
|
|
2763
|
+
?response.contentType
|
|
2764
|
+
:this.audioType
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
throw new TypeError('Arcane returned an invalid provider speech response.');
|
|
2770
|
+
}
|
|
2771
|
+
|
|
1580
2772
|
#getSpeechAudioContext(){
|
|
1581
2773
|
if(this.audioContext&&this.audioContext.state!=='closed'){
|
|
1582
2774
|
return this.audioContext;
|
|
@@ -1594,11 +2786,50 @@ class AI {
|
|
|
1594
2786
|
|
|
1595
2787
|
async fetchSTT(
|
|
1596
2788
|
audioFile,
|
|
1597
|
-
responseHandler=(text='')=>{}
|
|
2789
|
+
responseHandler=(text='')=>{},
|
|
2790
|
+
signal=null
|
|
1598
2791
|
){
|
|
1599
|
-
this.#assertServiceConfigured(this.sttService);
|
|
2792
|
+
this.#assertServiceConfigured(this.sttService,'stt');
|
|
2793
|
+
if(signal&&(
|
|
2794
|
+
typeof signal.aborted!=='boolean'
|
|
2795
|
+
||typeof signal.addEventListener!=='function'
|
|
2796
|
+
)){
|
|
2797
|
+
throw new TypeError('AI request signal must be an AbortSignal.');
|
|
2798
|
+
}
|
|
2799
|
+
if(signal?.aborted){
|
|
2800
|
+
throw normalizeAIRequestAbort();
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
if(this.#usesProviderRuntime('stt',this.sttService)){
|
|
2804
|
+
if(!audioFile||typeof audioFile.arrayBuffer!=='function'){
|
|
2805
|
+
throw new TypeError('Speech transcription requires an audio Blob or File.');
|
|
2806
|
+
}
|
|
2807
|
+
const response=await this.#providerRuntime.request(
|
|
2808
|
+
'stt',
|
|
2809
|
+
{
|
|
2810
|
+
operation:'transcribe',
|
|
2811
|
+
payload:{
|
|
2812
|
+
audio:audioFile,
|
|
2813
|
+
mimeType:String(audioFile.type||'audio/webm'),
|
|
2814
|
+
model:this.#providerRuntime.selection('stt')?.modelId
|
|
2815
|
+
},
|
|
2816
|
+
localOnly:false,
|
|
2817
|
+
signal
|
|
2818
|
+
}
|
|
2819
|
+
);
|
|
2820
|
+
const text=typeof response==='string'
|
|
2821
|
+
?response
|
|
2822
|
+
:response?.text;
|
|
2823
|
+
if(typeof text!=='string'){
|
|
2824
|
+
throw new TypeError(
|
|
2825
|
+
'Arcane returned an invalid provider speech transcription.'
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
await responseHandler(text);
|
|
2829
|
+
return text;
|
|
2830
|
+
}
|
|
1600
2831
|
|
|
1601
|
-
const nativeSpeech=this.#nativeSpeech(this.sttService);
|
|
2832
|
+
const nativeSpeech=this.#nativeSpeech(this.sttService,'stt');
|
|
1602
2833
|
|
|
1603
2834
|
if(nativeSpeech){
|
|
1604
2835
|
if(!audioFile||typeof audioFile.arrayBuffer!=='function'){
|
|
@@ -1632,7 +2863,8 @@ class AI {
|
|
|
1632
2863
|
method: 'POST',
|
|
1633
2864
|
credentials: credentials,
|
|
1634
2865
|
headers: this.#sttHeaders[this.sttService],
|
|
1635
|
-
body: formData
|
|
2866
|
+
body: formData,
|
|
2867
|
+
signal
|
|
1636
2868
|
}
|
|
1637
2869
|
);
|
|
1638
2870
|
|
|
@@ -1672,7 +2904,7 @@ class AI {
|
|
|
1672
2904
|
try{
|
|
1673
2905
|
sourceNode.stop();
|
|
1674
2906
|
}catch(error){
|
|
1675
|
-
console.warn('
|
|
2907
|
+
console.warn('AI audio could not be stopped cleanly.');
|
|
1676
2908
|
}
|
|
1677
2909
|
}
|
|
1678
2910
|
|
|
@@ -1798,7 +3030,7 @@ class AI {
|
|
|
1798
3030
|
#requestSpeechPlayback(){
|
|
1799
3031
|
this.#pumpSpeechPlayback().catch(
|
|
1800
3032
|
function reportSpeechPlaybackFailure(error){
|
|
1801
|
-
console.warn('
|
|
3033
|
+
console.warn('AI audio playback failed.');
|
|
1802
3034
|
}
|
|
1803
3035
|
);
|
|
1804
3036
|
}
|
|
@@ -1925,7 +3157,7 @@ class AI {
|
|
|
1925
3157
|
}
|
|
1926
3158
|
|
|
1927
3159
|
if(job.generation===this.speechGeneration&&error?.name!=='AbortError'){
|
|
1928
|
-
console.warn('
|
|
3160
|
+
console.warn('AI speech synthesis failed.');
|
|
1929
3161
|
}
|
|
1930
3162
|
|
|
1931
3163
|
this.#requestSpeechPlayback();
|
|
@@ -1974,7 +3206,7 @@ class AI {
|
|
|
1974
3206
|
);
|
|
1975
3207
|
|
|
1976
3208
|
if(error?.name&&error.name!=='NotAllowedError'){
|
|
1977
|
-
console.info('AI speech is waiting for audio playback permission
|
|
3209
|
+
console.info('AI speech is waiting for audio playback permission.');
|
|
1978
3210
|
}
|
|
1979
3211
|
|
|
1980
3212
|
return true;
|