arcane-os 0.29.1 → 0.30.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.30.0
4
+
5
+ - Add the public `arcane-os/ai/twin-cloud` subpath with stateless `fetchRequest`
6
+ for Node and browser callers. Supply the TWiN key, model, messages, optional
7
+ structured-output schema and cancellation signal explicitly. The result is
8
+ the complete parsed provider completion, with no browser startup, saved
9
+ conversation, hidden model default or output cap.
10
+ - Share the existing TWiN HTTP, structured JSON, overload retry and cancellation
11
+ implementation with the browser AI owner while preserving its public methods
12
+ and lifecycle. Only overload responses with HTTP 429 use the existing
13
+ three-second retry; other failures retain their complete provider response.
14
+
3
15
  ## 0.29.1
4
16
 
5
17
  - Preserve complete long and non-ASCII filenames in application release bundles
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.29.1` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.30.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -0,0 +1,230 @@
1
+ import Is from 'strong-type';
2
+ import {arcaneLogging} from '../logging.mjs';
3
+
4
+ const is = new Is(false);
5
+ const twinChatURL = 'https://inference.do-ai.run/v1/chat/completions';
6
+
7
+ /** A stateless TWiN request; browser AI shares the HTTP and format owners below. */
8
+ export async function fetchRequest({
9
+ twinKey,
10
+ model,
11
+ messages = [],
12
+ structuredOutput = false,
13
+ tools = [],
14
+ toolChoice = 'auto',
15
+ parallelToolCalls,
16
+ reasoningEffort,
17
+ signal = null,
18
+ id = Date.now(),
19
+ onRequest = function observeTWiNRequest(){},
20
+ onResponse = function observeTWiNResponse(){}
21
+ } = {}){
22
+ if(signal?.aborted){
23
+ throw normalizeAIRequestAbort(signal.reason);
24
+ }
25
+ if(!is.string(twinKey) || !twinKey){
26
+ const error = new Error('AI provider is not configured.');
27
+ error.code = 'AI_PROVIDER_NOT_CONFIGURED';
28
+ throw error;
29
+ }
30
+ if(!is.string(model) || !model){
31
+ throw new TypeError('TWiN fetchRequest requires an explicit model.');
32
+ }
33
+
34
+ const request = {model, messages, stream:false};
35
+ const format = structuredOutputFormat(structuredOutput);
36
+ if(format){
37
+ request.response_format = openAIResponseFormat(format);
38
+ }
39
+ if(tools.length){
40
+ request.tools = tools;
41
+ request.tool_choice = toolChoice;
42
+ if(parallelToolCalls !== undefined){
43
+ request.parallel_tool_calls = parallelToolCalls;
44
+ }
45
+ }
46
+ if(reasoningEffort){
47
+ request.reasoning_effort = reasoningEffort;
48
+ }
49
+
50
+ try{
51
+ await onRequest(
52
+ request,
53
+ id,
54
+ {operation:'fetch', transport:'http', destination:twinChatURL}
55
+ );
56
+ if(signal?.aborted){
57
+ throw normalizeAIRequestAbort(signal.reason);
58
+ }
59
+ const response = await fetchJSONResponse(
60
+ twinChatURL,
61
+ {
62
+ method:'POST',
63
+ credentials:'omit',
64
+ headers:{
65
+ 'Content-Type':'application/json',
66
+ Authorization:`Bearer ${twinKey}`
67
+ },
68
+ body:JSON.stringify(request),
69
+ ...(signal ? {signal} : {})
70
+ }
71
+ );
72
+ if(signal?.aborted){
73
+ throw normalizeAIRequestAbort(signal.reason);
74
+ }
75
+ await onResponse(response, id, false);
76
+ if(signal?.aborted){
77
+ throw normalizeAIRequestAbort(signal.reason);
78
+ }
79
+ return response;
80
+ }catch(error){
81
+ if(isAIRequestAbort(error, signal)){
82
+ throw normalizeAIRequestAbort(error);
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ export function isAIRequestAbort(error, signal){
89
+ return signal?.aborted
90
+ || error?.name === 'AbortError'
91
+ || error?.code === 'ARCANE_REQUEST_ABORTED'
92
+ || error?.code === 'ARCANE_AI_REQUEST_ABORTED'
93
+ || error?.code === 'AI_REQUEST_ABORTED';
94
+ }
95
+
96
+ export function normalizeAIRequestAbort(error){
97
+ if(error?.code === 'ARCANE_AI_REQUEST_ABORTED'){
98
+ return error;
99
+ }
100
+ const normalized = new Error('The AI request was cancelled.', {cause:error});
101
+ normalized.name = 'AbortError';
102
+ normalized.code = 'ARCANE_AI_REQUEST_ABORTED';
103
+ return normalized;
104
+ }
105
+
106
+ export function structuredOutputFormat(value = false){
107
+ if(value === false || value === null || value === undefined){
108
+ return null;
109
+ }
110
+ if(value === true || value === 'json'){
111
+ return 'json';
112
+ }
113
+ if(
114
+ is.object(value)
115
+ && !is.array(value)
116
+ && (
117
+ Object.getPrototypeOf(value) === Object.prototype
118
+ || Object.getPrototypeOf(value) === null
119
+ )
120
+ ){
121
+ return value;
122
+ }
123
+ const error = new TypeError(
124
+ 'AI structured output must be enabled with true, json, or a JSON Schema object.'
125
+ );
126
+ error.code = 'AI_STRUCTURED_OUTPUT_INVALID';
127
+ throw error;
128
+ }
129
+
130
+ export function openAIResponseFormat(format){
131
+ if(format === 'json'){
132
+ return {type:'json_object'};
133
+ }
134
+ if(format){
135
+ return {
136
+ type:'json_schema',
137
+ json_schema:{name:'structured_response', strict:true, schema:format}
138
+ };
139
+ }
140
+ return null;
141
+ }
142
+
143
+ /** Shared with browser streaming; consume no successful body at this boundary. */
144
+ export async function fetchHTTPResponse(url, options){
145
+ const {signal} = options;
146
+ const retryDelayMs = 3000;
147
+ try{
148
+ while(true){
149
+ if(signal?.aborted){
150
+ throw normalizeAIRequestAbort(signal.reason);
151
+ }
152
+ const response = await fetch(url, options);
153
+ if(signal?.aborted){
154
+ throw normalizeAIRequestAbort(signal.reason);
155
+ }
156
+ if(response.ok){
157
+ return response;
158
+ }
159
+ const contentType = response.headers.get('content-type') || '';
160
+ const error = contentType.includes('application/json')
161
+ ? await response.json()
162
+ : await response.text();
163
+ if(signal?.aborted){
164
+ throw normalizeAIRequestAbort(signal.reason);
165
+ }
166
+ const message = is.string(error)
167
+ ? error
168
+ : error?.error?.message ?? error?.message;
169
+ if(
170
+ response.status !== 429
171
+ || !is.string(message)
172
+ || !message.toLowerCase().includes('overload')
173
+ ){
174
+ throw error;
175
+ }
176
+ arcaneLogging.warn(
177
+ `${message}\nRetrying in ${retryDelayMs / 1000} seconds`,
178
+ error
179
+ );
180
+ await new Promise(function waitForOverloadRetry(resolve, reject){
181
+ function finishRetryDelay(){
182
+ signal?.removeEventListener('abort', cancelRetryDelay);
183
+ resolve();
184
+ }
185
+ function cancelRetryDelay(){
186
+ clearTimeout(timer);
187
+ signal.removeEventListener('abort', cancelRetryDelay);
188
+ reject(normalizeAIRequestAbort(signal.reason));
189
+ }
190
+ const timer = setTimeout(finishRetryDelay, retryDelayMs);
191
+ signal?.addEventListener('abort', cancelRetryDelay, {once:true});
192
+ if(signal?.aborted){
193
+ cancelRetryDelay();
194
+ }
195
+ });
196
+ }
197
+ }catch(error){
198
+ if(isAIRequestAbort(error, signal)){
199
+ throw normalizeAIRequestAbort(error);
200
+ }
201
+ throw error;
202
+ }
203
+ }
204
+
205
+ /** Return the entire parsed completion without selecting or rewriting choices. */
206
+ export async function fetchJSONResponse(url, options){
207
+ const {signal} = options;
208
+ try{
209
+ const response = await fetchHTTPResponse(url, options);
210
+ if(signal?.aborted){
211
+ throw normalizeAIRequestAbort(signal.reason);
212
+ }
213
+ const contentType = response.headers.get('content-type') || '';
214
+ if(!contentType.includes('application/json')){
215
+ throw new TypeError(
216
+ `AI request returned ${contentType || 'an unknown content type'} instead of JSON.`
217
+ );
218
+ }
219
+ const completion = await response.json();
220
+ if(signal?.aborted){
221
+ throw normalizeAIRequestAbort(signal.reason);
222
+ }
223
+ return completion;
224
+ }catch(error){
225
+ if(isAIRequestAbort(error, signal)){
226
+ throw normalizeAIRequestAbort(error);
227
+ }
228
+ throw error;
229
+ }
230
+ }
@@ -1,8 +1,125 @@
1
1
  # TWiN Cloud: one request
2
2
 
3
- TWiN Cloud is the high-level `AI.js` default remote LLM service, named `TWIN`.
4
- Speech stays on device and does not use the TWiN access key. This guide uses the
5
- same managed browser imports as the [browser speech quick start](browser-speech.md).
3
+ Use `fetchRequest` from `arcane-os/ai/twin-cloud` for a complete TWiN Cloud
4
+ request in Node or a browser with an explicit key and model. It imports no
5
+ browser profile, DOM, user singleton, or storage, and starts no work on import.
6
+ The existing browser `AI.js` interface remains available for applications that
7
+ already use its provider selection, lifecycle, and speech. TWiN Cloud is that
8
+ interface's default remote LLM service, named `TWIN`; speech stays on device
9
+ and does not use the TWiN access key.
10
+
11
+ ## Node: explicit key, model, and structured result
12
+
13
+ Install the published `arcane-os` package in the Node project. Keep the key in
14
+ the application's existing server configuration, outside source control and
15
+ diagnostics. In this example, `server-config.json` is that caller-owned local
16
+ configuration file with a `twinKey` property; add its exact path to `.gitignore`
17
+ before creating it. The SDK does not discover or write this file.
18
+
19
+ ```javascript
20
+ import serverConfig from './server-config.json' with {type: 'json'};
21
+ import {fetchRequest} from 'arcane-os/ai/twin-cloud';
22
+
23
+ const response = await fetchRequest({
24
+ twinKey: serverConfig.twinKey,
25
+ model: 'openai-gpt-oss-20b',
26
+ messages: [{
27
+ role: 'user',
28
+ content: 'Explain why the moon-powered toaster keeps burning breakfast. Return HTML and plain text.'
29
+ }],
30
+ structuredOutput: {
31
+ type: 'object',
32
+ properties: {
33
+ html: {type: 'string'},
34
+ text: {type: 'string'}
35
+ },
36
+ required: ['html', 'text'],
37
+ additionalProperties: false
38
+ }
39
+ });
40
+
41
+ console.log(response);
42
+ ```
43
+
44
+ `model` is required and remains exactly the supplied identifier. This function
45
+ does not select the browser profile's default model. The example's prompt and
46
+ `html`/`text` schema are caller-owned data, not SDK business logic. Changing
47
+ those fields changes the requested result without changing the SDK.
48
+
49
+ The resolved value is the complete parsed provider JSON, including every
50
+ choice and provider field. The SDK does not extract only the first message,
51
+ parse its content into a second object, or replace the response with an
52
+ application-specific record. The supplied schema becomes
53
+ `response_format: {type:'json_schema', json_schema:{name:'structured_response',
54
+ strict:true, schema:...}}`. `structuredOutput:true` or `'json'` instead selects
55
+ `response_format:{type:'json_object'}`; omission leaves structured output off.
56
+
57
+ ## Shared request behavior
58
+
59
+ The focused API accepts complete `messages`, optional `tools`, `toolChoice`,
60
+ `parallelToolCalls`, and `reasoningEffort` in addition to the explicit
61
+ `twinKey` and `model`. Tool options use the existing chat-completion wire fields
62
+ `tools`, `tool_choice`, and `parallel_tool_calls` when `tools` is nonempty.
63
+ A supplied nonempty `reasoningEffort` uses the provider's `reasoning_effort`
64
+ field; omission preserves its default. No output limit is added by this API.
65
+ The function neither executes tools nor adds provider-response envelope
66
+ validation.
67
+
68
+ Optional `id`, `onRequest(request,id,metadata)`, and
69
+ `onResponse(response,id,false)` follow the complete-response `AI.fetchRequest`
70
+ callback shape. The request callback runs before dispatch; the response
71
+ callback receives the complete parsed result before it is returned. Omitted
72
+ `id` uses `Date.now()`; request metadata is
73
+ `{operation:'fetch',transport:'http',destination:'https://inference.do-ai.run/v1/chat/completions'}`.
74
+ The key is
75
+ transport authentication, not part of either callback's message payload. Keep
76
+ credentials out of application logging as well.
77
+
78
+ Only HTTP `429` with a message containing `overload` (case-insensitive) repeats automatically,
79
+ after `3000` milliseconds. Another overload repeats the same complete request;
80
+ other HTTP failures do not become an automatic retry loop. Pass a fresh
81
+ `AbortController`'s `signal` and call `abort()` to cancel. Cancellation during
82
+ the request, response-body read, retry wait, or callback settlement prevents
83
+ successful result delivery and rejects with `ARCANE_AI_REQUEST_ABORTED`.
84
+ Other HTTP failures throw the complete parsed JSON error body or text body.
85
+ A missing key uses `AI_PROVIDER_NOT_CONFIGURED`, a missing explicit model
86
+ throws `TypeError`, and an unsupported `structuredOutput` input uses
87
+ `AI_STRUCTURED_OUTPUT_INVALID`.
88
+
89
+ The SDK retains no request or response history between calls and uses no
90
+ DBOPFS, chat entity, or memory extraction. Each call's `messages` are its
91
+ complete caller-supplied context. The caller decides whether and how to keep
92
+ the result; this stateless transport adds no saved conversation or migration.
93
+
94
+ Browser applications can use this same focused import through their generated
95
+ managed import map. Browser Fetch and CORS behavior still apply. Importing it
96
+ does not instantiate `AI`, read saved preferences, configure speech, or change
97
+ the existing `arcane-os/ai` browser entry.
98
+
99
+ ### Shared low-level integration helpers
100
+
101
+ The same module also exports the helpers used by browser `AI.js`. Ordinary
102
+ callers use `fetchRequest`; these exports let SDK transport integration share
103
+ the existing implementation rather than maintain another retry or body reader.
104
+
105
+ | Export | Contract |
106
+ | --- | --- |
107
+ | `fetchHTTPResponse(url,options)` | Uses the caller's Fetch options, overload retry and cancellation; returns a successful `Response` with its body unconsumed. |
108
+ | `fetchJSONResponse(url,options)` | Uses that HTTP owner, requires `application/json`, and returns the complete parsed body without selecting choices. |
109
+ | `structuredOutputFormat(value=false)` | Maps false/null/undefined to null, true/`'json'` to `'json'`, and preserves a supplied plain JSON Schema object. Other inputs use `AI_STRUCTURED_OUTPUT_INVALID`. |
110
+ | `openAIResponseFormat(format)` | Maps the normalized value to `json_object`, strict `json_schema` named `structured_response`, or null. |
111
+ | `isAIRequestAbort(error,signal)` | Recognizes an aborted signal, `AbortError`, or the existing Arcane AI/request cancellation codes. |
112
+ | `normalizeAIRequestAbort(error)` | Preserves an existing `ARCANE_AI_REQUEST_ABORTED` error or creates that `AbortError` with the original value as its cause. |
113
+
114
+ These helpers start no work on import. HTTP helpers require explicit URL and
115
+ options; they do not add a key, model, browser state, or retained conversation.
116
+ Overload warnings use the shared console logger and preserve the complete
117
+ provider error.
118
+
119
+ ## Existing browser AI interface
120
+
121
+ The following browser example uses the same managed imports as the
122
+ [browser speech quick start](browser-speech.md).
6
123
 
7
124
  ## Install and import
8
125
 
@@ -78,6 +78,7 @@ version; WebKitGTK availability must not be generalized to macOS.
78
78
  | Read host identity, capabilities, storage, preferences, appearance, or platform state | `globalThis.Arcane` | **Cross-host** where the method is implemented and admitted | Promise behavior and `Arcane.Error` are normalized. Result fields are normalized unless the method explicitly documents a platform-dependent snapshot. |
79
79
  | Use local AI without coupling app code to Ollama HTTP | `Arcane.localAI`, `Arcane.ai`, or `/arcane/modules/Ollama.js` | Primarily **Native**; Android exposes a narrower admitted inference projection | Admission, errors, and managed-operation events are normalized. Direct Ollama response envelopes remain **Provider-native**. |
80
80
  | Use TWiN Cloud from the renderer profile | `/arcane/modules/AI.js` | **Cloud** from an allowed browser/native renderer | High-level chat behavior is normalized by the module. The TWiN access key authenticates remote LLM chat; raw provider diagnostics remain provider-specific. No automatic cloud fallback is inferred from local failure. |
81
+ | Send one TWiN Cloud request with an explicit key and model | `fetchRequest` from `arcane-os/ai/twin-cloud` | **Node** and **Browser**, using standard Fetch and a remote HTTPS provider | Keeps complete messages and returns the full parsed provider JSON. Shared structured-output mapping, overload-only HTTP 429 retry after 3000 ms, and cancellation match browser TWiN transport. No browser profile, AI/user singleton, DBOPFS, or retained request history is created. |
81
82
  | Use speech through one application helper | `/arcane/modules/AI.js` and `Arcane.speech` | **Browser** or **Native** | The helper keeps audio on device: Whisper owns STT and Kokoro owns TTS. It automatically cleans only the outbound speech-input copy and normalizes application-facing audio/text behavior while browser and native request/response plumbing differs below that boundary. |
82
83
  | Inspect or manage raw Ollama models | `Arcane.ollama` or `/arcane/modules/Ollama.js` | **Native** desktop Core for management; narrower Android inference only | Wrapper method names, errors, streaming correlation, and admission are Arcane-controlled. Direct Ollama success envelopes are intentionally provider-native. |
83
84
  | Use native terminal, installation, user, provisioning, or machine controls | matching `Arcane.*` namespace | **Native** and app/capability restricted | Calls and errors use the common bridge contract. Platform results can be host-specific and are marked in the method guide. |
@@ -240,6 +241,14 @@ snapshot.
240
241
 
241
242
  ### Provider-native within an Arcane boundary
242
243
 
244
+ [`arcane-os/ai/twin-cloud`](ai/twin-cloud.md) accepts an explicit `twinKey` and
245
+ `model`, with the same named `fetchRequest` import in Node and managed browsers.
246
+ It preserves complete provider response JSON while mapping the supplied
247
+ structured-output, tool, and reasoning options to the TWiN wire contract.
248
+ The SDK adds no output cap or persisted context. Cancellation applies during
249
+ request, body read, overload retry wait, and callback settlement. The existing
250
+ profile-backed browser `AI.js` entry and its lifecycle remain separate.
251
+
243
252
  Direct `Arcane.ollama.chat()`, `generate()`, `show()`, `embed()`, and lifecycle
244
253
  methods return complete Ollama-compatible envelopes. Arcane still owns error
245
254
  normalization, chunk correlation, and host transport, but it does
@@ -6,7 +6,7 @@
6
6
  "minimumVersion": "22.23.2 for Node entrypoints",
7
7
  "moduleSystem": "ESM"
8
8
  },
9
- "memberCount": 210,
9
+ "memberCount": 217,
10
10
  "runtimeSubpathPatterns": {
11
11
  "arcane-os/modules/*": "./runtime/arcane/modules/*",
12
12
  "arcane-os/entities/*": "./runtime/arcane/entities/*"
@@ -3395,6 +3395,104 @@
3395
3395
  "availability": "Node and browser",
3396
3396
  "protocol": "Shared user.developer preference",
3397
3397
  "normalization": "Returns null until target.user.ready is true, then whether target.user.developer is exactly true; returns false if preference access throws"
3398
+ },
3399
+ {
3400
+ "id": "twin-cloud:fetchRequest",
3401
+ "name": "fetchRequest",
3402
+ "displayName": "fetchRequest()",
3403
+ "kind": "function",
3404
+ "signature": "async fetchRequest(options={})",
3405
+ "entrypoints": ["arcane-os/ai/twin-cloud"],
3406
+ "primaryImport": "arcane-os/ai/twin-cloud",
3407
+ "group": "TWiN Cloud requests",
3408
+ "summary": "Sends one complete TWiN Cloud request with an explicit caller-owned key and model.",
3409
+ "availability": "Node and Browser; remote HTTPS provider",
3410
+ "protocol": "TWiN Cloud complete-response chat",
3411
+ "normalization": "Preserves complete messages and parsed provider JSON; maps explicit structured-output, tool, and reasoning options; shares overload-only HTTP 429 retry after 3000 ms and cancellation; retains no browser profile, storage, or request history"
3412
+ },
3413
+ {
3414
+ "id": "twin-cloud:fetchHTTPResponse",
3415
+ "name": "fetchHTTPResponse",
3416
+ "displayName": "fetchHTTPResponse()",
3417
+ "kind": "function",
3418
+ "signature": "async fetchHTTPResponse(url,options)",
3419
+ "entrypoints": ["arcane-os/ai/twin-cloud"],
3420
+ "primaryImport": "arcane-os/ai/twin-cloud",
3421
+ "group": "Shared AI transport integration",
3422
+ "summary": "Returns a successful unconsumed Fetch Response through the shared AI HTTP owner.",
3423
+ "availability": "Node and Browser",
3424
+ "protocol": "Shared AI HTTP transport",
3425
+ "normalization": "Preserves caller Fetch options and complete error bodies; repeats only overload HTTP 429 after 3000 ms; signal cancellation uses ARCANE_AI_REQUEST_ABORTED"
3426
+ },
3427
+ {
3428
+ "id": "twin-cloud:fetchJSONResponse",
3429
+ "name": "fetchJSONResponse",
3430
+ "displayName": "fetchJSONResponse()",
3431
+ "kind": "function",
3432
+ "signature": "async fetchJSONResponse(url,options)",
3433
+ "entrypoints": ["arcane-os/ai/twin-cloud"],
3434
+ "primaryImport": "arcane-os/ai/twin-cloud",
3435
+ "group": "Shared AI transport integration",
3436
+ "summary": "Reads the complete successful provider JSON through the shared HTTP owner.",
3437
+ "availability": "Node and Browser",
3438
+ "protocol": "Shared AI HTTP transport",
3439
+ "normalization": "Requires application/json, retains every parsed field without envelope validation, and checks cancellation before delivery"
3440
+ },
3441
+ {
3442
+ "id": "twin-cloud:structuredOutputFormat",
3443
+ "name": "structuredOutputFormat",
3444
+ "displayName": "structuredOutputFormat()",
3445
+ "kind": "function",
3446
+ "signature": "structuredOutputFormat(value=false)",
3447
+ "entrypoints": ["arcane-os/ai/twin-cloud"],
3448
+ "primaryImport": "arcane-os/ai/twin-cloud",
3449
+ "group": "Shared AI transport integration",
3450
+ "summary": "Normalizes the existing structured-output option while preserving a supplied schema.",
3451
+ "availability": "Node and Browser",
3452
+ "protocol": "Shared AI structured-output options",
3453
+ "normalization": "False/null/undefined return null, true/json return json, a plain schema object is returned unchanged, and other inputs use AI_STRUCTURED_OUTPUT_INVALID"
3454
+ },
3455
+ {
3456
+ "id": "twin-cloud:openAIResponseFormat",
3457
+ "name": "openAIResponseFormat",
3458
+ "displayName": "openAIResponseFormat()",
3459
+ "kind": "function",
3460
+ "signature": "openAIResponseFormat(format)",
3461
+ "entrypoints": ["arcane-os/ai/twin-cloud"],
3462
+ "primaryImport": "arcane-os/ai/twin-cloud",
3463
+ "group": "Shared AI transport integration",
3464
+ "summary": "Maps normalized structured-output settings to chat-completion response_format.",
3465
+ "availability": "Node and Browser",
3466
+ "protocol": "Shared AI structured-output options",
3467
+ "normalization": "Returns json_object, strict json_schema named structured_response with the original schema, or null; does not validate schema contents"
3468
+ },
3469
+ {
3470
+ "id": "twin-cloud:isAIRequestAbort",
3471
+ "name": "isAIRequestAbort",
3472
+ "displayName": "isAIRequestAbort()",
3473
+ "kind": "function",
3474
+ "signature": "isAIRequestAbort(error,signal)",
3475
+ "entrypoints": ["arcane-os/ai/twin-cloud"],
3476
+ "primaryImport": "arcane-os/ai/twin-cloud",
3477
+ "group": "Shared AI transport integration",
3478
+ "summary": "Recognizes an aborted signal or an existing AI request cancellation error.",
3479
+ "availability": "Node and Browser",
3480
+ "protocol": "Shared AI cancellation",
3481
+ "normalization": "Returns a boolean for AbortError or ARCANE_REQUEST_ABORTED, ARCANE_AI_REQUEST_ABORTED, and AI_REQUEST_ABORTED; changes no operation"
3482
+ },
3483
+ {
3484
+ "id": "twin-cloud:normalizeAIRequestAbort",
3485
+ "name": "normalizeAIRequestAbort",
3486
+ "displayName": "normalizeAIRequestAbort()",
3487
+ "kind": "function",
3488
+ "signature": "normalizeAIRequestAbort(error)",
3489
+ "entrypoints": ["arcane-os/ai/twin-cloud"],
3490
+ "primaryImport": "arcane-os/ai/twin-cloud",
3491
+ "group": "Shared AI transport integration",
3492
+ "summary": "Returns the common AI AbortError while preserving the original cause.",
3493
+ "availability": "Node and Browser",
3494
+ "protocol": "Shared AI cancellation",
3495
+ "normalization": "Preserves an existing ARCANE_AI_REQUEST_ABORTED value or returns an AbortError with that code and the supplied cause"
3398
3496
  }
3399
3497
  ]
3400
3498
  }
@@ -3,7 +3,7 @@
3
3
  The npm package exposes a Node.js ESM control plane, the portable
4
4
  `arcane-os/event-manager`, `arcane-os/logging`, `arcane-os/mail`,
5
5
  `arcane-os/preference-store`, `arcane-os/speech-playback`,
6
- `arcane-os/speech-text`, `arcane-os/ai/tool-text-stream`, and `arcane-os/browser-device` entrypoints, and the browser-only
6
+ `arcane-os/speech-text`, `arcane-os/ai/tool-text-stream`, `arcane-os/ai/twin-cloud`, and `arcane-os/browser-device` entrypoints, and the browser-only
7
7
  `arcane-os/pwa`, `arcane-os/ai/browser-wasm` and `arcane-os/ai/browser-speech` entrypoints.
8
8
  The `arcane-os/modules/<filename>` and `arcane-os/entities/<filename>` paths
9
9
  resolve directly to the existing runtime files, with their actual extension.
@@ -66,6 +66,7 @@ runtime layouts.
66
66
  | `arcane-os/pwa` | Nonblocking PWA registration, worker updates, native installation state and a dismissible installation component. |
67
67
  | `arcane-os/ai/browser-wasm` | Caller-selected browser-local Wllama inference, complete DBOPFS model storage, streaming, cancellation, and structural tool-call results. |
68
68
  | `arcane-os/ai/tool-text-stream` | Shared selected tool-argument text observer for provider integration. |
69
+ | `arcane-os/ai/twin-cloud` | Complete TWiN Cloud requests from Node or a browser with an explicit key/model and shared retry/cancellation behavior. |
69
70
  | `arcane-os/ai/browser-speech` | Caller-selected browser-local Whisper STT and Kokoro TTS provider mechanisms, ordinary upstream assets, materialized/native routing, Workers, and cancellation. |
70
71
  | `arcane-os/mail` | Portable Mail runtime, durable outbox, complete transport responses, and provider-neutral acceptance contracts. |
71
72
 
@@ -121,6 +122,13 @@ browser map are cataloged separately in [Runtime modules](runtime-modules.md).
121
122
 
122
123
  | Member | Kind | Import | Group | Availability |
123
124
  | --- | --- | --- | --- | --- |
125
+ | `fetchRequest()` | function | `arcane-os/ai/twin-cloud` | TWiN Cloud requests | Node and Browser; remote HTTPS provider |
126
+ | `fetchHTTPResponse()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
127
+ | `fetchJSONResponse()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
128
+ | `structuredOutputFormat()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
129
+ | `openAIResponseFormat()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
130
+ | `isAIRequestAbort()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
131
+ | `normalizeAIRequestAbort()` | function | `arcane-os/ai/twin-cloud` | Shared AI transport integration | Node and Browser |
124
132
  | `APP_BUNDLE_DESCRIPTOR_NAME` | constant | `arcane-os` | Packaging and release bundles | Node |
