@llumiverse/core 1.3.0 → 1.4.0-dev.20260629.090753Z
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/CompletionStream.d.ts +48 -0
- package/lib/CompletionStream.d.ts.map +1 -0
- package/lib/{esm/CompletionStream.js → CompletionStream.js} +142 -67
- package/lib/CompletionStream.js.map +1 -0
- package/lib/{types/Driver.d.ts → Driver.d.ts} +38 -5
- package/lib/Driver.d.ts.map +1 -0
- package/lib/{esm/Driver.js → Driver.js} +151 -106
- package/lib/Driver.js.map +1 -0
- package/lib/{types/async.d.ts → async.d.ts} +3 -3
- package/lib/async.d.ts.map +1 -0
- package/lib/{esm/async.js → async.js} +4 -4
- package/lib/async.js.map +1 -0
- package/lib/{types/conversation-utils.d.ts → conversation-utils.d.ts} +9 -0
- package/lib/conversation-utils.d.ts.map +1 -0
- package/lib/{esm/conversation-utils.js → conversation-utils.js} +78 -15
- package/lib/conversation-utils.js.map +1 -0
- package/lib/embeddings.d.ts +58 -0
- package/lib/embeddings.d.ts.map +1 -0
- package/lib/embeddings.js +131 -0
- package/lib/embeddings.js.map +1 -0
- package/lib/formatters/commons.d.ts +3 -0
- package/lib/formatters/commons.d.ts.map +1 -0
- package/lib/formatters/commons.js +4 -0
- package/lib/formatters/commons.js.map +1 -0
- package/lib/{types/formatters → formatters}/generic.d.ts +2 -2
- package/lib/formatters/generic.d.ts.map +1 -0
- package/lib/{esm/formatters → formatters}/generic.js +8 -8
- package/lib/formatters/generic.js.map +1 -0
- package/lib/formatters/index.d.ts +4 -0
- package/lib/formatters/index.d.ts.map +1 -0
- package/lib/formatters/index.js +4 -0
- package/lib/formatters/index.js.map +1 -0
- package/lib/{types/formatters → formatters}/nova.d.ts +4 -4
- package/lib/formatters/nova.d.ts.map +1 -0
- package/lib/{esm/formatters → formatters}/nova.js +19 -17
- package/lib/formatters/nova.js.map +1 -0
- package/lib/http-agent.d.ts +53 -0
- package/lib/http-agent.d.ts.map +1 -0
- package/lib/http-agent.js +141 -0
- package/lib/http-agent.js.map +1 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -0
- package/lib/{types/json.d.ts → json.d.ts} +1 -1
- package/lib/json.d.ts.map +1 -0
- package/lib/{esm/json.js → json.js} +4 -3
- package/lib/json.js.map +1 -0
- package/lib/logger.d.ts +3 -0
- package/lib/logger.d.ts.map +1 -0
- package/lib/logger.js +42 -0
- package/lib/logger.js.map +1 -0
- package/lib/resolver.d.ts +2 -0
- package/lib/resolver.d.ts.map +1 -0
- package/lib/{esm/resolver.js → resolver.js} +7 -4
- package/lib/resolver.js.map +1 -0
- package/lib/stream.d.ts.map +1 -0
- package/lib/stream.js.map +1 -0
- package/lib/{types/validation.d.ts → validation.d.ts} +1 -1
- package/lib/validation.d.ts.map +1 -0
- package/lib/validation.js +100 -0
- package/lib/validation.js.map +1 -0
- package/package.json +28 -38
- package/src/CompletionStream.ts +208 -115
- package/src/Driver.error.test.ts +101 -72
- package/src/Driver.ts +208 -152
- package/src/async.ts +30 -30
- package/src/conversation-utils.ts +92 -16
- package/src/embeddings.ts +157 -0
- package/src/formatters/commons.ts +2 -2
- package/src/formatters/generic.ts +20 -17
- package/src/formatters/index.ts +3 -3
- package/src/formatters/nova.ts +53 -51
- package/src/http-agent.test.ts +281 -0
- package/src/http-agent.ts +177 -0
- package/src/index.ts +7 -5
- package/src/json.ts +6 -5
- package/src/logger.ts +42 -0
- package/src/resolver.ts +12 -9
- package/src/stream.ts +4 -5
- package/src/validation.ts +42 -29
- package/lib/cjs/CompletionStream.js +0 -293
- package/lib/cjs/CompletionStream.js.map +0 -1
- package/lib/cjs/Driver.js +0 -300
- package/lib/cjs/Driver.js.map +0 -1
- package/lib/cjs/async.js +0 -131
- package/lib/cjs/async.js.map +0 -1
- package/lib/cjs/conversation-utils.js +0 -579
- package/lib/cjs/conversation-utils.js.map +0 -1
- package/lib/cjs/formatters/commons.js +0 -7
- package/lib/cjs/formatters/commons.js.map +0 -1
- package/lib/cjs/formatters/generic.js +0 -59
- package/lib/cjs/formatters/generic.js.map +0 -1
- package/lib/cjs/formatters/index.js +0 -20
- package/lib/cjs/formatters/index.js.map +0 -1
- package/lib/cjs/formatters/nova.js +0 -99
- package/lib/cjs/formatters/nova.js.map +0 -1
- package/lib/cjs/index.js +0 -22
- package/lib/cjs/index.js.map +0 -1
- package/lib/cjs/json.js +0 -29
- package/lib/cjs/json.js.map +0 -1
- package/lib/cjs/package.json +0 -3
- package/lib/cjs/resolver.js +0 -45
- package/lib/cjs/resolver.js.map +0 -1
- package/lib/cjs/stream.js +0 -30
- package/lib/cjs/stream.js.map +0 -1
- package/lib/cjs/validation.js +0 -96
- package/lib/cjs/validation.js.map +0 -1
- package/lib/esm/CompletionStream.js.map +0 -1
- package/lib/esm/Driver.js.map +0 -1
- package/lib/esm/async.js.map +0 -1
- package/lib/esm/conversation-utils.js.map +0 -1
- package/lib/esm/formatters/commons.js +0 -4
- package/lib/esm/formatters/commons.js.map +0 -1
- package/lib/esm/formatters/generic.js.map +0 -1
- package/lib/esm/formatters/index.js +0 -4
- package/lib/esm/formatters/index.js.map +0 -1
- package/lib/esm/formatters/nova.js.map +0 -1
- package/lib/esm/index.js +0 -6
- package/lib/esm/index.js.map +0 -1
- package/lib/esm/json.js.map +0 -1
- package/lib/esm/resolver.js.map +0 -1
- package/lib/esm/stream.js.map +0 -1
- package/lib/esm/validation.js +0 -88
- package/lib/esm/validation.js.map +0 -1
- package/lib/types/CompletionStream.d.ts +0 -20
- package/lib/types/CompletionStream.d.ts.map +0 -1
- package/lib/types/Driver.d.ts.map +0 -1
- package/lib/types/async.d.ts.map +0 -1
- package/lib/types/conversation-utils.d.ts.map +0 -1
- package/lib/types/formatters/commons.d.ts +0 -3
- package/lib/types/formatters/commons.d.ts.map +0 -1
- package/lib/types/formatters/generic.d.ts.map +0 -1
- package/lib/types/formatters/index.d.ts +0 -4
- package/lib/types/formatters/index.d.ts.map +0 -1
- package/lib/types/formatters/nova.d.ts.map +0 -1
- package/lib/types/index.d.ts +0 -6
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/json.d.ts.map +0 -1
- package/lib/types/resolver.d.ts +0 -2
- package/lib/types/resolver.d.ts.map +0 -1
- package/lib/types/stream.d.ts.map +0 -1
- package/lib/types/validation.d.ts.map +0 -1
- /package/lib/{types/stream.d.ts → stream.d.ts} +0 -0
- /package/lib/{esm/stream.js → stream.js} +0 -0
|
@@ -49,6 +49,15 @@ export interface StripOptions {
|
|
|
49
49
|
* - N > 0: Truncate text to approximately N tokens (~4 chars/token)
|
|
50
50
|
*/
|
|
51
51
|
textMaxTokens?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Number of most-recent messages to leave untouched when truncating large
|
|
54
|
+
* text (the active working set). Only messages OLDER than the last
|
|
55
|
+
* `keepRecentMessages` entries get their large strings truncated to
|
|
56
|
+
* `textMaxTokens`. Requires a conversation with a `messages` array.
|
|
57
|
+
* - undefined/0: No turn awareness — truncate every large string (legacy).
|
|
58
|
+
* - N > 0: Keep the last N messages full; truncate older ones.
|
|
59
|
+
*/
|
|
60
|
+
keepRecentMessages?: number;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
/**
|
|
@@ -147,9 +156,7 @@ function isOpenAIBase64ImageBlock(obj: unknown): boolean {
|
|
|
147
156
|
if (o.type !== 'image_url') return false;
|
|
148
157
|
if (!o.image_url || typeof o.image_url !== 'object') return false;
|
|
149
158
|
const imgUrl = o.image_url as Record<string, unknown>;
|
|
150
|
-
return typeof imgUrl.url === 'string' &&
|
|
151
|
-
imgUrl.url.startsWith('data:image/') &&
|
|
152
|
-
imgUrl.url.includes(';base64,');
|
|
159
|
+
return typeof imgUrl.url === 'string' && imgUrl.url.startsWith('data:image/') && imgUrl.url.includes(';base64,');
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
/**
|
|
@@ -236,7 +243,7 @@ export function setConversationMeta(conversation: unknown, meta: ConversationMet
|
|
|
236
243
|
return { [ARRAY_WRAPPER_KEY]: conversation, [META_KEY]: meta };
|
|
237
244
|
}
|
|
238
245
|
if (typeof conversation === 'object' && conversation !== null) {
|
|
239
|
-
return { ...conversation as object, [META_KEY]: meta };
|
|
246
|
+
return { ...(conversation as object), [META_KEY]: meta };
|
|
240
247
|
}
|
|
241
248
|
return conversation;
|
|
242
249
|
}
|
|
@@ -305,7 +312,7 @@ function serializeBinaryForStorage(obj: unknown): unknown {
|
|
|
305
312
|
}
|
|
306
313
|
|
|
307
314
|
if (Array.isArray(obj)) {
|
|
308
|
-
return obj.map(item => serializeBinaryForStorage(item));
|
|
315
|
+
return obj.map((item) => serializeBinaryForStorage(item));
|
|
309
316
|
}
|
|
310
317
|
|
|
311
318
|
if (typeof obj === 'object') {
|
|
@@ -335,7 +342,7 @@ export function deserializeBinaryFromStorage(obj: unknown): unknown {
|
|
|
335
342
|
}
|
|
336
343
|
|
|
337
344
|
if (Array.isArray(obj)) {
|
|
338
|
-
return obj.map(item => deserializeBinaryFromStorage(item));
|
|
345
|
+
return obj.map((item) => deserializeBinaryFromStorage(item));
|
|
339
346
|
}
|
|
340
347
|
|
|
341
348
|
if (typeof obj === 'object') {
|
|
@@ -366,7 +373,7 @@ function stripBinaryFromConversationInternal(obj: unknown): unknown {
|
|
|
366
373
|
}
|
|
367
374
|
|
|
368
375
|
if (Array.isArray(obj)) {
|
|
369
|
-
return obj.map(item => {
|
|
376
|
+
return obj.map((item) => {
|
|
370
377
|
// Replace entire Bedrock image/document/video blocks with text blocks
|
|
371
378
|
if (isBedrockImageBlock(item) || isSerializedBedrockImageBlock(item)) {
|
|
372
379
|
return { text: IMAGE_PLACEHOLDER };
|
|
@@ -432,7 +439,7 @@ function stripBase64ImagesFromConversationInternal(obj: unknown): unknown {
|
|
|
432
439
|
}
|
|
433
440
|
|
|
434
441
|
if (Array.isArray(obj)) {
|
|
435
|
-
return obj.map(item => {
|
|
442
|
+
return obj.map((item) => {
|
|
436
443
|
// Replace entire OpenAI image_url blocks with text blocks
|
|
437
444
|
if (isOpenAIBase64ImageBlock(item)) {
|
|
438
445
|
return { type: 'text', text: IMAGE_PLACEHOLDER };
|
|
@@ -496,18 +503,72 @@ export function truncateLargeTextInConversation(obj: unknown, options?: StripOpt
|
|
|
496
503
|
}
|
|
497
504
|
|
|
498
505
|
const maxChars = maxTokens * CHARS_PER_TOKEN;
|
|
506
|
+
const keepRecent = options?.keepRecentMessages;
|
|
507
|
+
|
|
508
|
+
// Turn-aware path: keep the most recent `keepRecent` messages untouched (the
|
|
509
|
+
// agent's active working set — the file it just read/wrote, latest
|
|
510
|
+
// diagnostics) and truncate large strings only in older messages. This lets
|
|
511
|
+
// callers use an aggressive cap without blinding the model to current work.
|
|
512
|
+
if (keepRecent && keepRecent > 0) {
|
|
513
|
+
const messages = getConversationMessages(obj);
|
|
514
|
+
if (messages && messages.length > keepRecent) {
|
|
515
|
+
const cutoff = messages.length - keepRecent;
|
|
516
|
+
const truncated = messages.map((m, i) => (i < cutoff ? truncateLargeTextInternal(m, maxChars) : m));
|
|
517
|
+
return setConversationMessages(obj, truncated);
|
|
518
|
+
}
|
|
519
|
+
// No identifiable messages array, or short conversation: leave it full.
|
|
520
|
+
return obj;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Legacy path: truncate every large string in the conversation.
|
|
499
524
|
return truncateLargeTextInternal(obj, maxChars);
|
|
500
525
|
}
|
|
501
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Extract the message history array from a conversation object, handling the
|
|
529
|
+
* `{ messages: [...] }` shape (Claude/OpenAI), the wrapped-array shape, and a
|
|
530
|
+
* plain array. Returns undefined when no message array is identifiable.
|
|
531
|
+
*/
|
|
532
|
+
function getConversationMessages(obj: unknown): unknown[] | undefined {
|
|
533
|
+
if (Array.isArray(obj)) return obj;
|
|
534
|
+
const wrapped = unwrapConversationArray(obj);
|
|
535
|
+
if (wrapped) return wrapped;
|
|
536
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
537
|
+
const messages = (obj as Record<string, unknown>).messages;
|
|
538
|
+
if (Array.isArray(messages)) return messages;
|
|
539
|
+
}
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Rebuild a conversation with a new message array, matching the shape returned
|
|
545
|
+
* by getConversationMessages.
|
|
546
|
+
*/
|
|
547
|
+
function setConversationMessages(obj: unknown, messages: unknown[]): unknown {
|
|
548
|
+
if (Array.isArray(obj)) return messages;
|
|
549
|
+
if (unwrapConversationArray(obj)) {
|
|
550
|
+
return { ...(obj as object), [ARRAY_WRAPPER_KEY]: messages };
|
|
551
|
+
}
|
|
552
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
553
|
+
return { ...(obj as object), messages };
|
|
554
|
+
}
|
|
555
|
+
return obj;
|
|
556
|
+
}
|
|
557
|
+
|
|
502
558
|
function shouldPreserveMediaPayload(obj: unknown): boolean {
|
|
503
559
|
if (typeof obj !== 'object' || obj === null) return false;
|
|
504
560
|
|
|
505
561
|
const o = obj as Record<string, unknown>;
|
|
506
562
|
|
|
507
563
|
// Preserved Bedrock binary blocks and their serialized storage wrapper.
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
564
|
+
if (
|
|
565
|
+
isBedrockImageBlock(obj) ||
|
|
566
|
+
isSerializedBedrockImageBlock(obj) ||
|
|
567
|
+
isBedrockDocumentBlock(obj) ||
|
|
568
|
+
isSerializedBedrockDocumentBlock(obj) ||
|
|
569
|
+
isBedrockVideoBlock(obj) ||
|
|
570
|
+
isSerializedBedrockVideoBlock(obj)
|
|
571
|
+
) {
|
|
511
572
|
return true;
|
|
512
573
|
}
|
|
513
574
|
|
|
@@ -516,8 +577,12 @@ function shouldPreserveMediaPayload(obj: unknown): boolean {
|
|
|
516
577
|
}
|
|
517
578
|
|
|
518
579
|
// Preserved base64 media payloads for OpenAI, Gemini, and Anthropic/Claude.
|
|
519
|
-
if (
|
|
520
|
-
|
|
580
|
+
if (
|
|
581
|
+
isOpenAIBase64ImageBlock(obj) ||
|
|
582
|
+
isGeminiInlineDataBlock(obj) ||
|
|
583
|
+
isAnthropicBase64ImageBlock(obj) ||
|
|
584
|
+
isAnthropicBase64DocumentBlock(obj)
|
|
585
|
+
) {
|
|
521
586
|
return true;
|
|
522
587
|
}
|
|
523
588
|
|
|
@@ -533,13 +598,24 @@ function truncateLargeTextInternal(obj: unknown, maxChars: number): unknown {
|
|
|
533
598
|
return obj;
|
|
534
599
|
}
|
|
535
600
|
if (obj.length > maxChars) {
|
|
536
|
-
|
|
601
|
+
// Don't cut through a surrogate pair: substring() slices at a UTF-16
|
|
602
|
+
// code-unit index, so a boundary that lands between the two halves of
|
|
603
|
+
// a non-BMP character (emoji, CJK-ext, …) leaves a lone high surrogate.
|
|
604
|
+
// That is invalid Unicode — it can't be UTF-8 encoded losslessly and
|
|
605
|
+
// strict JSON parsers (e.g. Vertex/Gemini) reject the request body as
|
|
606
|
+
// "The input data is not valid json". Step back one unit in that case.
|
|
607
|
+
let end = maxChars;
|
|
608
|
+
const lastUnit = obj.charCodeAt(end - 1);
|
|
609
|
+
if (lastUnit >= 0xd800 && lastUnit <= 0xdbff) {
|
|
610
|
+
end -= 1;
|
|
611
|
+
}
|
|
612
|
+
return obj.substring(0, end) + TEXT_TRUNCATED_MARKER;
|
|
537
613
|
}
|
|
538
614
|
return obj;
|
|
539
615
|
}
|
|
540
616
|
|
|
541
617
|
if (Array.isArray(obj)) {
|
|
542
|
-
return obj.map(item => truncateLargeTextInternal(item, maxChars));
|
|
618
|
+
return obj.map((item) => truncateLargeTextInternal(item, maxChars));
|
|
543
619
|
}
|
|
544
620
|
|
|
545
621
|
if (typeof obj === 'object') {
|
|
@@ -608,7 +684,7 @@ function stripHeartbeatsInternal(obj: unknown): unknown {
|
|
|
608
684
|
}
|
|
609
685
|
|
|
610
686
|
if (Array.isArray(obj)) {
|
|
611
|
-
return obj.map(item => stripHeartbeatsInternal(item));
|
|
687
|
+
return obj.map((item) => stripHeartbeatsInternal(item));
|
|
612
688
|
}
|
|
613
689
|
|
|
614
690
|
if (typeof obj === 'object') {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataSource,
|
|
3
|
+
EmbeddingInput,
|
|
4
|
+
EmbeddingResultItem,
|
|
5
|
+
EmbeddingsOptions,
|
|
6
|
+
EmbeddingsResult,
|
|
7
|
+
EmbeddingTaskType,
|
|
8
|
+
TextEmbeddingInput,
|
|
9
|
+
} from '@llumiverse/common';
|
|
10
|
+
import { readStreamAsBase64, readStreamAsUint8Array } from './stream.js';
|
|
11
|
+
|
|
12
|
+
/** DataSource wrapping an in-memory base64 string. */
|
|
13
|
+
export class Base64DataSource implements DataSource {
|
|
14
|
+
constructor(
|
|
15
|
+
public readonly name: string,
|
|
16
|
+
public readonly mime_type: string,
|
|
17
|
+
private readonly base64: string,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
async getStream(): Promise<ReadableStream<Uint8Array>> {
|
|
21
|
+
const bytes = Buffer.from(this.base64, 'base64');
|
|
22
|
+
return new ReadableStream<Uint8Array>({
|
|
23
|
+
start(controller) {
|
|
24
|
+
controller.enqueue(new Uint8Array(bytes));
|
|
25
|
+
controller.close();
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getURL(): Promise<string> {
|
|
31
|
+
return `data:${this.mime_type};base64,${this.base64}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async getURI(): Promise<string> {
|
|
35
|
+
return this.getURL();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Convenience accessor that avoids re-encoding via getStream. */
|
|
39
|
+
getBase64(): string {
|
|
40
|
+
return this.base64;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* DataSource wrapping a remote URL. The URL is exposed via getURL() so
|
|
46
|
+
* drivers can pass it directly to providers that support URL inputs
|
|
47
|
+
* (gs://, s3://, https://). getStream() fetches the URL on demand.
|
|
48
|
+
*/
|
|
49
|
+
export class URLDataSource implements DataSource {
|
|
50
|
+
constructor(
|
|
51
|
+
public readonly name: string,
|
|
52
|
+
public readonly mime_type: string,
|
|
53
|
+
private readonly url: string,
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
async getStream(): Promise<ReadableStream<Uint8Array>> {
|
|
57
|
+
const response = await fetch(this.url);
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`Failed to fetch ${this.url}: ${response.status} ${response.statusText}`);
|
|
60
|
+
}
|
|
61
|
+
if (!response.body) {
|
|
62
|
+
throw new Error(`Response body is empty for ${this.url}`);
|
|
63
|
+
}
|
|
64
|
+
return response.body as ReadableStream<Uint8Array>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getURL(): Promise<string> {
|
|
68
|
+
return this.url;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getURI(): Promise<string> {
|
|
72
|
+
return this.url;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read a DataSource as a base64-encoded string. Equivalent to streaming the
|
|
78
|
+
* source and base64-encoding the bytes.
|
|
79
|
+
*/
|
|
80
|
+
export async function dataSourceToBase64(ds: DataSource): Promise<string> {
|
|
81
|
+
if (ds instanceof Base64DataSource) {
|
|
82
|
+
return ds.getBase64();
|
|
83
|
+
}
|
|
84
|
+
return readStreamAsBase64(await ds.getStream());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read a DataSource fully into a Uint8Array.
|
|
89
|
+
*/
|
|
90
|
+
export async function dataSourceToBytes(ds: DataSource): Promise<Uint8Array> {
|
|
91
|
+
return readStreamAsUint8Array(await ds.getStream());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Returns the first vector from a single-input EmbeddingsResult. */
|
|
95
|
+
export function firstVector(result: EmbeddingsResult): number[] {
|
|
96
|
+
const first = result.results[0]?.outputs[0]?.values;
|
|
97
|
+
if (!first) {
|
|
98
|
+
throw new Error('EmbeddingsResult contains no vectors');
|
|
99
|
+
}
|
|
100
|
+
return first;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate an EmbeddingsOptions object and return a normalized copy where:
|
|
105
|
+
* - request-level task_type is propagated to text inputs that don't define their own
|
|
106
|
+
* - inputs is guaranteed non-empty
|
|
107
|
+
*
|
|
108
|
+
* Drivers should call this before doing any provider-specific work.
|
|
109
|
+
*/
|
|
110
|
+
export function normalizeEmbeddingsOptions(options: EmbeddingsOptions): EmbeddingsOptions {
|
|
111
|
+
if (!options.inputs || options.inputs.length === 0) {
|
|
112
|
+
throw new Error('EmbeddingsOptions.inputs must contain at least one input');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const inputs: EmbeddingInput[] = options.inputs.map((input) => {
|
|
116
|
+
if (input.type === 'text') {
|
|
117
|
+
const text = input as TextEmbeddingInput;
|
|
118
|
+
return {
|
|
119
|
+
...text,
|
|
120
|
+
task_type: text.task_type ?? options.task_type,
|
|
121
|
+
} satisfies TextEmbeddingInput;
|
|
122
|
+
}
|
|
123
|
+
return input;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
...options,
|
|
128
|
+
inputs,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Apply a task-type prompt prefix to a text input. Used by drivers that need
|
|
134
|
+
* to pass task type through the prompt when the model does not accept it as
|
|
135
|
+
* an API parameter (e.g. Vertex AI gemini-embedding-2).
|
|
136
|
+
*/
|
|
137
|
+
export function applyTaskTypePrefix(
|
|
138
|
+
text: string,
|
|
139
|
+
taskType: EmbeddingTaskType | undefined,
|
|
140
|
+
mapping: Partial<Record<EmbeddingTaskType, string>>,
|
|
141
|
+
): string {
|
|
142
|
+
if (!taskType) return text;
|
|
143
|
+
const prefix = mapping[taskType];
|
|
144
|
+
return prefix ? prefix + text : text;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Group result items so each item lines up with the corresponding input.
|
|
149
|
+
* Helper for drivers that fan out multiple provider calls.
|
|
150
|
+
*/
|
|
151
|
+
export function buildEmbeddingsResult(
|
|
152
|
+
model: string,
|
|
153
|
+
results: EmbeddingResultItem[],
|
|
154
|
+
usage?: EmbeddingsResult['usage'],
|
|
155
|
+
): EmbeddingsResult {
|
|
156
|
+
return { model, results, usage };
|
|
157
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { JSONSchema } from
|
|
1
|
+
import type { JSONSchema } from '@llumiverse/common';
|
|
2
2
|
|
|
3
3
|
export function getJSONSafetyNotice(schema: JSONSchema) {
|
|
4
|
-
return
|
|
4
|
+
return `The answer must be a JSON object using the following JSON Schema:\n${JSON.stringify(schema, undefined, 2)}`;
|
|
5
5
|
}
|
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
import { JSONSchema } from
|
|
2
|
-
import { PromptRole, PromptSegment } from
|
|
3
|
-
import { getJSONSafetyNotice } from
|
|
1
|
+
import type { JSONSchema } from '@llumiverse/common';
|
|
2
|
+
import { PromptRole, type PromptSegment } from '@llumiverse/common';
|
|
3
|
+
import { getJSONSafetyNotice } from './commons.js';
|
|
4
4
|
|
|
5
5
|
interface Labels {
|
|
6
|
-
user: string
|
|
7
|
-
system: string
|
|
8
|
-
assistant: string
|
|
9
|
-
safety: string
|
|
10
|
-
instruction: string
|
|
6
|
+
user: string;
|
|
7
|
+
system: string;
|
|
8
|
+
assistant: string;
|
|
9
|
+
safety: string;
|
|
10
|
+
instruction: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function createTextPromptFormatter(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
export function createTextPromptFormatter(
|
|
14
|
+
labels: Labels = {
|
|
15
|
+
user: 'USER',
|
|
16
|
+
system: 'CONTEXT',
|
|
17
|
+
assistant: 'ASSISTANT',
|
|
18
|
+
safety: 'IMPORTANT',
|
|
19
|
+
instruction: 'INSTRUCTION',
|
|
20
|
+
},
|
|
21
|
+
) {
|
|
20
22
|
return function genericTextPrompt(segments: PromptSegment[], schema?: JSONSchema): string {
|
|
21
|
-
const isChat = segments.find(m => m.role === PromptRole.assistant);
|
|
23
|
+
const isChat = segments.find((m) => m.role === PromptRole.assistant);
|
|
22
24
|
const context: string[] = [];
|
|
23
25
|
const content: string[] = [];
|
|
24
26
|
const safety: string[] = [];
|
|
@@ -59,8 +61,9 @@ export function createTextPromptFormatter(labels: Labels = {
|
|
|
59
61
|
out.push(`${labels.safety}: ${safety.join('\n')}`);
|
|
60
62
|
}
|
|
61
63
|
return out.join('\n');
|
|
62
|
-
}
|
|
64
|
+
};
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
const formatTextPrompt = createTextPromptFormatter();
|
|
68
|
+
|
|
66
69
|
export { formatTextPrompt };
|
package/src/formatters/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
1
|
+
export * from './commons.js';
|
|
2
|
+
export * from './generic.js';
|
|
3
|
+
export * from './nova.js';
|
package/src/formatters/nova.ts
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { JSONSchema } from
|
|
2
|
-
import { PromptRole, PromptSegment } from
|
|
3
|
-
import { readStreamAsBase64 } from
|
|
4
|
-
import { getJSONSafetyNotice } from
|
|
1
|
+
import type { JSONSchema } from '@llumiverse/common';
|
|
2
|
+
import { PromptRole, type PromptSegment } from '@llumiverse/common';
|
|
3
|
+
import { readStreamAsBase64 } from '../stream.js';
|
|
4
|
+
import { getJSONSafetyNotice } from './commons.js';
|
|
5
5
|
|
|
6
6
|
export interface NovaMessage {
|
|
7
|
-
role: 'user' | 'assistant'
|
|
8
|
-
content: NovaMessagePart[]
|
|
7
|
+
role: 'user' | 'assistant';
|
|
8
|
+
content: NovaMessagePart[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface NovaSystemMessage {
|
|
12
|
-
text: string
|
|
12
|
+
text: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
interface NovaMessagePart {
|
|
16
|
-
text?: string // only set for text messages
|
|
16
|
+
text?: string; // only set for text messages
|
|
17
17
|
image?: {
|
|
18
|
-
format:
|
|
18
|
+
format: 'jpeg' | 'png' | 'gif' | 'webp';
|
|
19
19
|
source: {
|
|
20
|
-
bytes: string //"base64",
|
|
21
|
-
}
|
|
22
|
-
}
|
|
20
|
+
bytes: string; //"base64",
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
23
|
video?: {
|
|
24
|
-
format:
|
|
24
|
+
format: 'mkv' | 'mov' | 'mp4' | 'webm' | 'three_gp' | 'flv' | 'mpeg' | 'mpg' | 'wmv';
|
|
25
25
|
source: {
|
|
26
26
|
//Option 1: sending a s3 location
|
|
27
27
|
s3Location?: {
|
|
28
|
-
uri: string
|
|
29
|
-
bucketOwner: string // optional. example: "123456789012"
|
|
30
|
-
}
|
|
28
|
+
uri: string; // example: s3://my-bucket/object-key
|
|
29
|
+
bucketOwner: string; // optional. example: "123456789012"
|
|
30
|
+
};
|
|
31
31
|
//Option 2: sending a base64 encoded video
|
|
32
|
-
bytes?: string //"base64",
|
|
33
|
-
}
|
|
34
|
-
}
|
|
32
|
+
bytes?: string; //"base64",
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export interface NovaMessagesPrompt {
|
|
@@ -49,36 +49,36 @@ export async function formatNovaPrompt(segments: PromptSegment[], schema?: JSONS
|
|
|
49
49
|
const system: string[] = [];
|
|
50
50
|
const safety: string[] = [];
|
|
51
51
|
const messages: NovaMessage[] = [];
|
|
52
|
-
let negative: string =
|
|
53
|
-
let mask: string =
|
|
52
|
+
let negative: string = '';
|
|
53
|
+
let mask: string = '';
|
|
54
54
|
|
|
55
55
|
for (const segment of segments) {
|
|
56
|
-
|
|
57
56
|
const parts: NovaMessagePart[] = [];
|
|
58
|
-
if (segment.files)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const source = await f.getStream();
|
|
65
|
-
const data = await readStreamAsBase64(source);
|
|
66
|
-
const format = f.mime_type?.split('/')[1] || 'png';
|
|
67
|
-
|
|
68
|
-
parts.push({
|
|
69
|
-
image: {
|
|
70
|
-
format: format as "jpeg" | "png" | "gif" | "webp",
|
|
71
|
-
source: {
|
|
72
|
-
bytes: data
|
|
73
|
-
}
|
|
57
|
+
if (segment.files)
|
|
58
|
+
for (const f of segment.files) {
|
|
59
|
+
//TODO add video support
|
|
60
|
+
if (!f.mime_type?.startsWith('image')) {
|
|
61
|
+
continue;
|
|
74
62
|
}
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
|
|
64
|
+
const source = await f.getStream();
|
|
65
|
+
const data = await readStreamAsBase64(source);
|
|
66
|
+
const format = f.mime_type?.split('/')[1] || 'png';
|
|
67
|
+
|
|
68
|
+
parts.push({
|
|
69
|
+
image: {
|
|
70
|
+
format: format as 'jpeg' | 'png' | 'gif' | 'webp',
|
|
71
|
+
source: {
|
|
72
|
+
bytes: data,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
77
|
|
|
78
78
|
if (segment.content) {
|
|
79
79
|
parts.push({
|
|
80
|
-
text: segment.content
|
|
81
|
-
})
|
|
80
|
+
text: segment.content,
|
|
81
|
+
});
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
if (segment.role === PromptRole.system) {
|
|
@@ -96,13 +96,13 @@ export async function formatNovaPrompt(segments: PromptSegment[], schema?: JSONS
|
|
|
96
96
|
} else if (segment.role !== PromptRole.tool) {
|
|
97
97
|
messages.push({
|
|
98
98
|
role: segment.role,
|
|
99
|
-
content: parts
|
|
99
|
+
content: parts,
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
if (schema) {
|
|
105
|
-
safety.push(
|
|
105
|
+
safety.push(`IMPORTANT: ${getJSONSafetyNotice(schema)}`);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// messages must contains at least 1 item. If the prompt does not contains a user message (but only system messages)
|
|
@@ -116,7 +116,7 @@ export async function formatNovaPrompt(segments: PromptSegment[], schema?: JSONS
|
|
|
116
116
|
messages.push({ content: [{ text: systemMessage }], role: 'user' });
|
|
117
117
|
systemMessage = safety.join('\n');
|
|
118
118
|
} else if (safety.length > 0) {
|
|
119
|
-
systemMessage = systemMessage
|
|
119
|
+
systemMessage = `${systemMessage}\n\nIMPORTANT: ${safety.join('\n')}`;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/*start Nova's message to make sure it answers properly in JSON
|
|
@@ -124,18 +124,20 @@ export async function formatNovaPrompt(segments: PromptSegment[], schema?: JSONS
|
|
|
124
124
|
|
|
125
125
|
if (schema) {
|
|
126
126
|
messages.push({
|
|
127
|
-
role:
|
|
128
|
-
content: [
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
role: 'assistant',
|
|
128
|
+
content: [
|
|
129
|
+
{
|
|
130
|
+
text: '{',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
131
133
|
});
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
// put system messages first and safety last
|
|
135
137
|
return {
|
|
136
|
-
system: systemMessage ? [{ text: systemMessage }] : [{ text:
|
|
138
|
+
system: systemMessage ? [{ text: systemMessage }] : [{ text: '' }],
|
|
137
139
|
messages: messages,
|
|
138
140
|
negative: negative || undefined,
|
|
139
141
|
mask: mask || undefined,
|
|
140
|
-
}
|
|
142
|
+
};
|
|
141
143
|
}
|