@stage5/lumine 0.2.9 → 0.2.11
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/lib/assets.js +155 -56
- package/lib/constants.js +4 -2
- package/lib/workspace.js +9 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +9 -10
package/lib/assets.js
CHANGED
|
@@ -296,39 +296,77 @@ export async function assetsUpload(options) {
|
|
|
296
296
|
// second asset with a new URL, and the old one keeps eating quota. Project-
|
|
297
297
|
// owner assets are checked too: re-uploading media the project already has
|
|
298
298
|
// defeats the reuse flow.
|
|
299
|
+
const referencedGltfAssetUrls = candidates.flatMap(
|
|
300
|
+
(candidate) => candidate.referencedGltfAssetUrls || [],
|
|
301
|
+
);
|
|
302
|
+
let existingAssets = [];
|
|
303
|
+
let existingProjectAssets = [];
|
|
299
304
|
try {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
305
|
+
const listedAssets = await fetchAllRuntimeAssets({
|
|
306
|
+
options,
|
|
307
|
+
auth,
|
|
308
|
+
buildId,
|
|
309
|
+
buildApiToken: token,
|
|
310
|
+
});
|
|
311
|
+
existingAssets = listedAssets.assets;
|
|
312
|
+
existingProjectAssets = listedAssets.projectAssets || [];
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (referencedGltfAssetUrls.length > 0) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"Could not verify this glTF's Twinkle asset URLs. Refresh the asset list and try again.",
|
|
317
|
+
{ cause: error },
|
|
312
318
|
);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (referencedGltfAssetUrls.length > 0) {
|
|
322
|
+
const knownAssets = [...existingAssets, ...existingProjectAssets];
|
|
323
|
+
const unverifiedUrls = referencedGltfAssetUrls.filter(
|
|
324
|
+
(referencedUrl) =>
|
|
325
|
+
!knownAssets.some((asset) =>
|
|
326
|
+
runtimeAssetUrlMatchesGltfReference(asset, referencedUrl),
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
if (unverifiedUrls.length > 0) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`The glTF references Twinkle asset URL${
|
|
332
|
+
unverifiedUrls.length === 1 ? "" : "s"
|
|
333
|
+
} that do not belong to this Build: ${unverifiedUrls
|
|
334
|
+
.slice(0, 3)
|
|
335
|
+
.map((url) => `"${url}"`)
|
|
336
|
+
.join(
|
|
337
|
+
", ",
|
|
338
|
+
)}${unverifiedUrls.length > 3 ? ", ..." : ""}. Upload or reuse assets listed by \`lumine assets list\`, then use their exact URLs.`,
|
|
322
339
|
);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (existingAssets.length > 0 || existingProjectAssets.length > 0) {
|
|
343
|
+
try {
|
|
344
|
+
const nameMatches = (asset, fileName) =>
|
|
345
|
+
asset.originalFileName === fileName || asset.fileName === fileName;
|
|
346
|
+
for (const candidate of candidates) {
|
|
347
|
+
const existing = existingAssets.find((asset) =>
|
|
348
|
+
nameMatches(asset, candidate.fileName),
|
|
349
|
+
);
|
|
350
|
+
if (existing) {
|
|
351
|
+
console.error(
|
|
352
|
+
`lumine: note — an asset named "${candidate.fileName}" already exists (#${existing.id}). ` +
|
|
353
|
+
`Uploading creates a NEW asset with a new URL; if this replaces #${existing.id}, update the code and run \`lumine assets delete ${existing.id}\` (or \`lumine assets prune\`).`,
|
|
354
|
+
);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const projectExisting = existingProjectAssets.find((asset) =>
|
|
358
|
+
nameMatches(asset, candidate.fileName),
|
|
327
359
|
);
|
|
360
|
+
if (projectExisting) {
|
|
361
|
+
console.error(
|
|
362
|
+
`lumine: note — the project owner already has an asset named "${candidate.fileName}" (#${projectExisting.id}). ` +
|
|
363
|
+
`Consider reusing its URL instead of uploading a duplicate:\n ${projectExisting.url}`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
328
366
|
}
|
|
367
|
+
} catch {
|
|
368
|
+
// Duplicate-name warnings are advisory only.
|
|
329
369
|
}
|
|
330
|
-
} catch {
|
|
331
|
-
// Advisory only — never block an upload on the pre-listing.
|
|
332
370
|
}
|
|
333
371
|
const uploaded = [];
|
|
334
372
|
for (const candidate of candidates) {
|
|
@@ -503,7 +541,9 @@ export async function assetsPrune(options) {
|
|
|
503
541
|
body: { assetId: asset.id },
|
|
504
542
|
});
|
|
505
543
|
lastUsage = result?.usage || lastUsage;
|
|
506
|
-
console.log(
|
|
544
|
+
console.log(
|
|
545
|
+
`Deleted #${asset.id} ${asset.originalFileName || asset.fileName}`,
|
|
546
|
+
);
|
|
507
547
|
}
|
|
508
548
|
printAssetUsage(lastUsage);
|
|
509
549
|
await refreshWorkspaceAssetsManifest({
|
|
@@ -529,7 +569,12 @@ export async function confirmPrompt(question) {
|
|
|
529
569
|
// 5MB on slow links) — mirrors the web workspace uploader, plus bounded
|
|
530
570
|
// retries for transient network/5xx failures. 4xx means the presigned URL
|
|
531
571
|
// itself is bad; retrying cannot help.
|
|
532
|
-
export async function putAssetPartWithRetry({
|
|
572
|
+
export async function putAssetPartWithRetry({
|
|
573
|
+
url,
|
|
574
|
+
chunk,
|
|
575
|
+
mimeType,
|
|
576
|
+
partLabel,
|
|
577
|
+
}) {
|
|
533
578
|
let lastError = null;
|
|
534
579
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
535
580
|
try {
|
|
@@ -574,59 +619,104 @@ export async function readAssetUploadCandidate(filePath) {
|
|
|
574
619
|
if (!buffer.length) {
|
|
575
620
|
throw new Error(`Asset file is empty: ${filePath}`);
|
|
576
621
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
622
|
+
const referencedGltfAssetUrls =
|
|
623
|
+
extension === ".gltf"
|
|
624
|
+
? assertGltfUploadCandidateHasOnlySupportedUris({ fileName, buffer })
|
|
625
|
+
: [];
|
|
626
|
+
return {
|
|
627
|
+
absolutePath,
|
|
628
|
+
fileName,
|
|
629
|
+
mimeType,
|
|
630
|
+
buffer,
|
|
631
|
+
referencedGltfAssetUrls,
|
|
632
|
+
};
|
|
581
633
|
}
|
|
582
634
|
|
|
583
|
-
|
|
635
|
+
const TWINKLE_GLTF_ASSET_HOSTS = new Set([
|
|
636
|
+
"d3jvoamd2k4p0s.cloudfront.net",
|
|
637
|
+
"www.twin-kle.com",
|
|
638
|
+
"twin-kle.com",
|
|
639
|
+
]);
|
|
640
|
+
const TWINKLE_GLTF_ASSET_PATH_PREFIXES = [
|
|
641
|
+
"/attachments/build-runtime/",
|
|
642
|
+
"/attachments/optimized/",
|
|
643
|
+
];
|
|
644
|
+
|
|
645
|
+
function isSupportedGltfUri(uri) {
|
|
584
646
|
const normalizedUri = String(uri || "").trim();
|
|
585
647
|
if (!normalizedUri) return true;
|
|
586
|
-
if (normalizedUri.startsWith("/") || normalizedUri.startsWith("//")) {
|
|
587
|
-
return true;
|
|
588
|
-
}
|
|
589
648
|
const schemeMatch = normalizedUri.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
|
|
590
649
|
if (!schemeMatch) return false;
|
|
591
|
-
|
|
650
|
+
if (schemeMatch[1].toLowerCase() === "data") return true;
|
|
651
|
+
let parsedUri;
|
|
652
|
+
try {
|
|
653
|
+
parsedUri = new URL(normalizedUri);
|
|
654
|
+
} catch {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
return (
|
|
658
|
+
parsedUri.protocol === "https:" &&
|
|
659
|
+
!parsedUri.port &&
|
|
660
|
+
!parsedUri.username &&
|
|
661
|
+
!parsedUri.password &&
|
|
662
|
+
!parsedUri.search &&
|
|
663
|
+
!parsedUri.hash &&
|
|
664
|
+
TWINKLE_GLTF_ASSET_HOSTS.has(parsedUri.hostname.toLowerCase()) &&
|
|
665
|
+
TWINKLE_GLTF_ASSET_PATH_PREFIXES.some((prefix) =>
|
|
666
|
+
parsedUri.pathname.startsWith(prefix),
|
|
667
|
+
)
|
|
668
|
+
);
|
|
592
669
|
}
|
|
593
670
|
|
|
594
|
-
function
|
|
595
|
-
const
|
|
671
|
+
function collectUnsupportedGltfUris(parsedGltf) {
|
|
672
|
+
const unsupportedUris = [];
|
|
596
673
|
if (!parsedGltf || typeof parsedGltf !== "object") {
|
|
597
|
-
return
|
|
674
|
+
return unsupportedUris;
|
|
598
675
|
}
|
|
599
676
|
for (const entry of [
|
|
600
677
|
...(Array.isArray(parsedGltf.buffers) ? parsedGltf.buffers : []),
|
|
601
678
|
...(Array.isArray(parsedGltf.images) ? parsedGltf.images : []),
|
|
602
679
|
]) {
|
|
603
680
|
const uri = typeof entry?.uri === "string" ? entry.uri.trim() : "";
|
|
604
|
-
if (uri && !
|
|
605
|
-
|
|
681
|
+
if (uri && !isSupportedGltfUri(uri)) {
|
|
682
|
+
unsupportedUris.push(uri);
|
|
606
683
|
}
|
|
607
684
|
}
|
|
608
|
-
return
|
|
685
|
+
return unsupportedUris;
|
|
609
686
|
}
|
|
610
687
|
|
|
611
|
-
function
|
|
688
|
+
function assertGltfUploadCandidateHasOnlySupportedUris({ fileName, buffer }) {
|
|
612
689
|
let parsedGltf;
|
|
613
690
|
try {
|
|
614
691
|
parsedGltf = JSON.parse(buffer.toString("utf8"));
|
|
615
692
|
} catch {
|
|
616
693
|
throw new Error(`Invalid .gltf JSON in ${fileName}.`);
|
|
617
694
|
}
|
|
618
|
-
const
|
|
619
|
-
if (
|
|
620
|
-
return
|
|
695
|
+
const unsupportedUris = collectUnsupportedGltfUris(parsedGltf);
|
|
696
|
+
if (unsupportedUris.length === 0) {
|
|
697
|
+
return [
|
|
698
|
+
...(Array.isArray(parsedGltf.buffers) ? parsedGltf.buffers : []),
|
|
699
|
+
...(Array.isArray(parsedGltf.images) ? parsedGltf.images : []),
|
|
700
|
+
]
|
|
701
|
+
.map((entry) => (typeof entry?.uri === "string" ? entry.uri.trim() : ""))
|
|
702
|
+
.filter((uri) => uri && !uri.toLowerCase().startsWith("data:"));
|
|
621
703
|
}
|
|
622
|
-
const sampleUris =
|
|
704
|
+
const sampleUris = unsupportedUris
|
|
623
705
|
.slice(0, 3)
|
|
624
706
|
.map((uri) => `"${uri}"`)
|
|
625
707
|
.join(", ");
|
|
626
708
|
throw new Error(
|
|
627
|
-
`${fileName} contains
|
|
628
|
-
|
|
629
|
-
} (${sampleUris}${
|
|
709
|
+
`${fileName} contains unsupported glTF URI${
|
|
710
|
+
unsupportedUris.length === 1 ? "" : "s"
|
|
711
|
+
} (${sampleUris}${unsupportedUris.length > 3 ? ", ..." : ""}). Raw .gltf companion files are stored under separate asset URLs, and Build apps cannot load arbitrary external URLs. Use .glb, embed data URIs, or rewrite .gltf URIs to Twinkle runtime attachment URLs before uploading.`,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function runtimeAssetUrlMatchesGltfReference(asset, referencedUrl) {
|
|
716
|
+
const normalizedReference = String(referencedUrl || "").trim();
|
|
717
|
+
if (!normalizedReference) return false;
|
|
718
|
+
return [asset?.url, asset?.thumbUrl].some(
|
|
719
|
+
(assetUrl) => String(assetUrl || "").trim() === normalizedReference,
|
|
630
720
|
);
|
|
631
721
|
}
|
|
632
722
|
|
|
@@ -661,7 +751,10 @@ export async function uploadRuntimeAsset({
|
|
|
661
751
|
const parts = [];
|
|
662
752
|
for (let partNumber = 0; partNumber < urls.length; partNumber += 1) {
|
|
663
753
|
const start = partNumber * ASSET_UPLOAD_CHUNK_BYTES;
|
|
664
|
-
const end = Math.min(
|
|
754
|
+
const end = Math.min(
|
|
755
|
+
start + ASSET_UPLOAD_CHUNK_BYTES,
|
|
756
|
+
candidate.buffer.length,
|
|
757
|
+
);
|
|
665
758
|
const response = await putAssetPartWithRetry({
|
|
666
759
|
url: urls[partNumber],
|
|
667
760
|
chunk: candidate.buffer.subarray(start, end),
|
|
@@ -690,7 +783,9 @@ export async function uploadRuntimeAsset({
|
|
|
690
783
|
});
|
|
691
784
|
const asset = completed?.asset || prepared?.asset;
|
|
692
785
|
if (!asset?.url) {
|
|
693
|
-
throw new Error(
|
|
786
|
+
throw new Error(
|
|
787
|
+
`Upload of ${candidate.fileName} completed without an asset URL.`,
|
|
788
|
+
);
|
|
694
789
|
}
|
|
695
790
|
return asset;
|
|
696
791
|
} catch (error) {
|
|
@@ -799,7 +894,9 @@ export async function writeAssetsManifest({
|
|
|
799
894
|
const unmatchedReferences = refs
|
|
800
895
|
? refs.filter(
|
|
801
896
|
(ref) =>
|
|
802
|
-
!knownAssets.some((asset) =>
|
|
897
|
+
!knownAssets.some((asset) =>
|
|
898
|
+
runtimeAssetMatchesReference(asset, ref),
|
|
899
|
+
),
|
|
803
900
|
)
|
|
804
901
|
: null;
|
|
805
902
|
const metadataDir = path.join(dir, PROJECT_METADATA_DIR);
|
|
@@ -814,7 +911,9 @@ export async function writeAssetsManifest({
|
|
|
814
911
|
fetchedAt: new Date().toISOString(),
|
|
815
912
|
usage: usage || null,
|
|
816
913
|
assets: annotatedAssets,
|
|
817
|
-
...(annotatedProjectAssets
|
|
914
|
+
...(annotatedProjectAssets
|
|
915
|
+
? { projectAssets: annotatedProjectAssets }
|
|
916
|
+
: {}),
|
|
818
917
|
...(unmatchedReferences ? { unmatchedReferences } : {}),
|
|
819
918
|
},
|
|
820
919
|
null,
|
package/lib/constants.js
CHANGED
|
@@ -75,7 +75,8 @@ export const ASSET_MIME_BY_EXTENSION = {
|
|
|
75
75
|
export const PROJECT_MAX_FILES = 100;
|
|
76
76
|
export const PROJECT_MAX_EFFECTIVE_FILE_LINES = 500;
|
|
77
77
|
export const PROJECT_EFFECTIVE_LINE_MAX_COLUMNS = 160;
|
|
78
|
-
export const PROJECT_MAX_TOTAL_BYTES_DEFAULT =
|
|
78
|
+
export const PROJECT_MAX_TOTAL_BYTES_DEFAULT = 1024 * 1024;
|
|
79
|
+
export const PROJECT_WARN_TOTAL_BYTES = 500 * 1024;
|
|
79
80
|
// Matches asset references in project files: absolute CloudFront URLs and
|
|
80
81
|
// relative /attachments/build-runtime|optimized/... paths.
|
|
81
82
|
export const RUNTIME_ASSET_REFERENCE_PATTERN =
|
|
@@ -218,7 +219,8 @@ lumine save --summary "Describe the change"
|
|
|
218
219
|
|
|
219
220
|
- Max ${PROJECT_MAX_FILES} files per project; each file max ${PROJECT_MAX_EFFECTIVE_FILE_LINES} effective lines
|
|
220
221
|
(a physical line counts once per ${PROJECT_EFFECTIVE_LINE_MAX_COLUMNS} characters, so minified one-liners
|
|
221
|
-
blow the limit). Default total project size
|
|
222
|
+
blow the limit). Default total project size cap ${Math.floor(PROJECT_MAX_TOTAL_BYTES_DEFAULT / 1024)} KB,
|
|
223
|
+
with a soft warning from ${Math.floor(PROJECT_WARN_TOTAL_BYTES / 1024)} KB.
|
|
222
224
|
- Plan file splits BEFORE writing code; \`lumine check\` validates the local
|
|
223
225
|
workspace against these limits without saving, and \`lumine save\` fails fast
|
|
224
226
|
locally listing every violation.
|
package/lib/workspace.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
PROJECT_MAX_EFFECTIVE_FILE_LINES,
|
|
16
16
|
PROJECT_MAX_FILES,
|
|
17
17
|
PROJECT_MAX_TOTAL_BYTES_DEFAULT,
|
|
18
|
+
PROJECT_WARN_TOTAL_BYTES,
|
|
18
19
|
PROJECT_METADATA_DIR,
|
|
19
20
|
PROJECT_METADATA_FILE,
|
|
20
21
|
SDK_REFERENCE_FALLBACK,
|
|
@@ -304,6 +305,14 @@ export function collectProjectLimitFindings(files) {
|
|
|
304
305
|
PROJECT_MAX_TOTAL_BYTES_DEFAULT / 1024,
|
|
305
306
|
)} KB project limit — the server may reject this save.`,
|
|
306
307
|
);
|
|
308
|
+
} else if (totalBytes > PROJECT_WARN_TOTAL_BYTES) {
|
|
309
|
+
warnings.push(
|
|
310
|
+
`Project files total ${formatBytes(totalBytes)} — approaching the ${Math.floor(
|
|
311
|
+
PROJECT_MAX_TOTAL_BYTES_DEFAULT / 1024,
|
|
312
|
+
)} KB project cap (soft warning from ${Math.floor(
|
|
313
|
+
PROJECT_WARN_TOTAL_BYTES / 1024,
|
|
314
|
+
)} KB). Keep files lean where structure allows.`,
|
|
315
|
+
);
|
|
307
316
|
}
|
|
308
317
|
return { errors, warnings };
|
|
309
318
|
}
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
3
|
Version: 1.26.3
|
|
4
|
-
Updated: 2026-07-
|
|
5
|
-
Generated: 2026-07-
|
|
4
|
+
Updated: 2026-07-10
|
|
5
|
+
Generated: 2026-07-10T05:52:15.007Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -277,7 +277,7 @@ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
|
|
|
277
277
|
- Returns: { text, response, model, aiUsagePolicy }
|
|
278
278
|
- Generate text with the default Lumine text model, optionally streaming text updates through onText.
|
|
279
279
|
- Signed-in viewers only.
|
|
280
|
-
- Uses
|
|
280
|
+
- Uses Grok 4.5 by default.
|
|
281
281
|
- Each successful text generation consumes AI Energy from the signed-in viewer.
|
|
282
282
|
- history must be an array of { role: 'user' | 'assistant', content: string }. Twinkle.ai.chat does not read a text field.
|
|
283
283
|
- The server keeps the latest 12 valid history entries.
|
|
@@ -294,9 +294,9 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
294
294
|
- Use this instead of asking Twinkle.ai.chat to return JSON.
|
|
295
295
|
- expectedStructure must be a JSON object that describes the exact returned object shape.
|
|
296
296
|
- mode is accepted as an alias for thinkingMode, and mid is accepted as an alias for medium.
|
|
297
|
-
- thinkingMode low uses GPT
|
|
298
|
-
- thinkingMode medium uses GPT
|
|
299
|
-
- thinkingMode high uses
|
|
297
|
+
- thinkingMode low uses GPT-5.6 Luna and records free low-energy usage.
|
|
298
|
+
- thinkingMode medium uses GPT-5.6 Luna and normal AI Energy while AI Energy remains.
|
|
299
|
+
- thinkingMode high uses Grok 4.5 with high reasoning and high AI Energy while AI Energy remains.
|
|
300
300
|
- If medium or high is requested after AI Energy is empty, the server falls back to low and returns thinkingMode: low.
|
|
301
301
|
- The SDK validates shape and retries malformed JSON, but app code should still validate business-specific enum values.
|
|
302
302
|
- Example: const { object } = await Twinkle.ai.generateObject({ thinkingMode: 'medium', prompt: 'Classify the player intent from: ' + playerText, expectedStructure: { action: 'string', targetCharacter: 'string', confidence: 0, shouldAskFollowUp: false } });
|
|
@@ -335,10 +335,9 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
335
335
|
- The character route also accepts text or message fields for compatibility, but generated apps should use content.
|
|
336
336
|
- The server keeps the latest 16 valid character history entries.
|
|
337
337
|
- Pass onText/onStatus for streaming dialogue. Omit callbacks for non-streaming dialogue where the promise resolves with the final response.
|
|
338
|
-
- thinkingMode low uses Lite Mode and
|
|
339
|
-
- thinkingMode medium uses normal AI Energy: Zero uses
|
|
340
|
-
- thinkingMode high uses high AI Energy: Zero uses
|
|
341
|
-
- Zero maps low/medium/high to GPT nano/GPT mini/full GPT. Ciel maps low/medium/high to Claude Haiku/Claude Sonnet/Claude Opus 4.8.
|
|
338
|
+
- thinkingMode low uses Lite Mode: Zero uses Grok 4.5 with low reasoning and Ciel uses Claude Haiku 4.5; usage is recorded as free low-energy usage.
|
|
339
|
+
- thinkingMode medium uses normal AI Energy: Zero uses Grok 4.5 with medium reasoning and Ciel uses Claude Sonnet 5 while AI Energy remains.
|
|
340
|
+
- thinkingMode high uses high AI Energy: Zero uses Grok 4.5 with high reasoning and Ciel uses Claude Opus 4.8 with extended thinking while AI Energy remains.
|
|
342
341
|
- If medium or high is requested after AI Energy is empty, the server falls back to low and returns thinkingMode: low.
|
|
343
342
|
- Pass roomContext as a short shared scene transcript so Zero and Ciel can know what happened in the same room.
|
|
344
343
|
- includeWebsiteContext defaults to true. Set includeWebsiteContext: false for in-world NPC dialogue that should only use Zero/Ciel's basic character identity plus your scene/instructions.
|