125
133
  | `APP_BUNDLE_EXTENSION` | constant | `arcane-os` | Packaging and release bundles | Node |
126
134
  | `APP_BUNDLE_FORMAT` | constant | `arcane-os` | Packaging and release bundles | Node |
@@ -7636,6 +7644,265 @@ import {readArcaneDeveloperMode} from 'arcane-os/logging';
7636
7644
  const developerMode=readArcaneDeveloperMode();
7637
7645
  ```
7638
7646
 
7647
+ ## fetchRequest()
7648
+
7649
+ ### Overview
7650
+
7651
+ Makes one complete TWiN Cloud request with explicit caller-owned credentials
7652
+ and model selection. This named function is independent of the browser
7653
+ `AI` instance method with the same name.
7654
+
7655
+ ### Signature and result
7656
+
7657
+ ```text
7658
+ async fetchRequest(options={})
7659
+ ```
7660
+
7661
+ Import the named function from `arcane-os/ai/twin-cloud`. Supply `twinKey`,
7662
+ `model`, and complete `messages`. The function requires the explicit model;
7663
+ it reads no browser preference or default-model selection. Optional
7664
+ `structuredOutput:true` or `'json'` sends `response_format:{type:'json_object'}`.
7665
+ A supplied JSON Schema sends `response_format:{type:'json_schema',
7666
+ json_schema:{name:'structured_response',strict:true,schema:...}}`. Omission
7667
+ leaves structured output off. Optional `tools`, `toolChoice`,
7668
+ `parallelToolCalls`, and `reasoningEffort` map to their existing TWiN wire
7669
+ fields. Tool fields are sent when `tools` is nonempty; a nonempty
7670
+ `reasoningEffort` is forwarded without selecting a default. No default output
7671
+ cap, tool execution, or provider-response envelope validation is added.
7672
+
7673
+ The return value is the entire parsed provider JSON, not only one choice or
7674
+ message. Optional callbacks are `onRequest(request,id,metadata)` before
7675
+ dispatch and `onResponse(response,id,false)` before successful return;
7676
+ `id` defaults to `Date.now()` and may be supplied by the caller. Request metadata
7677
+ is `{operation:'fetch',transport:'http',destination:'https://inference.do-ai.run/v1/chat/completions'}`.
7678
+ Both callbacks are awaited, and callback
7679
+ failures propagate. Credentials are supplied to transport rather than added to
7680
+ the message or response callback payload.
7681
+
7682
+ ### Availability and normalization
7683
+
7684
+ **Node and Browser; Cloud transport.** Uses standard Fetch and cancellation,
7685
+ without DOM, browser profiles, user singletons, or storage initialization.
7686
+ Import starts no request. HTTP `429` whose message contains `overload`
7687
+ (case-insensitive) waits
7688
+ `3000` milliseconds and retries the complete request; other HTTP failures
7689
+ throw their complete parsed JSON or text bodies. A missing key throws
7690
+ `AI_PROVIDER_NOT_CONFIGURED`; a missing model throws `TypeError`. `signal`
7691
+ cancellation during transport, body reading, retry waiting,
7692
+ or callback settlement prevents successful return and uses
7693
+ `ARCANE_AI_REQUEST_ABORTED`. No request history, DBOPFS write, or recurring
7694
+ model context is retained. The caller owns persistence and key configuration.
7695
+ See [TWiN Cloud](ai/twin-cloud.md) for a complete Node JSON-schema example and
7696
+ the unchanged browser AI interface.
7697
+
7698
+ ### Example
7699
+
7700
+ ```javascript
7701
+ import serverConfig from './server-config.json' with {type: 'json'};
7702
+ import {fetchRequest} from 'arcane-os/ai/twin-cloud';
7703
+
7704
+ const response = await fetchRequest({
7705
+ twinKey: serverConfig.twinKey,
7706
+ model: 'openai-gpt-oss-20b',
7707
+ messages: [{role: 'user', content: 'Describe a moon-powered toaster.'}]
7708
+ });
7709
+
7710
+ console.log(response);
7711
+ ```
7712
+
7713
+ `server-config.json` is an application-owned, ignored configuration file;
7714
+ never commit its key or print it in diagnostics. The SDK does not read it.
7715
+
7716
+ ## fetchHTTPResponse()
7717
+
7718
+ ### Overview
7719
+
7720
+ Shared low-level HTTP owner used by browser AI and the focused TWiN API.
7721
+ Ordinary TWiN callers use `fetchRequest()` instead.
7722
+
7723
+ ### Signature and result
7724
+
7725
+ ```text
7726
+ async fetchHTTPResponse(url,options)
7727
+ ```
7728
+
7729
+ Returns the successful Fetch `Response` without consuming its body. Both URL
7730
+ and Fetch options are caller-supplied; this helper adds no key, model, or
7731
+ request envelope. Non-success responses are read completely as JSON when the
7732
+ content type contains `application/json`, otherwise as text. Only status 429
7733
+ with an overload message repeats after 3000 ms; other error bodies are thrown.
7734
+
7735
+ ### Availability and normalization
7736
+
7737
+ **Node and Browser.** Exported from `arcane-os/ai/twin-cloud` for shared SDK
7738
+ integration. `options.signal` cancels Fetch and the overload wait and is checked
7739
+ after response/error-body reads. Cancellation uses
7740
+ `ARCANE_AI_REQUEST_ABORTED`. Overload warnings use the existing shared logger.
7741
+
7742
+ ### Example
7743
+
7744
+ ```javascript
7745
+ import {fetchHTTPResponse} from 'arcane-os/ai/twin-cloud';
7746
+
7747
+ // The integration supplies its selected endpoint and complete Fetch options.
7748
+ const response = await fetchHTTPResponse(endpoint, requestOptions);
7749
+ ```
7750
+
7751
+ ## fetchJSONResponse()
7752
+
7753
+ ### Overview
7754
+
7755
+ Shared complete JSON-body reader built on `fetchHTTPResponse()`.
7756
+
7757
+ ### Signature and result
7758
+
7759
+ ```text
7760
+ async fetchJSONResponse(url,options)
7761
+ ```
7762
+
7763
+ Returns the full parsed JSON value. A successful response whose content type
7764
+ does not contain `application/json` throws `TypeError`; JSON parser failures
7765
+ propagate. The helper does not select choices or validate a provider envelope.
7766
+
7767
+ ### Availability and normalization
7768
+
7769
+ **Node and Browser.** Exported from `arcane-os/ai/twin-cloud` for shared SDK
7770
+ integration; it retains the HTTP owner's retry/cancellation behavior and checks
7771
+ cancellation again after parsing. Ordinary callers use `fetchRequest()`.
7772
+
7773
+ ### Example
7774
+
7775
+ ```javascript
7776
+ import {fetchJSONResponse} from 'arcane-os/ai/twin-cloud';
7777
+
7778
+ // The integration supplies its selected endpoint and complete Fetch options.
7779
+ const completion = await fetchJSONResponse(endpoint, requestOptions);
7780
+ ```
7781
+
7782
+ ## structuredOutputFormat()
7783
+
7784
+ ### Overview
7785
+
7786
+ Normalizes the existing AI structured-output option without rewriting a schema.
7787
+
7788
+ ### Signature and result
7789
+
7790
+ ```text
7791
+ structuredOutputFormat(value=false)
7792
+ ```
7793
+
7794
+ False, null, and undefined return null. True and `'json'` return `'json'`.
7795
+ A plain object with `Object.prototype` or a null prototype is returned
7796
+ unchanged. Other values throw `AI_STRUCTURED_OUTPUT_INVALID`.
7797
+
7798
+ ### Availability and normalization
7799
+
7800
+ **Node and Browser.** Synchronous helper exported from
7801
+ `arcane-os/ai/twin-cloud`; performs no network or storage operation.
7802
+
7803
+ ### Example
7804
+
7805
+ ```javascript
7806
+ import {structuredOutputFormat} from 'arcane-os/ai/twin-cloud';
7807
+
7808
+ const format = structuredOutputFormat({
7809
+ type: 'object',
7810
+ properties: {text: {type: 'string'}}
7811
+ });
7812
+ ```
7813
+
7814
+ ## openAIResponseFormat()
7815
+
7816
+ ### Overview
7817
+
7818
+ Maps a normalized structured-output choice to chat-completion wire fields.
7819
+
7820
+ ### Signature and result
7821
+
7822
+ ```text
7823
+ openAIResponseFormat(format)
7824
+ ```
7825
+
7826
+ `'json'` becomes `{type:'json_object'}`; a supplied schema becomes
7827
+ `{type:'json_schema',json_schema:{name:'structured_response',strict:true,schema:format}}`.
7828
+ A disabled format returns null. Use `structuredOutputFormat()` first to
7829
+ normalize the public option; this mapper does not validate schema contents.
7830
+
7831
+ ### Availability and normalization
7832
+
7833
+ **Node and Browser.** Synchronous helper exported from
7834
+ `arcane-os/ai/twin-cloud`, shared with browser AI and performing no I/O.
7835
+
7836
+ ### Example
7837
+
7838
+ ```javascript
7839
+ import {structuredOutputFormat, openAIResponseFormat} from 'arcane-os/ai/twin-cloud';
7840
+
7841
+ const responseFormat = openAIResponseFormat(structuredOutputFormat(true));
7842
+ // {type: 'json_object'}
7843
+ ```
7844
+
7845
+ ## isAIRequestAbort()
7846
+
7847
+ ### Overview
7848
+
7849
+ Recognizes the existing AI request cancellation forms.
7850
+
7851
+ ### Signature and result
7852
+
7853
+ ```text
7854
+ isAIRequestAbort(error,signal)
7855
+ ```
7856
+
7857
+ Returns a boolean: true when the signal is aborted, the error name is
7858
+ `AbortError`, or its code is `ARCANE_REQUEST_ABORTED`,
7859
+ `ARCANE_AI_REQUEST_ABORTED`, or `AI_REQUEST_ABORTED`.
7860
+
7861
+ ### Availability and normalization
7862
+
7863
+ **Node and Browser.** Synchronous helper exported from
7864
+ `arcane-os/ai/twin-cloud`; it neither changes nor aborts the supplied operation.
7865
+
7866
+ ### Example
7867
+
7868
+ ```javascript
7869
+ import {isAIRequestAbort} from 'arcane-os/ai/twin-cloud';
7870
+
7871
+ const controller = new AbortController();
7872
+ controller.abort();
7873
+ const cancelled = isAIRequestAbort(undefined, controller.signal);
7874
+ ```
7875
+
7876
+ ## normalizeAIRequestAbort()
7877
+
7878
+ ### Overview
7879
+
7880
+ Keeps cancellation under the shared AI error code while preserving its cause.
7881
+
7882
+ ### Signature and result
7883
+
7884
+ ```text
7885
+ normalizeAIRequestAbort(error)
7886
+ ```
7887
+
7888
+ An existing `ARCANE_AI_REQUEST_ABORTED` error is returned unchanged. Otherwise
7889
+ the result is an Error named `AbortError`, with code
7890
+ `ARCANE_AI_REQUEST_ABORTED`, message `The AI request was cancelled.`, and the
7891
+ supplied value as its cause. The helper returns the error; it does not throw it.
7892
+
7893
+ ### Availability and normalization
7894
+
7895
+ **Node and Browser.** Synchronous helper exported from
7896
+ `arcane-os/ai/twin-cloud`; creates no request, storage, or user state.
7897
+
7898
+ ### Example
7899
+
7900
+ ```javascript
7901
+ import {normalizeAIRequestAbort} from 'arcane-os/ai/twin-cloud';
7902
+
7903
+ const cancelled = normalizeAIRequestAbort(new DOMException('Cancelled', 'AbortError'));
7904
+ ```
7905
+
7639
7906
  ## Data export subpaths
7640
7907
 
7641
7908
  The package also exposes eight JSON Schemas (including `arcane-os/schemas/event-stack.json`) and its package manifest. These are data contracts, not callable JavaScript members. See [schema contracts](../architecture.md) and the files under `schemas/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.29.1",
