arcane-os 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -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 +475 -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 +280 -62
- package/runtime/arcane/components/speech.html +1113 -265
- package/runtime/arcane/entities/Chat.js +246 -43
- package/runtime/arcane/modules/AI.js +713 -162
- package/runtime/arcane/modules/AIProviderRuntime.js +2289 -0
- package/runtime/arcane/modules/AIRuntimeState.js +872 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +293 -27
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +682 -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 +6 -4
- package/src/cli/main.mjs +14 -2
- package/src/constants.mjs +1 -1
- package/src/dev-server.mjs +244 -13
- package/src/import-map.mjs +59 -1
- package/src/packager/core.mjs +2 -2
- package/src/runtime.mjs +14 -4
- package/src/sdk-browser-runtime.mjs +28 -75
- package/src/templates/workspace-template.mjs +4 -4
- package/src/toolchain.mjs +3 -0
- package/src/workspace-runtime.mjs +1 -1
- package/src/workspace.mjs +1 -1
|
@@ -1,79 +1,72 @@
|
|
|
1
1
|
import './DBOPFS.js';
|
|
2
2
|
import UserEntity from '../entities/User.js';
|
|
3
3
|
import {getAIPreferencesForRuntime} from './AIPreferenceRuntime.js';
|
|
4
|
+
import {getAIProviderRuntime} from './AIProviderRuntime.js';
|
|
4
5
|
import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
|
|
5
6
|
|
|
6
7
|
let credentials='include';
|
|
7
8
|
credentials='omit';
|
|
8
9
|
|
|
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
|
-
}
|
|
10
|
+
const LEGACY_AI_SERVICES=new Set(['OPENAI','OLLAMA','LOCAL_SPEACH']);
|
|
59
11
|
|
|
60
12
|
function isAIRequestAbort(error,signal){
|
|
61
13
|
return signal?.aborted
|
|
62
14
|
||error?.name==='AbortError'
|
|
63
15
|
||error?.code==='ARCANE_REQUEST_ABORTED'
|
|
16
|
+
||error?.code==='ARCANE_AI_REQUEST_ABORTED'
|
|
64
17
|
||error?.code==='AI_REQUEST_ABORTED';
|
|
65
18
|
}
|
|
66
19
|
|
|
67
20
|
function normalizeAIRequestAbort(error){
|
|
68
|
-
if(error?.code==='
|
|
21
|
+
if(error?.code==='ARCANE_AI_REQUEST_ABORTED'){
|
|
69
22
|
return error;
|
|
70
23
|
}
|
|
71
24
|
const normalized=new Error('The AI request was cancelled.',{cause:error});
|
|
72
25
|
normalized.name='AbortError';
|
|
73
|
-
normalized.code='
|
|
26
|
+
normalized.code='ARCANE_AI_REQUEST_ABORTED';
|
|
74
27
|
return normalized;
|
|
75
28
|
}
|
|
76
29
|
|
|
30
|
+
function normalizeAIStartupOptions(options){
|
|
31
|
+
if(options===undefined){
|
|
32
|
+
return Object.freeze({startMuted:true,signal:null});
|
|
33
|
+
}
|
|
34
|
+
if(!options||typeof options!=='object'||Array.isArray(options)){
|
|
35
|
+
throw new TypeError('AI startup options must be a plain object.');
|
|
36
|
+
}
|
|
37
|
+
const prototype=Object.getPrototypeOf(options);
|
|
38
|
+
if(prototype!==Object.prototype&&prototype!==null){
|
|
39
|
+
throw new TypeError('AI startup options must be a plain object.');
|
|
40
|
+
}
|
|
41
|
+
const descriptors=Object.getOwnPropertyDescriptors(options);
|
|
42
|
+
for(const key of Reflect.ownKeys(descriptors)){
|
|
43
|
+
if(typeof key==='symbol'||(key!=='startMuted'&&key!=='signal')){
|
|
44
|
+
throw new TypeError('AI startup options contain an unknown option.');
|
|
45
|
+
}
|
|
46
|
+
if(!Object.hasOwn(descriptors[key],'value')){
|
|
47
|
+
throw new TypeError(`AI startup options.${key} must be a data property.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const startMuted=Object.hasOwn(descriptors,'startMuted')
|
|
51
|
+
?descriptors.startMuted.value
|
|
52
|
+
:true;
|
|
53
|
+
const signal=Object.hasOwn(descriptors,'signal')
|
|
54
|
+
?descriptors.signal.value
|
|
55
|
+
:null;
|
|
56
|
+
if(typeof startMuted!=='boolean'){
|
|
57
|
+
throw new TypeError('AI startup startMuted must be a boolean.');
|
|
58
|
+
}
|
|
59
|
+
if(signal!==null&&signal!==undefined&&(
|
|
60
|
+
typeof signal!=='object'
|
|
61
|
+
||typeof signal.aborted!=='boolean'
|
|
62
|
+
||typeof signal.addEventListener!=='function'
|
|
63
|
+
||typeof signal.removeEventListener!=='function'
|
|
64
|
+
)){
|
|
65
|
+
throw new TypeError('AI startup signal must be an AbortSignal.');
|
|
66
|
+
}
|
|
67
|
+
return Object.freeze({startMuted,signal});
|
|
68
|
+
}
|
|
69
|
+
|
|
77
70
|
class AI {
|
|
78
71
|
// This is the enum section for inference configuration
|
|
79
72
|
#service = {
|
|
@@ -155,7 +148,7 @@ class AI {
|
|
|
155
148
|
}
|
|
156
149
|
|
|
157
150
|
ready=false;
|
|
158
|
-
muted=
|
|
151
|
+
muted=true;
|
|
159
152
|
|
|
160
153
|
|
|
161
154
|
llmService = '';
|
|
@@ -186,16 +179,33 @@ class AI {
|
|
|
186
179
|
return window.ai;
|
|
187
180
|
}
|
|
188
181
|
|
|
182
|
+
const preferences=[
|
|
183
|
+
llmService||'OPENAI',
|
|
184
|
+
sttService||'OPENAI',
|
|
185
|
+
ttsService||'OPENAI',
|
|
186
|
+
model||'OPENAI',
|
|
187
|
+
modelTTS||'OPENAI',
|
|
188
|
+
modelSTT||'OPENAI'
|
|
189
|
+
];
|
|
189
190
|
this.setAI(
|
|
190
|
-
|
|
191
|
-
sttService || 'OPENAI',
|
|
192
|
-
ttsService || 'OPENAI',
|
|
193
|
-
model || 'OPENAI',
|
|
194
|
-
modelTTS || 'OPENAI',
|
|
195
|
-
modelSTT || 'OPENAI'
|
|
191
|
+
...preferences
|
|
196
192
|
);
|
|
197
193
|
}
|
|
198
194
|
|
|
195
|
+
#providerRuntime=getAIProviderRuntime();
|
|
196
|
+
#preferenceTuple=Object.freeze([
|
|
197
|
+
'OPENAI',
|
|
198
|
+
'OPENAI',
|
|
199
|
+
'OPENAI',
|
|
200
|
+
'OPENAI',
|
|
201
|
+
'OPENAI',
|
|
202
|
+
'OPENAI'
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
get providerRuntime(){
|
|
206
|
+
return this.#providerRuntime;
|
|
207
|
+
}
|
|
208
|
+
|
|
199
209
|
get url() {
|
|
200
210
|
return `${this.#service.baseURL[this.llmService]}${this.#paths.chat[this.llmService]}`
|
|
201
211
|
}
|
|
@@ -234,6 +244,10 @@ class AI {
|
|
|
234
244
|
}
|
|
235
245
|
|
|
236
246
|
get configured(){
|
|
247
|
+
if(this.#usesProviderRuntime('llm',this.llmService)){
|
|
248
|
+
const state=this.#providerRuntime.status('llm');
|
|
249
|
+
return state.state==='ready'&&state.loaded===true;
|
|
250
|
+
}
|
|
237
251
|
if(this.llmService==='OLLAMA'){
|
|
238
252
|
return Boolean(this.model)&&Boolean(this.#nativeOllama());
|
|
239
253
|
}
|
|
@@ -243,9 +257,12 @@ class AI {
|
|
|
243
257
|
&&Boolean(this.license);
|
|
244
258
|
}
|
|
245
259
|
|
|
246
|
-
#assertServiceConfigured(service=this.llmService){
|
|
260
|
+
#assertServiceConfigured(service=this.llmService,role='llm'){
|
|
261
|
+
if(this.#usesProviderRuntime(role,service)){
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
247
264
|
if(service==='OLLAMA'){
|
|
248
|
-
if(this.#nativeOllama()){
|
|
265
|
+
if(role==='llm'&&this.#nativeOllama()){
|
|
249
266
|
return true;
|
|
250
267
|
}
|
|
251
268
|
|
|
@@ -255,8 +272,16 @@ class AI {
|
|
|
255
272
|
error.code='AI_NATIVE_LOCAL_REQUIRED';
|
|
256
273
|
throw error;
|
|
257
274
|
}
|
|
258
|
-
if(service
|
|
259
|
-
|
|
275
|
+
if(service==='LOCAL_SPEACH'){
|
|
276
|
+
if(this.#nativeSpeech(service,role)){
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const error=new Error(
|
|
281
|
+
`Local ${role.toUpperCase()} requires the capability-gated Arcane API.`
|
|
282
|
+
);
|
|
283
|
+
error.code='AI_NATIVE_LOCAL_REQUIRED';
|
|
284
|
+
throw error;
|
|
260
285
|
}
|
|
261
286
|
|
|
262
287
|
if(service==='OPENAI'&&this.license){
|
|
@@ -268,6 +293,35 @@ class AI {
|
|
|
268
293
|
throw error;
|
|
269
294
|
}
|
|
270
295
|
|
|
296
|
+
#usesProviderRuntime(role,service){
|
|
297
|
+
return Boolean(this.#providerRuntime.selection(role));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#shouldUseProviderRuntime(role,service,localOnly=false){
|
|
301
|
+
if(!localOnly){
|
|
302
|
+
return this.#usesProviderRuntime(role,service);
|
|
303
|
+
}
|
|
304
|
+
const selection=this.#providerRuntime.selection(
|
|
305
|
+
role,
|
|
306
|
+
{localOnly:true}
|
|
307
|
+
);
|
|
308
|
+
return Boolean(selection);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#hasLocalRoute(role,service){
|
|
312
|
+
const selection=this.#providerRuntime.selection(
|
|
313
|
+
role,
|
|
314
|
+
{localOnly:true}
|
|
315
|
+
);
|
|
316
|
+
if(selection){
|
|
317
|
+
return selection.localOnly===true;
|
|
318
|
+
}
|
|
319
|
+
if(this.#providerRuntime.selection(role)){
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
return role==='llm'&&service==='OLLAMA';
|
|
323
|
+
}
|
|
324
|
+
|
|
271
325
|
audioMessageChunks='';
|
|
272
326
|
sourceNodes=[];
|
|
273
327
|
isSpeaking=false;
|
|
@@ -282,6 +336,140 @@ class AI {
|
|
|
282
336
|
speechSynthesisTail=Promise.resolve();
|
|
283
337
|
speechUnlockHandler=null;
|
|
284
338
|
|
|
339
|
+
#nextPreferenceTuple(values){
|
|
340
|
+
const current=this.#preferenceTuple;
|
|
341
|
+
const next=values.map(function normalizeAIPreference(value,index){
|
|
342
|
+
if(value===undefined||value===null||value===''){
|
|
343
|
+
return current[index];
|
|
344
|
+
}
|
|
345
|
+
if(typeof value!=='string'||value.trim()!==value||!value){
|
|
346
|
+
throw new TypeError('AI preferences must be nonempty trimmed strings.');
|
|
347
|
+
}
|
|
348
|
+
return value;
|
|
349
|
+
});
|
|
350
|
+
return Object.freeze(next);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#assertValidProviderTuple(tuple){
|
|
354
|
+
if(tuple[0]==='OLLAMA'){
|
|
355
|
+
const mappedModel=tuple[3]==='OPENAI'?null:this.#models[tuple[3]];
|
|
356
|
+
if(!mappedModel&&!normalizeOllamaModelIdentifier(tuple[3])){
|
|
357
|
+
const error=new TypeError('The Ollama model preference is invalid.');
|
|
358
|
+
error.code='AI_MODEL_INVALID';
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
#applyPreferenceTuple(tuple){
|
|
365
|
+
const [
|
|
366
|
+
llmService,
|
|
367
|
+
sttService,
|
|
368
|
+
ttsService,
|
|
369
|
+
model,
|
|
370
|
+
modelTTS,
|
|
371
|
+
modelSTT
|
|
372
|
+
]=tuple;
|
|
373
|
+
let normalizedLLMModel=model;
|
|
374
|
+
if(llmService==='OLLAMA'){
|
|
375
|
+
const mappedModel=model==='OPENAI'?null:this.#models[model];
|
|
376
|
+
normalizedLLMModel=mappedModel
|
|
377
|
+
||normalizeOllamaModelIdentifier(model)
|
|
378
|
+
||model;
|
|
379
|
+
}else if(llmService==='OPENAI'){
|
|
380
|
+
normalizedLLMModel=this.#models.OPENAI;
|
|
381
|
+
}
|
|
382
|
+
this.llmService=llmService;
|
|
383
|
+
this.sttService=sttService;
|
|
384
|
+
this.ttsService=ttsService;
|
|
385
|
+
this.model=normalizedLLMModel;
|
|
386
|
+
this.modelTTS=this.#ttsModels[modelTTS]||modelTTS;
|
|
387
|
+
this.modelSTT=this.#sttModels[modelSTT]||modelSTT;
|
|
388
|
+
this.reasoningEffort='';
|
|
389
|
+
this.#preferenceTuple=Object.freeze(tuple.slice());
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#tupleFromProviderRoutes(selections){
|
|
393
|
+
const llm=selections.llm.default;
|
|
394
|
+
const stt=selections.stt.default;
|
|
395
|
+
const tts=selections.tts.default;
|
|
396
|
+
return Object.freeze([
|
|
397
|
+
llm?.providerId||'',
|
|
398
|
+
stt?.providerId||'',
|
|
399
|
+
tts?.providerId||'',
|
|
400
|
+
llm?.modelId||'',
|
|
401
|
+
tts?.modelId||'',
|
|
402
|
+
stt?.modelId||''
|
|
403
|
+
]);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#routesFromPreferenceTuple(tuple){
|
|
407
|
+
const roles={
|
|
408
|
+
llm:[tuple[0],tuple[3]],
|
|
409
|
+
stt:[tuple[1],tuple[5]],
|
|
410
|
+
tts:[tuple[2],tuple[4]]
|
|
411
|
+
};
|
|
412
|
+
const selections={};
|
|
413
|
+
for(const role of ['llm','stt','tts']){
|
|
414
|
+
const [providerId,modelId]=roles[role];
|
|
415
|
+
const identity=providerId&&modelId
|
|
416
|
+
?this.#providerRuntime.providerIdentity(role,providerId)
|
|
417
|
+
:null;
|
|
418
|
+
const pendingNonLegacy=Boolean(
|
|
419
|
+
providerId
|
|
420
|
+
&&modelId
|
|
421
|
+
&&!LEGACY_AI_SERVICES.has(providerId)
|
|
422
|
+
);
|
|
423
|
+
if(!identity&&!pendingNonLegacy){
|
|
424
|
+
selections[role]={default:null,localOnly:null};
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const selection={
|
|
428
|
+
providerId,
|
|
429
|
+
modelId,
|
|
430
|
+
localOnly:identity?.localOnly??null
|
|
431
|
+
};
|
|
432
|
+
selections[role]={
|
|
433
|
+
default:selection,
|
|
434
|
+
localOnly:identity?.localOnly===true
|
|
435
|
+
?{...selection,localOnly:true}
|
|
436
|
+
:null
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return selections;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
#assertRegisteredLegacyRoutes(selections){
|
|
443
|
+
for(const role of ['llm','stt','tts']){
|
|
444
|
+
for(const routeName of ['default','localOnly']){
|
|
445
|
+
const selection=selections?.[role]?.[routeName];
|
|
446
|
+
if(selection
|
|
447
|
+
&&LEGACY_AI_SERVICES.has(selection.providerId)
|
|
448
|
+
&&!this.#providerRuntime.hasProvider(role,selection.providerId)){
|
|
449
|
+
const error=new Error(
|
|
450
|
+
`Legacy AI provider ${selection.providerId} requires an explicit ${role} adapter before routing.`
|
|
451
|
+
);
|
|
452
|
+
error.code='ARCANE_AI_PROVIDER_UNAVAILABLE';
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async #unloadProviderRolesForTransition(){
|
|
460
|
+
const settlements=await Promise.allSettled([
|
|
461
|
+
this.#providerRuntime.unload('llm'),
|
|
462
|
+
this.#providerRuntime.unload('stt'),
|
|
463
|
+
this.#providerRuntime.unload('tts')
|
|
464
|
+
]);
|
|
465
|
+
const failure=settlements.find(function findAITransitionCleanupFailure(result){
|
|
466
|
+
return result.status==='rejected';
|
|
467
|
+
});
|
|
468
|
+
if(failure){
|
|
469
|
+
throw failure.reason;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
285
473
|
// Set models to be used by the AI.
|
|
286
474
|
// Note: Only those that are defined are set.
|
|
287
475
|
setAI(
|
|
@@ -304,29 +492,82 @@ class AI {
|
|
|
304
492
|
) {
|
|
305
493
|
return false;
|
|
306
494
|
}
|
|
495
|
+
const tuple=this.#nextPreferenceTuple([
|
|
496
|
+
llmService,
|
|
497
|
+
sttService,
|
|
498
|
+
ttsService,
|
|
499
|
+
model,
|
|
500
|
+
modelTTS,
|
|
501
|
+
modelSTT
|
|
502
|
+
]);
|
|
503
|
+
this.#assertValidProviderTuple(tuple);
|
|
504
|
+
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
505
|
+
this.#applyPreferenceTuple(tuple);
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
307
508
|
|
|
308
|
-
|
|
309
|
-
this
|
|
310
|
-
this.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
509
|
+
configureProviders(selections){
|
|
510
|
+
this.#assertRegisteredLegacyRoutes(selections);
|
|
511
|
+
const configured=this.#providerRuntime.configure(selections);
|
|
512
|
+
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
513
|
+
return configured;
|
|
514
|
+
}
|
|
314
515
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
516
|
+
async transitionAI(
|
|
517
|
+
llmService,
|
|
518
|
+
sttService,
|
|
519
|
+
ttsService,
|
|
520
|
+
model,
|
|
521
|
+
modelTTS,
|
|
522
|
+
modelSTT
|
|
523
|
+
){
|
|
524
|
+
const tuple=this.#nextPreferenceTuple([
|
|
525
|
+
llmService,
|
|
526
|
+
sttService,
|
|
527
|
+
ttsService,
|
|
528
|
+
model,
|
|
529
|
+
modelTTS,
|
|
530
|
+
modelSTT
|
|
531
|
+
]);
|
|
532
|
+
this.#assertValidProviderTuple(tuple);
|
|
533
|
+
this.stopAudio();
|
|
534
|
+
await this.#unloadProviderRolesForTransition();
|
|
535
|
+
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
536
|
+
this.#applyPreferenceTuple(tuple);
|
|
537
|
+
return this.#providerRuntime.status();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async transitionProviders(selections){
|
|
541
|
+
this.#assertRegisteredLegacyRoutes(selections);
|
|
542
|
+
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
543
|
+
this.stopAudio();
|
|
544
|
+
await this.#unloadProviderRolesForTransition();
|
|
545
|
+
const configured=this.#providerRuntime.configure(prepared);
|
|
546
|
+
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
547
|
+
return configured;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async startProviders(options){
|
|
551
|
+
const normalized=normalizeAIStartupOptions(options);
|
|
552
|
+
this.muted=normalized.startMuted;
|
|
553
|
+
if(normalized.startMuted){
|
|
554
|
+
this.stopAudio();
|
|
555
|
+
}
|
|
556
|
+
return this.#providerRuntime.start(normalized);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async setSpeechMuted(muted){
|
|
560
|
+
if(typeof muted!=='boolean'){
|
|
561
|
+
throw new TypeError('AI speech muted state must be a boolean.');
|
|
562
|
+
}
|
|
563
|
+
this.muted=muted;
|
|
564
|
+
if(muted){
|
|
565
|
+
this.stopAudio();
|
|
566
|
+
}
|
|
567
|
+
if(!this.#usesProviderRuntime('tts',this.ttsService)){
|
|
568
|
+
return true;
|
|
324
569
|
}
|
|
325
|
-
this.
|
|
326
|
-
this.modelTTS=this.#ttsModels[modelTTS];
|
|
327
|
-
this.modelSTT=this.#sttModels[modelSTT];
|
|
328
|
-
this.reasoningEffort='';
|
|
329
|
-
|
|
570
|
+
await this.#providerRuntime.setSpeechMuted(muted);
|
|
330
571
|
return true;
|
|
331
572
|
}
|
|
332
573
|
|
|
@@ -376,14 +617,19 @@ class AI {
|
|
|
376
617
|
:null;
|
|
377
618
|
}
|
|
378
619
|
|
|
379
|
-
#nativeSpeech(service){
|
|
620
|
+
#nativeSpeech(service,role){
|
|
380
621
|
const client=globalThis.Arcane?.speech;
|
|
381
622
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
623
|
+
if(service!=='LOCAL_SPEACH'){
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
if(role==='tts'&&typeof client?.synthesize==='function'){
|
|
627
|
+
return client;
|
|
628
|
+
}
|
|
629
|
+
if(role==='stt'&&typeof client?.transcribe==='function'){
|
|
630
|
+
return client;
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
387
633
|
}
|
|
388
634
|
|
|
389
635
|
async #androidNativeHost(){
|
|
@@ -589,34 +835,81 @@ class AI {
|
|
|
589
835
|
return null;
|
|
590
836
|
}
|
|
591
837
|
|
|
592
|
-
#reportRequest(requestHandler,request,id
|
|
838
|
+
async #reportRequest(requestHandler,request,id){
|
|
593
839
|
if(typeof requestHandler!=='function'){
|
|
594
|
-
throw new TypeError('AI
|
|
840
|
+
throw new TypeError('AI onRequest callback must be a function.');
|
|
595
841
|
}
|
|
842
|
+
await requestHandler(request,id);
|
|
843
|
+
}
|
|
596
844
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
845
|
+
#providerStreamEmissions(chunk,seeThinking){
|
|
846
|
+
const chunks=[];
|
|
847
|
+
const toolNames=[];
|
|
848
|
+
const choices=Array.isArray(chunk?.choices)?chunk.choices:[];
|
|
849
|
+
for(const choice of choices){
|
|
850
|
+
const delta=choice?.delta||{};
|
|
851
|
+
if(seeThinking&&typeof delta.reasoning_content==='string'){
|
|
852
|
+
chunks.push({text:delta.reasoning_content,thinking:true});
|
|
853
|
+
}
|
|
854
|
+
if(typeof delta.content==='string'){
|
|
855
|
+
chunks.push({text:delta.content,thinking:false});
|
|
856
|
+
}
|
|
857
|
+
for(const call of Array.isArray(delta.tool_calls)?delta.tool_calls:[]){
|
|
858
|
+
const name=call?.function?.name;
|
|
859
|
+
if(typeof name==='string'&&name){
|
|
860
|
+
toolNames.push(name);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
603
863
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
864
|
+
if(!choices.length){
|
|
865
|
+
if(seeThinking&&typeof chunk?.thinking==='string'){
|
|
866
|
+
chunks.push({text:chunk.thinking,thinking:true});
|
|
867
|
+
}
|
|
868
|
+
const text=typeof chunk?.text==='string'
|
|
869
|
+
?chunk.text
|
|
870
|
+
:typeof chunk?.content==='string'
|
|
871
|
+
?chunk.content
|
|
872
|
+
:'';
|
|
873
|
+
if(text){
|
|
874
|
+
chunks.push({text,thinking:false});
|
|
875
|
+
}
|
|
876
|
+
const calls=Array.isArray(chunk?.toolCalls)
|
|
877
|
+
?chunk.toolCalls
|
|
878
|
+
:Array.isArray(chunk?.tool_calls)
|
|
879
|
+
?chunk.tool_calls
|
|
880
|
+
:[];
|
|
881
|
+
for(const call of calls){
|
|
882
|
+
const name=call?.function?.name||call?.name;
|
|
883
|
+
if(typeof name==='string'&&name){
|
|
884
|
+
toolNames.push(name);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return {chunks,toolNames};
|
|
611
889
|
}
|
|
612
890
|
|
|
613
|
-
#
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
891
|
+
#providerCompletionOutput(completion){
|
|
892
|
+
if(typeof completion==='string'){
|
|
893
|
+
return completion;
|
|
894
|
+
}
|
|
895
|
+
const toolRecord={};
|
|
896
|
+
let toolCount=0;
|
|
897
|
+
for(const choice of Array.isArray(completion?.choices)?completion.choices:[]){
|
|
898
|
+
for(const call of Array.isArray(choice?.message?.tool_calls)
|
|
899
|
+
?choice.message.tool_calls
|
|
900
|
+
:[]){
|
|
901
|
+
const name=call?.function?.name;
|
|
902
|
+
if(typeof name==='string'&&name){
|
|
903
|
+
toolRecord[name]=call.function.arguments;
|
|
904
|
+
toolCount+=1;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if(toolCount){
|
|
909
|
+
return toolRecord;
|
|
910
|
+
}
|
|
911
|
+
const content=completion?.choices?.[0]?.message?.content;
|
|
912
|
+
return typeof content==='string'?content:completion;
|
|
620
913
|
}
|
|
621
914
|
|
|
622
915
|
#assertRequiredOllamaToolCall(toolCalls=[],toolChoice='auto'){
|
|
@@ -701,13 +994,116 @@ class AI {
|
|
|
701
994
|
if(localOnly!==true&&localOnly!==false){
|
|
702
995
|
throw new TypeError('AI localOnly must be a boolean.');
|
|
703
996
|
}
|
|
704
|
-
if(localOnly
|
|
997
|
+
if(localOnly&&!this.#hasLocalRoute('llm',this.llmService)){
|
|
705
998
|
const error=new Error(
|
|
706
999
|
'This AI request requires a configured local model.'
|
|
707
1000
|
);
|
|
708
1001
|
error.code='AI_LOCAL_MODEL_REQUIRED';
|
|
709
1002
|
throw error;
|
|
710
1003
|
}
|
|
1004
|
+
if(this.#shouldUseProviderRuntime('llm',this.llmService,localOnly)){
|
|
1005
|
+
const request={
|
|
1006
|
+
messages,
|
|
1007
|
+
structuredOutput,
|
|
1008
|
+
tools,
|
|
1009
|
+
toolChoice,
|
|
1010
|
+
parallelToolCalls,
|
|
1011
|
+
id,
|
|
1012
|
+
seeThinking
|
|
1013
|
+
};
|
|
1014
|
+
const displayId=`M-${id}`;
|
|
1015
|
+
const announcedTools=new Set();
|
|
1016
|
+
let handle=null;
|
|
1017
|
+
try{
|
|
1018
|
+
if(signal?.aborted){
|
|
1019
|
+
throw normalizeAIRequestAbort();
|
|
1020
|
+
}
|
|
1021
|
+
await this.#reportRequest(onRequest,request,id);
|
|
1022
|
+
if(signal?.aborted){
|
|
1023
|
+
throw normalizeAIRequestAbort();
|
|
1024
|
+
}
|
|
1025
|
+
handle=await this.#providerRuntime.request(
|
|
1026
|
+
'llm',
|
|
1027
|
+
{
|
|
1028
|
+
operation:'stream',
|
|
1029
|
+
payload:request,
|
|
1030
|
+
localOnly,
|
|
1031
|
+
signal
|
|
1032
|
+
}
|
|
1033
|
+
);
|
|
1034
|
+
for await(const chunk of handle){
|
|
1035
|
+
if(signal?.aborted){
|
|
1036
|
+
throw normalizeAIRequestAbort();
|
|
1037
|
+
}
|
|
1038
|
+
const emissions=this.#providerStreamEmissions(chunk,seeThinking);
|
|
1039
|
+
for(const emission of emissions.chunks){
|
|
1040
|
+
if(signal?.aborted){
|
|
1041
|
+
throw normalizeAIRequestAbort();
|
|
1042
|
+
}
|
|
1043
|
+
await onChunk(
|
|
1044
|
+
emission.text,
|
|
1045
|
+
displayId,
|
|
1046
|
+
emission.thinking
|
|
1047
|
+
);
|
|
1048
|
+
if(signal?.aborted){
|
|
1049
|
+
throw normalizeAIRequestAbort();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
for(const name of emissions.toolNames){
|
|
1053
|
+
if(signal?.aborted){
|
|
1054
|
+
throw normalizeAIRequestAbort();
|
|
1055
|
+
}
|
|
1056
|
+
if(!announcedTools.has(name)){
|
|
1057
|
+
announcedTools.add(name);
|
|
1058
|
+
await onToolCall(name);
|
|
1059
|
+
if(signal?.aborted){
|
|
1060
|
+
throw normalizeAIRequestAbort();
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
const completion=await handle.result;
|
|
1066
|
+
if(signal?.aborted){
|
|
1067
|
+
throw normalizeAIRequestAbort();
|
|
1068
|
+
}
|
|
1069
|
+
for(const choice of Array.isArray(completion?.choices)
|
|
1070
|
+
?completion.choices
|
|
1071
|
+
:[]){
|
|
1072
|
+
for(const call of Array.isArray(choice?.message?.tool_calls)
|
|
1073
|
+
?choice.message.tool_calls
|
|
1074
|
+
:[]){
|
|
1075
|
+
const name=call?.function?.name;
|
|
1076
|
+
if(typeof name==='string'&&name&&!announcedTools.has(name)){
|
|
1077
|
+
if(signal?.aborted){
|
|
1078
|
+
throw normalizeAIRequestAbort();
|
|
1079
|
+
}
|
|
1080
|
+
announcedTools.add(name);
|
|
1081
|
+
await onToolCall(name);
|
|
1082
|
+
if(signal?.aborted){
|
|
1083
|
+
throw normalizeAIRequestAbort();
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const result=this.#providerCompletionOutput(completion);
|
|
1089
|
+
await onComplete(result,displayId,false);
|
|
1090
|
+
if(signal?.aborted){
|
|
1091
|
+
throw normalizeAIRequestAbort();
|
|
1092
|
+
}
|
|
1093
|
+
this.finishTTS();
|
|
1094
|
+
return result;
|
|
1095
|
+
}catch(error){
|
|
1096
|
+
if(handle){
|
|
1097
|
+
await handle.cancel(error).catch(
|
|
1098
|
+
function retainProviderStreamCleanupFailure() {}
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
this.stopAudio();
|
|
1102
|
+
throw isAIRequestAbort(error,signal)
|
|
1103
|
+
?normalizeAIRequestAbort(error)
|
|
1104
|
+
:error;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
711
1107
|
|
|
712
1108
|
return this.streamMessage(
|
|
713
1109
|
messages,
|
|
@@ -739,6 +1135,23 @@ class AI {
|
|
|
739
1135
|
requestHandler=function ignoreStreamRequest(){},
|
|
740
1136
|
structuredOutput=false
|
|
741
1137
|
){
|
|
1138
|
+
if(this.#shouldUseProviderRuntime('llm',this.llmService,false)){
|
|
1139
|
+
return this.streamRequest({
|
|
1140
|
+
messages,
|
|
1141
|
+
structuredOutput,
|
|
1142
|
+
localOnly:false,
|
|
1143
|
+
onChunk:streamHandler,
|
|
1144
|
+
onComplete:streamComplete,
|
|
1145
|
+
tools,
|
|
1146
|
+
toolChoice:tool_choice,
|
|
1147
|
+
onToolCall:earlyFunctionTrigger,
|
|
1148
|
+
onRequest:requestHandler,
|
|
1149
|
+
parallelToolCalls:parallel_tool_calls,
|
|
1150
|
+
id,
|
|
1151
|
+
seeThinking,
|
|
1152
|
+
signal
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
742
1155
|
let speechTurnCompleted=false;
|
|
743
1156
|
|
|
744
1157
|
try{
|
|
@@ -788,7 +1201,6 @@ class AI {
|
|
|
788
1201
|
if(nativeOllama){
|
|
789
1202
|
let nativeContent='';
|
|
790
1203
|
const nativeToolCalls={};
|
|
791
|
-
const nativeResponseChunks=[];
|
|
792
1204
|
const triggeredTools=new Set();
|
|
793
1205
|
const ollamaTools=this.#ollamaTools(tools,tool_choice);
|
|
794
1206
|
const ollamaMessages=this.#ollamaMessages(messages,tool_choice);
|
|
@@ -802,7 +1214,7 @@ class AI {
|
|
|
802
1214
|
};
|
|
803
1215
|
|
|
804
1216
|
function reportEarlyFunctionFailure(error){
|
|
805
|
-
console.error('Early tool trigger failed
|
|
1217
|
+
console.error('Early tool trigger failed.');
|
|
806
1218
|
}
|
|
807
1219
|
|
|
808
1220
|
function receiveNativeToolCalls(message={}){
|
|
@@ -832,7 +1244,7 @@ class AI {
|
|
|
832
1244
|
}
|
|
833
1245
|
}
|
|
834
1246
|
|
|
835
|
-
this.#reportRequest(requestHandler,ollamaRequest,id,{
|
|
1247
|
+
await this.#reportRequest(requestHandler,ollamaRequest,id,{
|
|
836
1248
|
operation:'stream',
|
|
837
1249
|
transport:'native',
|
|
838
1250
|
destination:'Arcane.ollama.chat'
|
|
@@ -844,7 +1256,6 @@ class AI {
|
|
|
844
1256
|
if(signal?.aborted){
|
|
845
1257
|
return;
|
|
846
1258
|
}
|
|
847
|
-
nativeResponseChunks.push(chunk);
|
|
848
1259
|
const message=chunk?.message||{};
|
|
849
1260
|
const thinking=seeThinking
|
|
850
1261
|
?String(message.thinking||'')
|
|
@@ -866,15 +1277,6 @@ class AI {
|
|
|
866
1277
|
signal
|
|
867
1278
|
}
|
|
868
1279
|
);
|
|
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
1280
|
if(signal?.aborted){
|
|
879
1281
|
throw normalizeAIRequestAbort();
|
|
880
1282
|
}
|
|
@@ -898,7 +1300,7 @@ class AI {
|
|
|
898
1300
|
return nativeResult;
|
|
899
1301
|
}
|
|
900
1302
|
|
|
901
|
-
this.#reportRequest(requestHandler,request,id,{
|
|
1303
|
+
await this.#reportRequest(requestHandler,request,id,{
|
|
902
1304
|
operation:'stream',
|
|
903
1305
|
transport:'http',
|
|
904
1306
|
destination:this.url
|
|
@@ -921,7 +1323,7 @@ class AI {
|
|
|
921
1323
|
if(signal?.aborted||err?.name==='AbortError'){
|
|
922
1324
|
const error=new Error('The AI request was cancelled.',{cause:err});
|
|
923
1325
|
error.name='AbortError';
|
|
924
|
-
error.code='
|
|
1326
|
+
error.code='ARCANE_AI_REQUEST_ABORTED';
|
|
925
1327
|
throw error;
|
|
926
1328
|
}
|
|
927
1329
|
const error=new Error(
|
|
@@ -936,7 +1338,6 @@ class AI {
|
|
|
936
1338
|
|
|
937
1339
|
let chunkString='';
|
|
938
1340
|
let chunkCache='';
|
|
939
|
-
const responseEvents=[];
|
|
940
1341
|
const streamedToolCalls=new Map();
|
|
941
1342
|
const triggeredTools=new Set();
|
|
942
1343
|
const decoder = new TextDecoder('utf-8');
|
|
@@ -969,7 +1370,7 @@ class AI {
|
|
|
969
1370
|
Promise.resolve(
|
|
970
1371
|
earlyFunctionTrigger(record.name)
|
|
971
1372
|
).catch(
|
|
972
|
-
|
|
1373
|
+
()=>console.error('Early tool trigger failed.')
|
|
973
1374
|
);
|
|
974
1375
|
}
|
|
975
1376
|
}
|
|
@@ -1007,14 +1408,12 @@ class AI {
|
|
|
1007
1408
|
chunkCache+=delta;
|
|
1008
1409
|
|
|
1009
1410
|
if (chunkCache.trim() === '[DONE]') {
|
|
1010
|
-
responseEvents.push('[DONE]');
|
|
1011
1411
|
chunkCache = '';
|
|
1012
1412
|
return;
|
|
1013
1413
|
}
|
|
1014
1414
|
|
|
1015
1415
|
try{
|
|
1016
1416
|
const resp=JSON.parse(chunkCache)||{};
|
|
1017
|
-
responseEvents.push(resp);
|
|
1018
1417
|
//console.log(JSON.stringify(resp));
|
|
1019
1418
|
//console.log(resp)
|
|
1020
1419
|
const choice = resp.choices?.[0] || {};
|
|
@@ -1058,7 +1457,7 @@ class AI {
|
|
|
1058
1457
|
receiveStreamedToolCalls(tool_calls);
|
|
1059
1458
|
}
|
|
1060
1459
|
} catch(err) {
|
|
1061
|
-
console.warn(
|
|
1460
|
+
console.warn('AI stream callback failed.');
|
|
1062
1461
|
}
|
|
1063
1462
|
}
|
|
1064
1463
|
);
|
|
@@ -1072,12 +1471,6 @@ class AI {
|
|
|
1072
1471
|
reader.releaseLock();
|
|
1073
1472
|
}
|
|
1074
1473
|
|
|
1075
|
-
this.#reportResponse(responseEvents,id,{
|
|
1076
|
-
operation:'stream',
|
|
1077
|
-
transport:'http',
|
|
1078
|
-
destination:this.url
|
|
1079
|
-
});
|
|
1080
|
-
|
|
1081
1474
|
const tool_funcs={};
|
|
1082
1475
|
const orderedToolCalls=[...streamedToolCalls.values()].sort(
|
|
1083
1476
|
function sortStreamedToolCalls(a,b){
|
|
@@ -1136,13 +1529,47 @@ class AI {
|
|
|
1136
1529
|
if(localOnly!==true&&localOnly!==false){
|
|
1137
1530
|
throw new TypeError('AI localOnly must be a boolean.');
|
|
1138
1531
|
}
|
|
1139
|
-
if(localOnly
|
|
1532
|
+
if(localOnly&&!this.#hasLocalRoute('llm',this.llmService)){
|
|
1140
1533
|
const error=new Error(
|
|
1141
1534
|
'This AI request requires a configured local model.'
|
|
1142
1535
|
);
|
|
1143
1536
|
error.code='AI_LOCAL_MODEL_REQUIRED';
|
|
1144
1537
|
throw error;
|
|
1145
1538
|
}
|
|
1539
|
+
if(this.#shouldUseProviderRuntime('llm',this.llmService,localOnly)){
|
|
1540
|
+
if(signal?.aborted){
|
|
1541
|
+
throw normalizeAIRequestAbort();
|
|
1542
|
+
}
|
|
1543
|
+
const request={
|
|
1544
|
+
messages,
|
|
1545
|
+
structuredOutput,
|
|
1546
|
+
tools,
|
|
1547
|
+
toolChoice,
|
|
1548
|
+
parallelToolCalls,
|
|
1549
|
+
id
|
|
1550
|
+
};
|
|
1551
|
+
await this.#reportRequest(onRequest,request,id);
|
|
1552
|
+
if(signal?.aborted){
|
|
1553
|
+
throw normalizeAIRequestAbort();
|
|
1554
|
+
}
|
|
1555
|
+
const response=await this.#providerRuntime.request(
|
|
1556
|
+
'llm',
|
|
1557
|
+
{
|
|
1558
|
+
operation:'chat',
|
|
1559
|
+
payload:request,
|
|
1560
|
+
localOnly,
|
|
1561
|
+
signal
|
|
1562
|
+
}
|
|
1563
|
+
);
|
|
1564
|
+
if(signal?.aborted){
|
|
1565
|
+
throw normalizeAIRequestAbort();
|
|
1566
|
+
}
|
|
1567
|
+
await onResponse(response,id,false);
|
|
1568
|
+
if(signal?.aborted){
|
|
1569
|
+
throw normalizeAIRequestAbort();
|
|
1570
|
+
}
|
|
1571
|
+
return response;
|
|
1572
|
+
}
|
|
1146
1573
|
|
|
1147
1574
|
return this.fetch(
|
|
1148
1575
|
messages,
|
|
@@ -1168,6 +1595,20 @@ class AI {
|
|
|
1168
1595
|
requestHandler=function ignoreFetchRequest(){},
|
|
1169
1596
|
signal=null,
|
|
1170
1597
|
){
|
|
1598
|
+
if(this.#shouldUseProviderRuntime('llm',this.llmService,false)){
|
|
1599
|
+
return this.fetchRequest({
|
|
1600
|
+
messages,
|
|
1601
|
+
structuredOutput,
|
|
1602
|
+
localOnly:false,
|
|
1603
|
+
tools,
|
|
1604
|
+
toolChoice:tool_choice,
|
|
1605
|
+
parallelToolCalls:parallel_tool_calls,
|
|
1606
|
+
id,
|
|
1607
|
+
signal,
|
|
1608
|
+
onRequest:requestHandler,
|
|
1609
|
+
onResponse:responseHandler
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1171
1612
|
this.#assertServiceConfigured(this.llmService);
|
|
1172
1613
|
if(signal&&(
|
|
1173
1614
|
typeof signal.aborted!=='boolean'
|
|
@@ -1215,7 +1656,7 @@ class AI {
|
|
|
1215
1656
|
...(structuredOutputFormat?{format:structuredOutputFormat}:{}),
|
|
1216
1657
|
...(ollamaTools.length?{tools:ollamaTools}:{})
|
|
1217
1658
|
};
|
|
1218
|
-
this.#reportRequest(requestHandler,nativeRequest,id,{
|
|
1659
|
+
await this.#reportRequest(requestHandler,nativeRequest,id,{
|
|
1219
1660
|
operation:'fetch',
|
|
1220
1661
|
transport:'native',
|
|
1221
1662
|
destination:'Arcane.ollama.chat'
|
|
@@ -1232,11 +1673,6 @@ class AI {
|
|
|
1232
1673
|
}
|
|
1233
1674
|
throw error;
|
|
1234
1675
|
}
|
|
1235
|
-
this.#reportResponse(nativeResponse,id,{
|
|
1236
|
-
operation:'fetch',
|
|
1237
|
-
transport:'native',
|
|
1238
|
-
destination:'Arcane.ollama.chat'
|
|
1239
|
-
});
|
|
1240
1676
|
if(signal?.aborted){
|
|
1241
1677
|
throw normalizeAIRequestAbort();
|
|
1242
1678
|
}
|
|
@@ -1258,7 +1694,7 @@ class AI {
|
|
|
1258
1694
|
return responseJSON;
|
|
1259
1695
|
}
|
|
1260
1696
|
|
|
1261
|
-
this.#reportRequest(requestHandler,request,id,{
|
|
1697
|
+
await this.#reportRequest(requestHandler,request,id,{
|
|
1262
1698
|
operation:'fetch',
|
|
1263
1699
|
transport:'http',
|
|
1264
1700
|
destination:this.url
|
|
@@ -1309,11 +1745,6 @@ class AI {
|
|
|
1309
1745
|
}
|
|
1310
1746
|
throw error;
|
|
1311
1747
|
}
|
|
1312
|
-
this.#reportResponse(responseJSON,id,{
|
|
1313
|
-
operation:'fetch',
|
|
1314
|
-
transport:'http',
|
|
1315
|
-
destination:this.url
|
|
1316
|
-
});
|
|
1317
1748
|
if(signal?.aborted){
|
|
1318
1749
|
throw normalizeAIRequestAbort();
|
|
1319
1750
|
}
|
|
@@ -1348,9 +1779,9 @@ class AI {
|
|
|
1348
1779
|
}
|
|
1349
1780
|
|
|
1350
1781
|
try{
|
|
1351
|
-
this.#assertServiceConfigured(this.ttsService);
|
|
1782
|
+
this.#assertServiceConfigured(this.ttsService,'tts');
|
|
1352
1783
|
}catch(error){
|
|
1353
|
-
console.warn('
|
|
1784
|
+
console.warn('AI speech provider is unavailable.');
|
|
1354
1785
|
return Promise.resolve(false);
|
|
1355
1786
|
}
|
|
1356
1787
|
|
|
@@ -1498,7 +1929,27 @@ class AI {
|
|
|
1498
1929
|
}
|
|
1499
1930
|
|
|
1500
1931
|
async #requestSpeechAudio(job){
|
|
1501
|
-
|
|
1932
|
+
if(this.#usesProviderRuntime('tts',this.ttsService)){
|
|
1933
|
+
job.abortController=new AbortController();
|
|
1934
|
+
const response=await this.#providerRuntime.request(
|
|
1935
|
+
'tts',
|
|
1936
|
+
{
|
|
1937
|
+
operation:'synthesize',
|
|
1938
|
+
payload:{
|
|
1939
|
+
model:this.#providerRuntime.selection('tts')?.modelId,
|
|
1940
|
+
voice:String(window.user?.AI_voice||'af_heart'),
|
|
1941
|
+
input:job.text,
|
|
1942
|
+
responseFormat:this.audioFormat,
|
|
1943
|
+
speed:this.voiceSpeed
|
|
1944
|
+
},
|
|
1945
|
+
localOnly:false,
|
|
1946
|
+
signal:job.abortController.signal
|
|
1947
|
+
}
|
|
1948
|
+
);
|
|
1949
|
+
return this.#normalizeProviderSpeechAudio(response);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
const nativeSpeech=this.#nativeSpeech(this.ttsService,'tts');
|
|
1502
1953
|
|
|
1503
1954
|
if(nativeSpeech){
|
|
1504
1955
|
const response=await nativeSpeech.synthesize({
|
|
@@ -1577,6 +2028,66 @@ class AI {
|
|
|
1577
2028
|
return {chunks,type:this.audioType};
|
|
1578
2029
|
}
|
|
1579
2030
|
|
|
2031
|
+
async #normalizeProviderSpeechAudio(response){
|
|
2032
|
+
if(response instanceof Blob){
|
|
2033
|
+
return {
|
|
2034
|
+
chunks:[new Uint8Array(await response.arrayBuffer())],
|
|
2035
|
+
type:response.type||this.audioType
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
if(response instanceof ArrayBuffer||ArrayBuffer.isView(response)){
|
|
2040
|
+
const bytes=response instanceof ArrayBuffer
|
|
2041
|
+
?new Uint8Array(response)
|
|
2042
|
+
:new Uint8Array(
|
|
2043
|
+
response.buffer,
|
|
2044
|
+
response.byteOffset,
|
|
2045
|
+
response.byteLength
|
|
2046
|
+
);
|
|
2047
|
+
return {
|
|
2048
|
+
chunks:[bytes],
|
|
2049
|
+
type:this.audioType
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
if(response&&typeof response==='object'){
|
|
2054
|
+
if(typeof response.audioBase64==='string'){
|
|
2055
|
+
return {
|
|
2056
|
+
chunks:[this.#base64ToBytes(response.audioBase64)],
|
|
2057
|
+
type:typeof response.contentType==='string'
|
|
2058
|
+
?response.contentType
|
|
2059
|
+
:this.audioType
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
if(response.audio instanceof Blob){
|
|
2063
|
+
return {
|
|
2064
|
+
chunks:[new Uint8Array(await response.audio.arrayBuffer())],
|
|
2065
|
+
type:response.audio.type
|
|
2066
|
+
||response.contentType
|
|
2067
|
+
||this.audioType
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
if(response.audio instanceof ArrayBuffer
|
|
2071
|
+
||ArrayBuffer.isView(response.audio)){
|
|
2072
|
+
const bytes=response.audio instanceof ArrayBuffer
|
|
2073
|
+
?new Uint8Array(response.audio)
|
|
2074
|
+
:new Uint8Array(
|
|
2075
|
+
response.audio.buffer,
|
|
2076
|
+
response.audio.byteOffset,
|
|
2077
|
+
response.audio.byteLength
|
|
2078
|
+
);
|
|
2079
|
+
return {
|
|
2080
|
+
chunks:[bytes],
|
|
2081
|
+
type:typeof response.contentType==='string'
|
|
2082
|
+
?response.contentType
|
|
2083
|
+
:this.audioType
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
throw new TypeError('Arcane returned an invalid provider speech response.');
|
|
2089
|
+
}
|
|
2090
|
+
|
|
1580
2091
|
#getSpeechAudioContext(){
|
|
1581
2092
|
if(this.audioContext&&this.audioContext.state!=='closed'){
|
|
1582
2093
|
return this.audioContext;
|
|
@@ -1594,11 +2105,50 @@ class AI {
|
|
|
1594
2105
|
|
|
1595
2106
|
async fetchSTT(
|
|
1596
2107
|
audioFile,
|
|
1597
|
-
responseHandler=(text='')=>{}
|
|
2108
|
+
responseHandler=(text='')=>{},
|
|
2109
|
+
signal=null
|
|
1598
2110
|
){
|
|
1599
|
-
this.#assertServiceConfigured(this.sttService);
|
|
2111
|
+
this.#assertServiceConfigured(this.sttService,'stt');
|
|
2112
|
+
if(signal&&(
|
|
2113
|
+
typeof signal.aborted!=='boolean'
|
|
2114
|
+
||typeof signal.addEventListener!=='function'
|
|
2115
|
+
)){
|
|
2116
|
+
throw new TypeError('AI request signal must be an AbortSignal.');
|
|
2117
|
+
}
|
|
2118
|
+
if(signal?.aborted){
|
|
2119
|
+
throw normalizeAIRequestAbort();
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
if(this.#usesProviderRuntime('stt',this.sttService)){
|
|
2123
|
+
if(!audioFile||typeof audioFile.arrayBuffer!=='function'){
|
|
2124
|
+
throw new TypeError('Speech transcription requires an audio Blob or File.');
|
|
2125
|
+
}
|
|
2126
|
+
const response=await this.#providerRuntime.request(
|
|
2127
|
+
'stt',
|
|
2128
|
+
{
|
|
2129
|
+
operation:'transcribe',
|
|
2130
|
+
payload:{
|
|
2131
|
+
audio:audioFile,
|
|
2132
|
+
mimeType:String(audioFile.type||'audio/webm'),
|
|
2133
|
+
model:this.#providerRuntime.selection('stt')?.modelId
|
|
2134
|
+
},
|
|
2135
|
+
localOnly:false,
|
|
2136
|
+
signal
|
|
2137
|
+
}
|
|
2138
|
+
);
|
|
2139
|
+
const text=typeof response==='string'
|
|
2140
|
+
?response
|
|
2141
|
+
:response?.text;
|
|
2142
|
+
if(typeof text!=='string'){
|
|
2143
|
+
throw new TypeError(
|
|
2144
|
+
'Arcane returned an invalid provider speech transcription.'
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
await responseHandler(text);
|
|
2148
|
+
return text;
|
|
2149
|
+
}
|
|
1600
2150
|
|
|
1601
|
-
const nativeSpeech=this.#nativeSpeech(this.sttService);
|
|
2151
|
+
const nativeSpeech=this.#nativeSpeech(this.sttService,'stt');
|
|
1602
2152
|
|
|
1603
2153
|
if(nativeSpeech){
|
|
1604
2154
|
if(!audioFile||typeof audioFile.arrayBuffer!=='function'){
|
|
@@ -1632,7 +2182,8 @@ class AI {
|
|
|
1632
2182
|
method: 'POST',
|
|
1633
2183
|
credentials: credentials,
|
|
1634
2184
|
headers: this.#sttHeaders[this.sttService],
|
|
1635
|
-
body: formData
|
|
2185
|
+
body: formData,
|
|
2186
|
+
signal
|
|
1636
2187
|
}
|
|
1637
2188
|
);
|
|
1638
2189
|
|
|
@@ -1672,7 +2223,7 @@ class AI {
|
|
|
1672
2223
|
try{
|
|
1673
2224
|
sourceNode.stop();
|
|
1674
2225
|
}catch(error){
|
|
1675
|
-
console.warn('
|
|
2226
|
+
console.warn('AI audio could not be stopped cleanly.');
|
|
1676
2227
|
}
|
|
1677
2228
|
}
|
|
1678
2229
|
|
|
@@ -1798,7 +2349,7 @@ class AI {
|
|
|
1798
2349
|
#requestSpeechPlayback(){
|
|
1799
2350
|
this.#pumpSpeechPlayback().catch(
|
|
1800
2351
|
function reportSpeechPlaybackFailure(error){
|
|
1801
|
-
console.warn('
|
|
2352
|
+
console.warn('AI audio playback failed.');
|
|
1802
2353
|
}
|
|
1803
2354
|
);
|
|
1804
2355
|
}
|
|
@@ -1925,7 +2476,7 @@ class AI {
|
|
|
1925
2476
|
}
|
|
1926
2477
|
|
|
1927
2478
|
if(job.generation===this.speechGeneration&&error?.name!=='AbortError'){
|
|
1928
|
-
console.warn('
|
|
2479
|
+
console.warn('AI speech synthesis failed.');
|
|
1929
2480
|
}
|
|
1930
2481
|
|
|
1931
2482
|
this.#requestSpeechPlayback();
|
|
@@ -1974,7 +2525,7 @@ class AI {
|
|
|
1974
2525
|
);
|
|
1975
2526
|
|
|
1976
2527
|
if(error?.name&&error.name!=='NotAllowedError'){
|
|
1977
|
-
console.info('AI speech is waiting for audio playback permission
|
|
2528
|
+
console.info('AI speech is waiting for audio playback permission.');
|
|
1978
2529
|
}
|
|
1979
2530
|
|
|
1980
2531
|
return true;
|