@llumiverse/core 0.22.0 → 0.23.0-dev-20251118
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/README.md +1 -1
- package/lib/cjs/CompletionStream.js +89 -20
- package/lib/cjs/CompletionStream.js.map +1 -1
- package/lib/cjs/Driver.js +23 -5
- package/lib/cjs/Driver.js.map +1 -1
- package/lib/cjs/async.js.map +1 -1
- package/lib/cjs/json.js +3 -174
- package/lib/cjs/json.js.map +1 -1
- package/lib/cjs/stream.js +16 -10
- package/lib/cjs/stream.js.map +1 -1
- package/lib/cjs/validation.js +31 -7
- package/lib/cjs/validation.js.map +1 -1
- package/lib/esm/CompletionStream.js +89 -20
- package/lib/esm/CompletionStream.js.map +1 -1
- package/lib/esm/Driver.js +23 -5
- package/lib/esm/Driver.js.map +1 -1
- package/lib/esm/async.js.map +1 -1
- package/lib/esm/json.js +3 -172
- package/lib/esm/json.js.map +1 -1
- package/lib/esm/stream.js +16 -10
- package/lib/esm/stream.js.map +1 -1
- package/lib/esm/validation.js +31 -7
- package/lib/esm/validation.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/CompletionStream.d.ts +2 -2
- package/lib/types/CompletionStream.d.ts.map +1 -1
- package/lib/types/Driver.d.ts +3 -3
- package/lib/types/Driver.d.ts.map +1 -1
- package/lib/types/async.d.ts +2 -2
- package/lib/types/async.d.ts.map +1 -1
- package/lib/types/json.d.ts +0 -13
- package/lib/types/json.d.ts.map +1 -1
- package/lib/types/stream.d.ts.map +1 -1
- package/lib/types/validation.d.ts +2 -2
- package/lib/types/validation.d.ts.map +1 -1
- package/package.json +87 -86
- package/src/CompletionStream.ts +88 -21
- package/src/Driver.ts +29 -13
- package/src/async.ts +4 -4
- package/src/json.ts +3 -168
- package/src/stream.ts +19 -11
- package/src/validation.ts +33 -10
package/src/CompletionStream.ts
CHANGED
|
@@ -3,39 +3,36 @@ import { AbstractDriver } from "./Driver.js";
|
|
|
3
3
|
|
|
4
4
|
export class DefaultCompletionStream<PromptT = any> implements CompletionStream<PromptT> {
|
|
5
5
|
|
|
6
|
-
chunks:
|
|
6
|
+
chunks: number; // Counter for number of chunks instead of storing strings
|
|
7
7
|
completion: ExecutionResponse<PromptT> | undefined;
|
|
8
8
|
|
|
9
9
|
constructor(public driver: AbstractDriver<DriverOptions, PromptT>,
|
|
10
10
|
public prompt: PromptT,
|
|
11
11
|
public options: ExecutionOptions) {
|
|
12
|
-
this.chunks =
|
|
12
|
+
this.chunks = 0;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async *[Symbol.asyncIterator]() {
|
|
16
16
|
// reset state
|
|
17
17
|
this.completion = undefined;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
const chunks = this.chunks;
|
|
18
|
+
this.chunks = 0;
|
|
19
|
+
const accumulatedResults: any[] = []; // Accumulate CompletionResult[] from chunks
|
|
22
20
|
|
|
23
21
|
this.driver.logger.debug(
|
|
24
22
|
`[${this.driver.provider}] Streaming Execution of ${this.options.model} with prompt`,
|
|
25
23
|
);
|
|
26
24
|
|
|
27
25
|
const start = Date.now();
|
|
28
|
-
let stream;
|
|
29
26
|
let finish_reason: string | undefined = undefined;
|
|
30
27
|
let promptTokens: number = 0;
|
|
31
28
|
let resultTokens: number | undefined = undefined;
|
|
32
29
|
|
|
33
30
|
try {
|
|
34
|
-
stream = await this.driver.requestTextCompletionStream(this.prompt, this.options);
|
|
31
|
+
const stream = await this.driver.requestTextCompletionStream(this.prompt, this.options);
|
|
35
32
|
for await (const chunk of stream) {
|
|
36
33
|
if (chunk) {
|
|
37
34
|
if (typeof chunk === 'string') {
|
|
38
|
-
chunks
|
|
35
|
+
this.chunks++;
|
|
39
36
|
yield chunk;
|
|
40
37
|
} else {
|
|
41
38
|
if (chunk.finish_reason) { //Do not replace non-null values with null values
|
|
@@ -48,9 +45,65 @@ export class DefaultCompletionStream<PromptT = any> implements CompletionStream<
|
|
|
48
45
|
promptTokens = Math.max(promptTokens, chunk.token_usage.prompt ?? 0);
|
|
49
46
|
resultTokens = Math.max(resultTokens ?? 0, chunk.token_usage.result ?? 0);
|
|
50
47
|
}
|
|
51
|
-
if (chunk.result) {
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
if (Array.isArray(chunk.result) && chunk.result.length > 0) {
|
|
49
|
+
// Process each result in the chunk, combining consecutive text/JSON
|
|
50
|
+
for (const result of chunk.result) {
|
|
51
|
+
// Check if we can combine with the last accumulated result
|
|
52
|
+
const lastResult = accumulatedResults[accumulatedResults.length - 1];
|
|
53
|
+
|
|
54
|
+
if (lastResult &&
|
|
55
|
+
((lastResult.type === 'text' && result.type === 'text') ||
|
|
56
|
+
(lastResult.type === 'json' && result.type === 'json'))) {
|
|
57
|
+
// Combine consecutive text or JSON results
|
|
58
|
+
if (result.type === 'text') {
|
|
59
|
+
lastResult.value += result.value;
|
|
60
|
+
} else if (result.type === 'json') {
|
|
61
|
+
// For JSON, combine the parsed objects directly
|
|
62
|
+
try {
|
|
63
|
+
const lastParsed = lastResult.value;
|
|
64
|
+
const currentParsed = result.value;
|
|
65
|
+
if (lastParsed !== null && typeof lastParsed === 'object' &&
|
|
66
|
+
currentParsed !== null && typeof currentParsed === 'object') {
|
|
67
|
+
const combined = { ...lastParsed, ...currentParsed };
|
|
68
|
+
lastResult.value = combined;
|
|
69
|
+
} else {
|
|
70
|
+
// If not objects, convert to string and concatenate
|
|
71
|
+
const lastStr = typeof lastParsed === 'string' ? lastParsed : JSON.stringify(lastParsed);
|
|
72
|
+
const currentStr = typeof currentParsed === 'string' ? currentParsed : JSON.stringify(currentParsed);
|
|
73
|
+
lastResult.value = lastStr + currentStr;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// If anything fails, just concatenate string representations
|
|
77
|
+
lastResult.value = String(lastResult.value) + String(result.value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// Add as new result
|
|
82
|
+
accumulatedResults.push(result);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Convert CompletionResult[] to string for streaming
|
|
87
|
+
// Only yield if we have results to show
|
|
88
|
+
const resultText = chunk.result.map(r => {
|
|
89
|
+
switch (r.type) {
|
|
90
|
+
case 'text':
|
|
91
|
+
return r.value;
|
|
92
|
+
case 'json':
|
|
93
|
+
return JSON.stringify(r.value);
|
|
94
|
+
case 'image':
|
|
95
|
+
// Show truncated image placeholder for streaming
|
|
96
|
+
const truncatedValue = typeof r.value === 'string' ? r.value.slice(0, 10) : String(r.value).slice(0, 10);
|
|
97
|
+
return `\n[Image: ${truncatedValue}...]\n`;
|
|
98
|
+
default:
|
|
99
|
+
return String((r as any).value || '');
|
|
100
|
+
}
|
|
101
|
+
}).join('');
|
|
102
|
+
|
|
103
|
+
if (resultText) {
|
|
104
|
+
this.chunks++;
|
|
105
|
+
yield resultText;
|
|
106
|
+
}
|
|
54
107
|
}
|
|
55
108
|
}
|
|
56
109
|
}
|
|
@@ -60,24 +113,24 @@ export class DefaultCompletionStream<PromptT = any> implements CompletionStream<
|
|
|
60
113
|
throw error;
|
|
61
114
|
}
|
|
62
115
|
|
|
63
|
-
const content = chunks.join('');
|
|
64
|
-
|
|
65
116
|
// Return undefined for the ExecutionTokenUsage object if there is nothing to fill it with.
|
|
66
|
-
// Allows for checking for
|
|
67
|
-
|
|
117
|
+
// Allows for checking for truthy-ness on token_usage, rather than it's internals. For testing and downstream usage.
|
|
118
|
+
const tokens: ExecutionTokenUsage | undefined = resultTokens ?
|
|
68
119
|
{ prompt: promptTokens, result: resultTokens, total: resultTokens + promptTokens, } : undefined
|
|
69
120
|
|
|
70
121
|
this.completion = {
|
|
71
|
-
result:
|
|
122
|
+
result: accumulatedResults, // Return the accumulated CompletionResult[] instead of text
|
|
72
123
|
prompt: this.prompt,
|
|
73
124
|
execution_time: Date.now() - start,
|
|
74
125
|
token_usage: tokens,
|
|
75
126
|
finish_reason: finish_reason,
|
|
76
|
-
chunks: chunks
|
|
127
|
+
chunks: this.chunks,
|
|
77
128
|
}
|
|
78
129
|
|
|
79
130
|
try {
|
|
80
|
-
|
|
131
|
+
if (this.completion) {
|
|
132
|
+
this.driver.validateResult(this.completion, this.options);
|
|
133
|
+
}
|
|
81
134
|
} catch (error: any) {
|
|
82
135
|
error.prompt = this.prompt;
|
|
83
136
|
throw error;
|
|
@@ -103,9 +156,23 @@ export class FallbackCompletionStream<PromptT = any> implements CompletionStream
|
|
|
103
156
|
);
|
|
104
157
|
try {
|
|
105
158
|
const completion = await this.driver._execute(this.prompt, this.options);
|
|
106
|
-
|
|
159
|
+
// For fallback streaming, yield the text content but keep the original completion
|
|
160
|
+
const content = completion.result.map(r => {
|
|
161
|
+
switch (r.type) {
|
|
162
|
+
case 'text':
|
|
163
|
+
return r.value;
|
|
164
|
+
case 'json':
|
|
165
|
+
return JSON.stringify(r.value);
|
|
166
|
+
case 'image':
|
|
167
|
+
// Show truncated image placeholder for streaming
|
|
168
|
+
const truncatedValue = typeof r.value === 'string' ? r.value.slice(0, 10) : String(r.value).slice(0, 10);
|
|
169
|
+
return `[Image: ${truncatedValue}...]`;
|
|
170
|
+
default:
|
|
171
|
+
return String((r as any).value || '');
|
|
172
|
+
}
|
|
173
|
+
}).join('');
|
|
107
174
|
yield content;
|
|
108
|
-
this.completion = completion;
|
|
175
|
+
this.completion = completion; // Return the original completion with untouched CompletionResult[]
|
|
109
176
|
} catch (error: any) {
|
|
110
177
|
error.prompt = this.prompt;
|
|
111
178
|
throw error;
|
package/src/Driver.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { formatTextPrompt } from "./formatters/index.js";
|
|
|
9
9
|
import {
|
|
10
10
|
AIModel,
|
|
11
11
|
Completion,
|
|
12
|
-
|
|
12
|
+
CompletionChunkObject,
|
|
13
13
|
CompletionStream,
|
|
14
14
|
DataSource,
|
|
15
15
|
DriverOptions,
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
EmbeddingsResult,
|
|
18
18
|
ExecutionOptions,
|
|
19
19
|
ExecutionResponse,
|
|
20
|
-
ImageGeneration,
|
|
21
20
|
Logger,
|
|
22
21
|
Modalities,
|
|
23
22
|
ModelSearchPayload,
|
|
@@ -29,19 +28,35 @@ import {
|
|
|
29
28
|
} from "@llumiverse/common";
|
|
30
29
|
import { validateResult } from "./validation.js";
|
|
31
30
|
|
|
31
|
+
// Helper to create logger methods that support both message-only and object-first signatures
|
|
32
|
+
function createConsoleLoggerMethod(consoleMethod: (...args: unknown[]) => void): Logger['info'] {
|
|
33
|
+
return ((objOrMsg: any, msgOrNever?: any, ...args: (string | number | boolean)[]) => {
|
|
34
|
+
if (typeof objOrMsg === 'string') {
|
|
35
|
+
// Message-only: logger.info("message", ...args)
|
|
36
|
+
consoleMethod(objOrMsg, msgOrNever, ...args);
|
|
37
|
+
} else if (msgOrNever !== undefined) {
|
|
38
|
+
// Object-first: logger.info({ obj }, "message", ...args)
|
|
39
|
+
consoleMethod(msgOrNever, objOrMsg, ...args);
|
|
40
|
+
} else {
|
|
41
|
+
// Object-only: logger.info({ obj })
|
|
42
|
+
consoleMethod(objOrMsg, ...args);
|
|
43
|
+
}
|
|
44
|
+
}) as Logger['info'];
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
const ConsoleLogger: Logger = {
|
|
33
|
-
debug: console.debug,
|
|
34
|
-
info: console.info,
|
|
35
|
-
warn: console.warn,
|
|
36
|
-
error: console.error,
|
|
48
|
+
debug: createConsoleLoggerMethod(console.debug.bind(console)),
|
|
49
|
+
info: createConsoleLoggerMethod(console.info.bind(console)),
|
|
50
|
+
warn: createConsoleLoggerMethod(console.warn.bind(console)),
|
|
51
|
+
error: createConsoleLoggerMethod(console.error.bind(console)),
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
const noop = () => void 0;
|
|
40
55
|
const NoopLogger: Logger = {
|
|
41
|
-
debug: noop,
|
|
42
|
-
info: noop,
|
|
43
|
-
warn: noop,
|
|
44
|
-
error: noop,
|
|
56
|
+
debug: noop as Logger['debug'],
|
|
57
|
+
info: noop as Logger['info'],
|
|
58
|
+
warn: noop as Logger['warn'],
|
|
59
|
+
error: noop as Logger['error'],
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
export function createLogger(logger: Logger | "console" | undefined) {
|
|
@@ -131,7 +146,8 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
|
|
|
131
146
|
try {
|
|
132
147
|
result.result = validateResult(result.result, options.result_schema);
|
|
133
148
|
} catch (error: any) {
|
|
134
|
-
|
|
149
|
+
const errorMessage = `[${this.provider}] [${options.model}] ${error.code ? '[' + error.code + '] ' : ''}Result validation error: ${error.message}`;
|
|
150
|
+
this.logger.error({ err: error, data: result.result }, errorMessage);
|
|
135
151
|
result.error = {
|
|
136
152
|
code: error.code || error.name,
|
|
137
153
|
message: error.message,
|
|
@@ -225,9 +241,9 @@ export abstract class AbstractDriver<OptionsT extends DriverOptions = DriverOpti
|
|
|
225
241
|
|
|
226
242
|
abstract requestTextCompletion(prompt: PromptT, options: ExecutionOptions): Promise<Completion>;
|
|
227
243
|
|
|
228
|
-
abstract requestTextCompletionStream(prompt: PromptT, options: ExecutionOptions): Promise<AsyncIterable<
|
|
244
|
+
abstract requestTextCompletionStream(prompt: PromptT, options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>>;
|
|
229
245
|
|
|
230
|
-
async requestImageGeneration(_prompt: PromptT, _options: ExecutionOptions): Promise<Completion
|
|
246
|
+
async requestImageGeneration(_prompt: PromptT, _options: ExecutionOptions): Promise<Completion> {
|
|
231
247
|
throw new Error("Image generation not implemented.");
|
|
232
248
|
//Cannot be made abstract, as abstract methods are required in the derived class
|
|
233
249
|
}
|
package/src/async.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ServerSentEvent } from "@vertesia/api-fetch-client"
|
|
2
|
-
import {
|
|
2
|
+
import { CompletionChunkObject } from "@llumiverse/common";
|
|
3
3
|
|
|
4
4
|
export async function* asyncMap<T, R>(asyncIterable: AsyncIterable<T>, callback: (value: T, index: number) => R) {
|
|
5
5
|
let i = 0;
|
|
@@ -18,9 +18,9 @@ export function oneAsyncIterator<T>(value: T): AsyncIterable<T> {
|
|
|
18
18
|
/**
|
|
19
19
|
* Given a ReadableStream of server sent events, tran
|
|
20
20
|
*/
|
|
21
|
-
export function transformSSEStream(stream: ReadableStream<ServerSentEvent>, transform: (data: string) =>
|
|
21
|
+
export function transformSSEStream(stream: ReadableStream<ServerSentEvent>, transform: (data: string) => CompletionChunkObject): ReadableStream<CompletionChunkObject> & AsyncIterable<CompletionChunkObject> {
|
|
22
22
|
// on node and bun the ReadableStream is an async iterable
|
|
23
|
-
return stream.pipeThrough(new TransformStream<ServerSentEvent,
|
|
23
|
+
return stream.pipeThrough(new TransformStream<ServerSentEvent, CompletionChunkObject>({
|
|
24
24
|
transform(event: ServerSentEvent, controller) {
|
|
25
25
|
if (event.type === 'event' && event.data && event.data !== '[DONE]') {
|
|
26
26
|
try {
|
|
@@ -32,7 +32,7 @@ export function transformSSEStream(stream: ReadableStream<ServerSentEvent>, tran
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
}))
|
|
35
|
+
})) satisfies ReadableStream<CompletionChunkObject> & AsyncIterable<CompletionChunkObject>;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export class EventStream<T, ReturnT = any> implements AsyncIterable<T> {
|
package/src/json.ts
CHANGED
|
@@ -1,181 +1,16 @@
|
|
|
1
1
|
import { JSONValue } from "@llumiverse/common";
|
|
2
|
+
import { jsonrepair } from 'jsonrepair';
|
|
2
3
|
|
|
3
4
|
function extractJsonFromText(text: string): string {
|
|
4
5
|
const start = text.indexOf("{");
|
|
5
6
|
const end = text.lastIndexOf("}");
|
|
6
|
-
|
|
7
|
-
return text.replace(/\\n/g, "");
|
|
7
|
+
return text.substring(start, end + 1);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function extractAndParseJSON(text: string): JSONValue {
|
|
11
11
|
return parseJSON(extractJsonFromText(text));
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const RX_DQUOTE = /^"([^"\\]|\\.)*"/us;
|
|
15
|
-
const RX_SQUOTE = /^'([^'\\]|\\.)*'/us;
|
|
16
|
-
const RX_NUMBER = /^-?\d+(\.\d+)?/;
|
|
17
|
-
const RX_BOOLEAN = /^true|false/;
|
|
18
|
-
const RX_NULL = /^null/;
|
|
19
|
-
const RX_KEY = /^[$_a-zA-Z][$_a-zA-Z0-9]*/;
|
|
20
|
-
const RX_PUNCTUATION = /^\s*([\[\]{}:,])\s*/;
|
|
21
|
-
|
|
22
|
-
function fixText(value: string) {
|
|
23
|
-
return value.replaceAll('\n', '\\n').replaceAll('\r', '\\r');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function decodeSingleQuotedString(value: string) {
|
|
27
|
-
return JSON.parse('"' + value.slice(1, -1).replaceAll(/(?<!\\)"/g, '\\"') + '"');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class JsonParser {
|
|
31
|
-
pos: number = 0;
|
|
32
|
-
|
|
33
|
-
constructor(public text: string) { }
|
|
34
|
-
|
|
35
|
-
skip(n: number) {
|
|
36
|
-
this.text = this.text.substring(n);
|
|
37
|
-
this.pos += n;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
tryReadPunctuation() {
|
|
41
|
-
const m = RX_PUNCTUATION.exec(this.text);
|
|
42
|
-
if (m) {
|
|
43
|
-
this.skip(m[0].length);
|
|
44
|
-
return m[1];
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
readKey() {
|
|
49
|
-
const first = this.text.charCodeAt(0);
|
|
50
|
-
if (first === 34) { // "
|
|
51
|
-
const m = RX_DQUOTE.exec(this.text);
|
|
52
|
-
if (m) {
|
|
53
|
-
this.skip(m[0].length);
|
|
54
|
-
return JSON.parse(m[0]);
|
|
55
|
-
}
|
|
56
|
-
} else if (first === 39) { // '
|
|
57
|
-
const m = RX_SQUOTE.exec(this.text);
|
|
58
|
-
if (m) {
|
|
59
|
-
this.skip(m[0].length);
|
|
60
|
-
return decodeSingleQuotedString(m[0]);
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
const m = RX_KEY.exec(this.text);
|
|
64
|
-
if (m) {
|
|
65
|
-
this.skip(m[0].length);
|
|
66
|
-
return m[0];
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
throw new Error('Expected a key at position ' + this.pos + ' but found ' + this.text);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
readScalar() {
|
|
73
|
-
const first = this.text.charCodeAt(0);
|
|
74
|
-
if (first === 34) { // "
|
|
75
|
-
const m = RX_DQUOTE.exec(this.text);
|
|
76
|
-
if (m) {
|
|
77
|
-
this.skip(m[0].length);
|
|
78
|
-
return JSON.parse(fixText(m[0]));
|
|
79
|
-
}
|
|
80
|
-
} else if (first === 39) { // '
|
|
81
|
-
const m = RX_SQUOTE.exec(this.text);
|
|
82
|
-
if (m) {
|
|
83
|
-
this.skip(m[0].length);
|
|
84
|
-
return decodeSingleQuotedString(fixText(m[0]));
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
87
|
-
let m = RX_NUMBER.exec(this.text);
|
|
88
|
-
if (m) {
|
|
89
|
-
this.skip(m[0].length);
|
|
90
|
-
return parseFloat(m[0]);
|
|
91
|
-
}
|
|
92
|
-
m = RX_BOOLEAN.exec(this.text);
|
|
93
|
-
if (m) {
|
|
94
|
-
this.skip(m[0].length);
|
|
95
|
-
return m[0] === 'true';
|
|
96
|
-
}
|
|
97
|
-
m = RX_NULL.exec(this.text);
|
|
98
|
-
if (m) {
|
|
99
|
-
this.skip(m[0].length);
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
throw new Error('Expected a value at position ' + this.pos + ' but found ' + this.text);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
readObject() {
|
|
107
|
-
let key: string | undefined;
|
|
108
|
-
const obj: any = {};
|
|
109
|
-
while (true) {
|
|
110
|
-
if (!key) { // read key
|
|
111
|
-
const p = this.tryReadPunctuation();
|
|
112
|
-
if (p === '}') {
|
|
113
|
-
return obj;
|
|
114
|
-
} else if (p === ',') {
|
|
115
|
-
continue;
|
|
116
|
-
} else if (p) {
|
|
117
|
-
throw new Error('Expected a key at position ' + this.pos + ' but found ' + this.text);
|
|
118
|
-
}
|
|
119
|
-
key = this.readKey();
|
|
120
|
-
if (!key) {
|
|
121
|
-
throw new Error('Expected a key at position ' + this.pos + ' but found ' + this.text);
|
|
122
|
-
}
|
|
123
|
-
if (this.tryReadPunctuation() !== ':') {
|
|
124
|
-
throw new Error('Expected a colon at position ' + this.pos + ' but found ' + this.text);
|
|
125
|
-
};
|
|
126
|
-
} else { // read value
|
|
127
|
-
const value = this.readValue();
|
|
128
|
-
if (value === undefined) {
|
|
129
|
-
throw new Error('Expected a value at position ' + this.pos + ' but found ' + this.text);
|
|
130
|
-
}
|
|
131
|
-
obj[key] = value;
|
|
132
|
-
key = undefined;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
readArray() {
|
|
138
|
-
const ar: any[] = [];
|
|
139
|
-
while (true) {
|
|
140
|
-
const p = this.tryReadPunctuation();
|
|
141
|
-
if (p === ',') {
|
|
142
|
-
continue;
|
|
143
|
-
} else if (p === ']') {
|
|
144
|
-
return ar;
|
|
145
|
-
} else if (p === '[') {
|
|
146
|
-
ar.push(this.readArray());
|
|
147
|
-
} else if (p === '{') {
|
|
148
|
-
ar.push(this.readObject());
|
|
149
|
-
} else if (!p) {
|
|
150
|
-
ar.push(this.readScalar());
|
|
151
|
-
} else {
|
|
152
|
-
throw new Error('Expected a value at position ' + this.pos + ' but found ' + this.text);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
readValue() {
|
|
158
|
-
const p = this.tryReadPunctuation();
|
|
159
|
-
if (p === '{') {
|
|
160
|
-
return this.readObject();
|
|
161
|
-
} else if (p === '[') {
|
|
162
|
-
return this.readArray();
|
|
163
|
-
} else if (!p) {
|
|
164
|
-
return this.readScalar();
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
static parse(text: string) {
|
|
169
|
-
const parser = new JsonParser(text);
|
|
170
|
-
const r = parser.readValue();
|
|
171
|
-
if (r === undefined) {
|
|
172
|
-
throw new Error('Not a valid JSON');
|
|
173
|
-
}
|
|
174
|
-
return r;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
14
|
export function parseJSON(text: string): JSONValue {
|
|
180
15
|
text = text.trim();
|
|
181
16
|
try {
|
|
@@ -183,7 +18,7 @@ export function parseJSON(text: string): JSONValue {
|
|
|
183
18
|
} catch (err: any) {
|
|
184
19
|
// use a relaxed parser
|
|
185
20
|
try {
|
|
186
|
-
return
|
|
21
|
+
return JSON.parse(jsonrepair(text));
|
|
187
22
|
} catch (err2: any) { // throw the original error
|
|
188
23
|
throw err;
|
|
189
24
|
}
|
package/src/stream.ts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
|
|
2
2
|
export async function readStreamAsBase64(stream: ReadableStream): Promise<string> {
|
|
3
|
-
|
|
3
|
+
const uint8Array = await readStreamAsUint8Array(stream);
|
|
4
|
+
return Buffer.from(uint8Array).toString('base64');
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export async function readStreamAsString(stream: ReadableStream): Promise<string> {
|
|
7
|
-
|
|
8
|
+
const uint8Array = await readStreamAsUint8Array(stream);
|
|
9
|
+
return Buffer.from(uint8Array).toString();
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export async function readStreamAsUint8Array(stream: ReadableStream): Promise<Uint8Array> {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function _readStreamAsBuffer(stream: ReadableStream): Promise<Buffer> {
|
|
17
|
-
const out: Buffer[] = [];
|
|
13
|
+
const chunks: Uint8Array[] = [];
|
|
14
|
+
let totalLength = 0;
|
|
15
|
+
|
|
18
16
|
for await (const chunk of stream) {
|
|
19
|
-
|
|
17
|
+
const uint8Chunk = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
18
|
+
chunks.push(uint8Chunk);
|
|
19
|
+
totalLength += uint8Chunk.length;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const combined = new Uint8Array(totalLength);
|
|
23
|
+
let offset = 0;
|
|
24
|
+
for (const chunk of chunks) {
|
|
25
|
+
combined.set(chunk, offset);
|
|
26
|
+
offset += chunk.length;
|
|
20
27
|
}
|
|
21
|
-
|
|
28
|
+
|
|
29
|
+
return combined;
|
|
22
30
|
}
|
package/src/validation.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { CompletionResult, ResultValidationError } from "@llumiverse/common";
|
|
1
2
|
import { Ajv } from 'ajv';
|
|
2
3
|
import addFormats from 'ajv-formats';
|
|
3
4
|
import { extractAndParseJSON } from "./json.js";
|
|
4
5
|
import { resolveField } from './resolver.js';
|
|
5
|
-
import { ResultValidationError } from "@llumiverse/common";
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
const ajv = new Ajv({
|
|
@@ -28,17 +28,40 @@ export class ValidationError extends Error implements ResultValidationError {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
let
|
|
31
|
+
function parseCompletionAsJson(data: CompletionResult[]) {
|
|
32
|
+
let lastError: ValidationError | undefined;
|
|
33
|
+
for (const part of data) {
|
|
34
|
+
if (part.type === "text") {
|
|
35
|
+
const text = part.value.trim();
|
|
36
|
+
try {
|
|
37
|
+
return extractAndParseJSON(text);
|
|
38
|
+
} catch (error: any) {
|
|
39
|
+
lastError = new ValidationError("json_error", error.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!lastError) {
|
|
44
|
+
lastError = new ValidationError("json_error", "No JSON compatible response found in completion result");
|
|
45
|
+
}
|
|
46
|
+
throw lastError;
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
export function validateResult(data: CompletionResult[], schema: Object): CompletionResult[] {
|
|
51
|
+
let json;
|
|
52
|
+
if (Array.isArray(data)) {
|
|
53
|
+
const jsonResults = data.filter(r => r.type === "json");
|
|
54
|
+
if (jsonResults.length > 0) {
|
|
55
|
+
json = jsonResults[0].value;
|
|
56
|
+
} else {
|
|
57
|
+
try {
|
|
58
|
+
json = parseCompletionAsJson(data);
|
|
59
|
+
} catch (error: any) {
|
|
60
|
+
throw new ValidationError("json_error", error.message)
|
|
61
|
+
}
|
|
39
62
|
}
|
|
40
63
|
} else {
|
|
41
|
-
|
|
64
|
+
throw new Error("Data to validate must be an array")
|
|
42
65
|
}
|
|
43
66
|
|
|
44
67
|
const validate = ajv.compile(schema);
|
|
@@ -71,5 +94,5 @@ export function validateResult(data: any, schema: Object) {
|
|
|
71
94
|
}
|
|
72
95
|
}
|
|
73
96
|
|
|
74
|
-
return json;
|
|
97
|
+
return [{ type: "json", value: json }];
|
|
75
98
|
}
|