3
+ "version": "0.30.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -38,6 +38,7 @@
38
38
  "./speech-text": "./browser-runtime/speech-text.mjs",
39
39
  "./ai/browser-wasm": "./browser-runtime/ai/browser-wasm.mjs",
40
40
  "./ai/tool-text-stream": "./browser-runtime/ai/tool-text-stream.mjs",
41
+ "./ai/twin-cloud": "./browser-runtime/ai/twin-cloud.mjs",
41
42
  "./ai/browser-speech": "./browser-runtime/ai/browser-speech.mjs",
42
43
  "./mail": "./src/mail-api.mjs",
43
44
  "./testing": "./src/testing.mjs",
@@ -85,7 +86,7 @@
85
86
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
86
87
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
87
88
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
88
- "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
89
+ "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs",
89
90
  "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
90
91
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
91
92
  "check": "node tools/check-source.mjs && npm test",
@@ -16,6 +16,14 @@ import {
16
16
  } from './AIProviderRuntime.js';
17
17
  import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
18
18
  import {arcaneLogging} from 'arcane-os/logging';
19
+ import {
20
+ fetchHTTPResponse,
21
+ fetchJSONResponse,
22
+ isAIRequestAbort,
23
+ normalizeAIRequestAbort,
24
+ structuredOutputFormat as normalizeStructuredOutput,
25
+ openAIResponseFormat
26
+ } from 'arcane-os/ai/twin-cloud';
19
27
  import {MarkdownSpeech,stripSpeechFormatting} from 'arcane-os/speech-text';
20
28
  import {prepareSpeech} from './PreparedSpeech.js';
21
29
  import {createToolTextObserver} from 'arcane-os/ai/tool-text-stream';
@@ -121,24 +129,6 @@ function aiInitializationError(code,reason,message){
121
129
  return error;
122
130
  }
123
131
 
124
- function isAIRequestAbort(error,signal){
125
- return signal?.aborted
126
- ||error?.name==='AbortError'
127
- ||error?.code==='ARCANE_REQUEST_ABORTED'
128
- ||error?.code==='ARCANE_AI_REQUEST_ABORTED'
129
- ||error?.code==='AI_REQUEST_ABORTED';
130
- }
131
-
132
- function normalizeAIRequestAbort(error){
133
- if(error?.code==='ARCANE_AI_REQUEST_ABORTED'){
134
- return error;
135
- }
136
- const normalized=new Error('The AI request was cancelled.',{cause:error});
137
- normalized.name='AbortError';
138
- normalized.code='ARCANE_AI_REQUEST_ABORTED';
139
- return normalized;
140
- }
141
-
142
132
  function normalizeAIReasoningEffort(value){
143
133
  if(value===undefined||value===null||value===''){
144
134
  return '';
@@ -3776,69 +3766,6 @@ class AI {
3776
3766
  return true;
3777
3767
  }
3778
3768
 
3779
- async #fetchHTTPResponse(url,options){
3780
- const {signal}=options;
3781
- const retryDelayMs=3000;
3782
- try{
3783
- while(true){
3784
- if(signal?.aborted){
3785
- throw normalizeAIRequestAbort(signal.reason);
3786
- }
3787
- const response=await fetch(url,options);
3788
- if(signal?.aborted){
3789
- throw normalizeAIRequestAbort(signal.reason);
3790
- }
3791
- if(response.ok){
3792
- return response;
3793
- }
3794
-
3795
- const contentType=response.headers.get('content-type')||'';
3796
- const error=contentType.includes('application/json')
3797
- ?await response.json()
3798
- :await response.text();
3799
- if(signal?.aborted){
3800
- throw normalizeAIRequestAbort(signal.reason);
3801
- }
3802
- const message=is.string(error)
3803
- ?error
3804
- :error?.error?.message??error?.message;
3805
- if(
3806
- response.status!==429
3807
- ||!is.string(message)
3808
- ||!message.toLowerCase().includes('overload')
3809
- ){
3810
- throw error;
3811
- }
3812
-
3813
- arcaneLogging.warn(
3814
- `${message}\nRetrying in ${retryDelayMs / 1000} seconds`,
3815
- error
3816
- );
3817
- await new Promise(function waitForOverloadRetry(resolve,reject){
3818
- function finishRetryDelay(){
3819
- signal?.removeEventListener('abort',cancelRetryDelay);
3820
- resolve();
3821
- }
3822
- function cancelRetryDelay(){
3823
- clearTimeout(timer);
3824
- signal.removeEventListener('abort',cancelRetryDelay);
3825
- reject(normalizeAIRequestAbort(signal.reason));
3826
- }
3827
- const timer=setTimeout(finishRetryDelay,retryDelayMs);
3828
- signal?.addEventListener('abort',cancelRetryDelay,{once:true});
3829
- if(signal?.aborted){
3830
- cancelRetryDelay();
3831
- }
3832
- });
3833
- }
3834
- }catch(error){
3835
- if(isAIRequestAbort(error,signal)){
3836
- throw normalizeAIRequestAbort(error);
3837
- }
3838
- throw error;
3839
- }
3840
- }
3841
-
3842
3769
  #nativeOllama(){
3843
3770
  const client=globalThis.Arcane?.ollama;
3844
3771
 
@@ -4171,48 +4098,6 @@ class AI {
4171
4098
  ];
4172
4099
  }
4173
4100
 
4174
- #structuredOutputFormat(value=false){
4175
- if(value===false||value===null||value===undefined){
4176
- return null;
4177
- }
4178
- if(value===true||value==='json'){
4179
- return 'json';
4180
- }
4181
- if(
4182
- is.object(value)
4183
- &&!is.array(value)
4184
- &&(
4185
- Object.getPrototypeOf(value)===Object.prototype
4186
- ||Object.getPrototypeOf(value)===null
4187
- )
4188
- ){
4189
- return value;
4190
- }
4191
-
4192
- const error=new TypeError(
4193
- 'AI structured output must be enabled with true, json, or a JSON Schema object.'
4194
- );
4195
- error.code='AI_STRUCTURED_OUTPUT_INVALID';
4196
- throw error;
4197
- }
4198
-
4199
- #openAIResponseFormat(structuredOutputFormat){
4200
- if(structuredOutputFormat==='json'){
4201
- return {type:'json_object'};
4202
- }
4203
- if(structuredOutputFormat){
4204
- return {
4205
- type:'json_schema',
4206
- json_schema:{
4207
- name:'structured_response',
4208
- strict:true,
4209
- schema:structuredOutputFormat
4210
- }
4211
- };
4212
- }
4213
- return null;
4214
- }
4215
-
4216
4101
  async #reportRequest(requestHandler,request,id,metadata){
4217
4102
  if(!is.function(requestHandler)){
4218
4103
  throw new TypeError('AI onRequest callback must be a function.');
@@ -4762,7 +4647,7 @@ class AI {
4762
4647
  if(signal?.aborted){
4763
4648
  throw normalizeAIRequestAbort();
4764
4649
  }
4765
- const structuredOutputFormat=this.#structuredOutputFormat(
4650
+ const structuredOutputFormat=normalizeStructuredOutput(
4766
4651
  structuredOutput
4767
4652
  );
4768
4653
 
@@ -4776,7 +4661,7 @@ class AI {
4776
4661
  }
4777
4662
 
4778
4663
  if(structuredOutputFormat){
4779
- request.response_format=this.#openAIResponseFormat(
4664
+ request.response_format=openAIResponseFormat(
4780
4665
  structuredOutputFormat
4781
4666
  );
4782
4667
  }
@@ -4970,7 +4855,7 @@ class AI {
4970
4855
  destination:this.url
4971
4856
  });
4972
4857
  const body = JSON.stringify(request);
4973
- const response=await this.#fetchHTTPResponse(
4858
+ const response=await fetchHTTPResponse(
4974
4859
  this.url,
4975
4860
  {
4976
4861
  method:'POST',
@@ -5622,7 +5507,7 @@ class AI {
5622
5507
  if(signal?.aborted){
5623
5508
  throw normalizeAIRequestAbort();
5624
5509
  }
5625
- const structuredOutputFormat=this.#structuredOutputFormat(structuredOutput);
5510
+ const structuredOutputFormat=normalizeStructuredOutput(structuredOutput);
5626
5511
 
5627
5512
  const normalizedReasoningEffort=normalizeAIReasoningEffort(
5628
5513
  reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
@@ -5634,7 +5519,7 @@ class AI {
5634
5519
  }
5635
5520
 
5636
5521
  if(structuredOutputFormat){
5637
- request.response_format=this.#openAIResponseFormat(
5522
+ request.response_format=openAIResponseFormat(
5638
5523
  structuredOutputFormat
5639
5524
  );
5640
5525
  }
@@ -5718,7 +5603,7 @@ class AI {
5718
5603
  destination:this.url
5719
5604
  });
5720
5605
  const body = JSON.stringify(request);
5721
- const response=await this.#fetchHTTPResponse(
5606
+ const responseJSON=await fetchJSONResponse(
5722
5607
  this.url,
5723
5608
  {
5724
5609
  method:'POST',
@@ -5729,36 +5614,11 @@ class AI {
5729
5614
  }
5730
5615
  );
5731
5616
 
5732
- const contentType=response.headers.get('content-type')||'';
5733
-
5734
- if(!contentType.includes('application/json')){
5735
- throw new TypeError(
5736
- `AI request returned ${contentType||'an unknown content type'} instead of JSON.`
5737
- );
5738
- }
5739
-
5740
- let responseJSON;
5741
- try{
5742
- responseJSON=await response.json();
5743
- }catch(error){
5744
- if(isAIRequestAbort(error,signal)){
5745
- throw normalizeAIRequestAbort(error);
5746
- }
5747
- throw error;
5748
- }
5749
5617
  if(signal?.aborted){
5750
5618
  throw normalizeAIRequestAbort();
5751
5619
  }
5752
-
5753
- if(!response.id){
5754
- response.id=id;
5755
- }
5756
-
5757
- //console.log(responseJSON);
5758
- //async
5759
5620
  normalizeAICompletionToolCalls(responseJSON);
5760
5621
  await responseHandler(responseJSON,id,false);
5761
- //sync
5762
5622
  return responseJSON;
5763
5623
  }
5764
5624
 
@@ -47,6 +47,7 @@ const SDK_BROWSER_SELF_IMPORTS=new Map([
47
47
  ['arcane-os/speech-text','sdk/speech-text.mjs'],
48
48
  ['arcane-os/ai/browser-wasm',SDK_BROWSER_AI_ENTRY],
49
49
  ['arcane-os/ai/tool-text-stream','sdk/ai/tool-text-stream.mjs'],
50
+ ['arcane-os/ai/twin-cloud','sdk/ai/twin-cloud.mjs'],
50
51
  ['arcane-os/ai/browser-speech',SDK_BROWSER_SPEECH_ENTRY]
51
52
  ]);
52
53
  function fail(message,code='ARCANE_IMPORT_MAP_INVALID'){