@valni/cli-darwin-x64 0.1.5 → 0.1.7
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/package.json +1 -1
- package/runtime/extensions/pi-switchboard/extensions/switchboard/catalog.ts +20 -18
- package/runtime/extensions/pi-switchboard/extensions/switchboard/index.ts +13 -8
- package/runtime/extensions/pi-switchboard/extensions/switchboard/toolProxy.ts +3 -2
- package/runtime/extensions/pi-switchboard/extensions/switchboard/tools.ts +3 -1
- package/runtime/extensions/pi-switchboard/extensions/switchboard/types.ts +3 -0
- package/runtime/extensions/pi-switchboard/package.json +3 -3
- package/runtime/extensions/switchboard-local/extensions/local/eventStream.ts +88 -0
- package/runtime/extensions/switchboard-local/extensions/local/hostContext.ts +49 -0
- package/runtime/extensions/switchboard-local/extensions/local/jsonParse.ts +124 -0
- package/runtime/extensions/switchboard-local/extensions/local/nativeStream.ts +20 -12
- package/runtime/extensions/switchboard-local/node_modules/partial-json/LICENSE +21 -0
- package/runtime/extensions/switchboard-local/node_modules/partial-json/README.md +131 -0
- package/runtime/extensions/switchboard-local/node_modules/partial-json/dist/index.js +220 -0
- package/runtime/extensions/switchboard-local/node_modules/partial-json/dist/options.js +72 -0
- package/runtime/extensions/switchboard-local/node_modules/partial-json/package.json +43 -0
- package/runtime/extensions/switchboard-local/package.json +5 -4
- package/runtime/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/runtime/package.json +6 -6
- package/runtime/valni +1 -1
package/package.json
CHANGED
|
@@ -48,13 +48,11 @@ export async function loadRegistryModels(): Promise<Record<string, RegistryModel
|
|
|
48
48
|
const distDirectory = findPiAiDist();
|
|
49
49
|
const registry: Record<string, RegistryModel> = {};
|
|
50
50
|
if (distDirectory === undefined) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
51
|
+
// The catalog supplies the context window and the display name, and a
|
|
52
|
+
// genuinely missing context window is reported precisely in
|
|
53
|
+
// buildModelConfigs. The registry is only a source of last resort.
|
|
55
54
|
return registry;
|
|
56
55
|
}
|
|
57
|
-
const missing: string[] = [];
|
|
58
56
|
for (const file of REGISTRY_FILES) {
|
|
59
57
|
let loaded: Record<string, Record<string, RegistryModel>>;
|
|
60
58
|
try {
|
|
@@ -63,10 +61,9 @@ export async function loadRegistryModels(): Promise<Record<string, RegistryModel
|
|
|
63
61
|
Record<string, RegistryModel>
|
|
64
62
|
>;
|
|
65
63
|
} catch (error) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
64
|
+
// A registry file that is not there is not a problem: everything a
|
|
65
|
+
// user sees first comes from the catalog.
|
|
66
|
+
if (isModuleNotFound(error)) continue;
|
|
70
67
|
throw error;
|
|
71
68
|
}
|
|
72
69
|
for (const exported of Object.values(loaded)) {
|
|
@@ -76,11 +73,6 @@ export async function loadRegistryModels(): Promise<Record<string, RegistryModel
|
|
|
76
73
|
}
|
|
77
74
|
}
|
|
78
75
|
}
|
|
79
|
-
if (missing.length === REGISTRY_FILES.length) {
|
|
80
|
-
console.error(
|
|
81
|
-
`pi-switchboard: no model registry in ${distDirectory}/providers (${missing.join(", ")}). Models will list without a context window or display name.`,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
76
|
return registry;
|
|
85
77
|
}
|
|
86
78
|
|
|
@@ -109,10 +101,11 @@ function toModelConfig(
|
|
|
109
101
|
price: ModelRecordPrice | undefined,
|
|
110
102
|
registryModel: RegistryModel | undefined,
|
|
111
103
|
catalogContextWindow: number | undefined,
|
|
104
|
+
catalogDisplayName: string | undefined,
|
|
112
105
|
) {
|
|
113
106
|
return {
|
|
114
107
|
id: record.id,
|
|
115
|
-
name: registryModel?.name ?? record.id,
|
|
108
|
+
name: catalogDisplayName ?? registryModel?.name ?? record.id,
|
|
116
109
|
api: KIND_TO_API[kindTag],
|
|
117
110
|
baseUrl: `${resolveBaseUrl()}${SENTINEL_SEGMENT}${kindTag}`,
|
|
118
111
|
reasoning: supportsReasoning(kindTag, profile) ?? registryModel?.reasoning ?? false,
|
|
@@ -127,12 +120,13 @@ function toModelConfig(
|
|
|
127
120
|
};
|
|
128
121
|
}
|
|
129
122
|
|
|
130
|
-
export async function discoverCatalog(baseUrl: string, bearer: string): Promise<ModelsPage> {
|
|
123
|
+
export async function discoverCatalog(baseUrl: string, bearer: string, signal?: AbortSignal): Promise<ModelsPage> {
|
|
124
|
+
const timeout = AbortSignal.timeout(CATALOG_FETCH_TIMEOUT_MS);
|
|
131
125
|
let response: Response;
|
|
132
126
|
try {
|
|
133
127
|
response = await fetch(`${baseUrl}${MODELS_PATH}`, {
|
|
134
128
|
headers: { Authorization: `Bearer ${bearer}` },
|
|
135
|
-
signal: AbortSignal.timeout
|
|
129
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
136
130
|
});
|
|
137
131
|
} catch (error) {
|
|
138
132
|
if (isAbortError(error)) throw error;
|
|
@@ -168,7 +162,15 @@ export function buildModelConfigs(catalog: ModelsPage, registry: Record<string,
|
|
|
168
162
|
const catalogContextWindow = catalog.context_windows?.[record.id];
|
|
169
163
|
if (catalogContextWindow === undefined && registry[record.id]?.contextWindow === undefined) assumed++;
|
|
170
164
|
modelConfigs.push(
|
|
171
|
-
toModelConfig(
|
|
165
|
+
toModelConfig(
|
|
166
|
+
record,
|
|
167
|
+
kindTag,
|
|
168
|
+
record.kind[kindTag],
|
|
169
|
+
catalog.prices[record.id],
|
|
170
|
+
registry[record.id],
|
|
171
|
+
catalogContextWindow,
|
|
172
|
+
catalog.display_names?.[record.id],
|
|
173
|
+
),
|
|
172
174
|
);
|
|
173
175
|
}
|
|
174
176
|
if (skipped.length > 0) {
|
|
@@ -99,22 +99,27 @@ export default async function (pi: ExtensionAPI) {
|
|
|
99
99
|
refreshModels: async (context: RefreshModelsContext) => {
|
|
100
100
|
const bearer = credentialBearer(context.credential);
|
|
101
101
|
if (!bearer) {
|
|
102
|
-
await context.
|
|
102
|
+
await context.publish({ persist: null });
|
|
103
103
|
return [];
|
|
104
104
|
}
|
|
105
|
-
const stored = await context.store.read();
|
|
106
105
|
if (!context.allowNetwork) {
|
|
107
|
-
return stored
|
|
106
|
+
return context.stored
|
|
107
|
+
? ([...context.stored.models] as ReturnType<typeof buildModelConfigs>)
|
|
108
|
+
: startupModels;
|
|
108
109
|
}
|
|
109
|
-
const catalog = await discoverCatalog(baseUrl, bearer);
|
|
110
|
+
const catalog = await discoverCatalog(baseUrl, bearer, context.signal);
|
|
110
111
|
const configs = buildModelConfigs(catalog, registry);
|
|
111
|
-
await context.
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
const published = await context.publish({
|
|
113
|
+
persist: {
|
|
114
|
+
models: configs as unknown as ModelsStoreEntry["models"],
|
|
115
|
+
checkedAt: Math.floor(Date.now() / MILLISECONDS_PER_SECOND),
|
|
116
|
+
},
|
|
114
117
|
});
|
|
118
|
+
if (!published) return configs;
|
|
115
119
|
if (context.credential?.type === "oauth") {
|
|
116
120
|
try {
|
|
117
|
-
|
|
121
|
+
const tools = await discoverTools(baseUrl, bearer, context.signal);
|
|
122
|
+
if (!context.signal.aborted) registerServerTools(pi, tools);
|
|
118
123
|
} catch (error) {
|
|
119
124
|
console.error("pi-switchboard: server tool discovery failed", error);
|
|
120
125
|
}
|
|
@@ -28,14 +28,15 @@ export interface AdvertisedTool {
|
|
|
28
28
|
version?: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export async function discoverTools(baseUrl: string, bearer: string): Promise<AdvertisedTool[]> {
|
|
31
|
+
export async function discoverTools(baseUrl: string, bearer: string, signal?: AbortSignal): Promise<AdvertisedTool[]> {
|
|
32
|
+
const timeout = AbortSignal.timeout(DISCOVER_TIMEOUT_MS);
|
|
32
33
|
let response: Response;
|
|
33
34
|
try {
|
|
34
35
|
response = await fetch(`${baseUrl}${MCP_PATH}`, {
|
|
35
36
|
method: "POST",
|
|
36
37
|
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${bearer}` },
|
|
37
38
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
38
|
-
signal: AbortSignal.timeout
|
|
39
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
39
40
|
});
|
|
40
41
|
} catch {
|
|
41
42
|
return [];
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// typebox directly, not through a harness package: this extension runs in
|
|
2
|
+
// more than one host and must not name any of them at runtime.
|
|
3
|
+
import { Type } from "typebox";
|
|
2
4
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
5
|
import { type AdvertisedTool, makeProxyExecute } from "./toolProxy.ts";
|
|
4
6
|
|
|
@@ -31,7 +31,10 @@ export interface ModelRecordPrice {
|
|
|
31
31
|
export interface ModelsPage {
|
|
32
32
|
models: ModelRecord[];
|
|
33
33
|
prices: Record<string, ModelRecordPrice>;
|
|
34
|
+
/** Real context window per model id, from the catalog. */
|
|
34
35
|
context_windows?: Record<string, number>;
|
|
36
|
+
/** Human-readable name per model id, from the catalog. */
|
|
37
|
+
display_names?: Record<string, string>;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export type RegistryModel = Model<(typeof KIND_TO_API)[SwitchboardKind]>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-switchboard",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Route the pi coding agent through Switchboard: one prepaid balance, spend controls, and a ledger for every model in the catalog",
|
|
6
6
|
"keywords": [
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
]
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@earendil-works/pi-ai": "0.
|
|
27
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
26
|
+
"@earendil-works/pi-ai": ">=1.0.0",
|
|
27
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
28
28
|
"typescript": "5.9.3"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { AssistantMessage, AssistantMessageEvent } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
// Generic event stream class for async iteration
|
|
4
|
+
export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
5
|
+
private queue: T[] = [];
|
|
6
|
+
private waiting: ((value: IteratorResult<T>) => void)[] = [];
|
|
7
|
+
private done = false;
|
|
8
|
+
private finalResultPromise: Promise<R>;
|
|
9
|
+
private resolveFinalResult!: (result: R) => void;
|
|
10
|
+
private isComplete: (event: T) => boolean;
|
|
11
|
+
private extractResult: (event: T) => R;
|
|
12
|
+
|
|
13
|
+
constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) {
|
|
14
|
+
this.isComplete = isComplete;
|
|
15
|
+
this.extractResult = extractResult;
|
|
16
|
+
this.finalResultPromise = new Promise((resolve) => {
|
|
17
|
+
this.resolveFinalResult = resolve;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
push(event: T): void {
|
|
22
|
+
if (this.done) return;
|
|
23
|
+
|
|
24
|
+
if (this.isComplete(event)) {
|
|
25
|
+
this.done = true;
|
|
26
|
+
this.resolveFinalResult(this.extractResult(event));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Deliver to waiting consumer or queue it
|
|
30
|
+
const waiter = this.waiting.shift();
|
|
31
|
+
if (waiter) {
|
|
32
|
+
waiter({ value: event, done: false });
|
|
33
|
+
} else {
|
|
34
|
+
this.queue.push(event);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
end(result?: R): void {
|
|
39
|
+
this.done = true;
|
|
40
|
+
if (result !== undefined) {
|
|
41
|
+
this.resolveFinalResult(result);
|
|
42
|
+
}
|
|
43
|
+
// Notify all waiting consumers that we're done
|
|
44
|
+
while (this.waiting.length > 0) {
|
|
45
|
+
const waiter = this.waiting.shift()!;
|
|
46
|
+
waiter({ value: undefined as any, done: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
51
|
+
while (true) {
|
|
52
|
+
if (this.queue.length > 0) {
|
|
53
|
+
yield this.queue.shift()!;
|
|
54
|
+
} else if (this.done) {
|
|
55
|
+
return;
|
|
56
|
+
} else {
|
|
57
|
+
const result = await new Promise<IteratorResult<T>>((resolve) => this.waiting.push(resolve));
|
|
58
|
+
if (result.done) return;
|
|
59
|
+
yield result.value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
result(): Promise<R> {
|
|
65
|
+
return this.finalResultPromise;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
|
|
70
|
+
constructor() {
|
|
71
|
+
super(
|
|
72
|
+
(event) => event.type === "done" || event.type === "error",
|
|
73
|
+
(event) => {
|
|
74
|
+
if (event.type === "done") {
|
|
75
|
+
return event.message;
|
|
76
|
+
} else if (event.type === "error") {
|
|
77
|
+
return event.error;
|
|
78
|
+
}
|
|
79
|
+
throw new Error("Unexpected event type for final result");
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Factory function for AssistantMessageEventStream (for use in extensions) */
|
|
86
|
+
export function createAssistantMessageEventStream(): AssistantMessageEventStream {
|
|
87
|
+
return new AssistantMessageEventStream();
|
|
88
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TextContent, Tool, TranscriptContext } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read the system prompt and the tools out of a transcript.
|
|
5
|
+
*
|
|
6
|
+
* Both hosts hand a provider the same shape now: system messages replayed in
|
|
7
|
+
* order, where later content appends to the prompt and tools are added and
|
|
8
|
+
* removed by name. Doing the replay here rather than calling a host helper is
|
|
9
|
+
* what keeps a host package out of the runtime imports.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface SystemMessage {
|
|
13
|
+
role: string;
|
|
14
|
+
content: string | TextContent[];
|
|
15
|
+
toolsAdded?: Tool[];
|
|
16
|
+
toolsRemoved?: { name: string }[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isSystemMessage(message: unknown): message is SystemMessage {
|
|
20
|
+
return message !== null && typeof message === "object" && (message as { role?: unknown }).role === "system";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function contentText(content: string | TextContent[]): string {
|
|
24
|
+
if (typeof content === "string") return content;
|
|
25
|
+
return content
|
|
26
|
+
.filter((part) => part.type === "text")
|
|
27
|
+
.map((part) => part.text)
|
|
28
|
+
.join("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function hostSystemPrompt(context: TranscriptContext): string | undefined {
|
|
32
|
+
const parts: string[] = [];
|
|
33
|
+
for (const message of context.messages) {
|
|
34
|
+
if (!isSystemMessage(message)) continue;
|
|
35
|
+
const text = contentText(message.content);
|
|
36
|
+
if (text.length > 0) parts.push(text);
|
|
37
|
+
}
|
|
38
|
+
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function hostTools(context: TranscriptContext): Tool[] {
|
|
42
|
+
const tools = new Map<string, Tool>();
|
|
43
|
+
for (const message of context.messages) {
|
|
44
|
+
if (!isSystemMessage(message)) continue;
|
|
45
|
+
for (const tool of message.toolsRemoved ?? []) tools.delete(tool.name);
|
|
46
|
+
for (const tool of message.toolsAdded ?? []) tools.set(tool.name, tool);
|
|
47
|
+
}
|
|
48
|
+
return [...tools.values()];
|
|
49
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { parse as partialParse } from "partial-json";
|
|
2
|
+
|
|
3
|
+
const VALID_JSON_ESCAPES = new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
|
|
4
|
+
|
|
5
|
+
function isControlCharacter(char: string): boolean {
|
|
6
|
+
const codePoint = char.codePointAt(0);
|
|
7
|
+
return codePoint !== undefined && codePoint >= 0x00 && codePoint <= 0x1f;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function escapeControlCharacter(char: string): string {
|
|
11
|
+
switch (char) {
|
|
12
|
+
case "\b":
|
|
13
|
+
return "\\b";
|
|
14
|
+
case "\f":
|
|
15
|
+
return "\\f";
|
|
16
|
+
case "\n":
|
|
17
|
+
return "\\n";
|
|
18
|
+
case "\r":
|
|
19
|
+
return "\\r";
|
|
20
|
+
case "\t":
|
|
21
|
+
return "\\t";
|
|
22
|
+
default:
|
|
23
|
+
return `\\u${char.codePointAt(0)?.toString(16).padStart(4, "0") ?? "0000"}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Repairs malformed JSON string literals by:
|
|
29
|
+
* - escaping raw control characters inside strings
|
|
30
|
+
* - doubling backslashes before invalid escape characters
|
|
31
|
+
*/
|
|
32
|
+
export function repairJson(json: string): string {
|
|
33
|
+
let repaired = "";
|
|
34
|
+
let inString = false;
|
|
35
|
+
|
|
36
|
+
for (let index = 0; index < json.length; index++) {
|
|
37
|
+
const char = json[index];
|
|
38
|
+
|
|
39
|
+
if (!inString) {
|
|
40
|
+
repaired += char;
|
|
41
|
+
if (char === '"') {
|
|
42
|
+
inString = true;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (char === '"') {
|
|
48
|
+
repaired += char;
|
|
49
|
+
inString = false;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (char === "\\") {
|
|
54
|
+
const nextChar = json[index + 1];
|
|
55
|
+
if (nextChar === undefined) {
|
|
56
|
+
repaired += "\\\\";
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (nextChar === "u") {
|
|
61
|
+
const unicodeDigits = json.slice(index + 2, index + 6);
|
|
62
|
+
if (/^[0-9a-fA-F]{4}$/.test(unicodeDigits)) {
|
|
63
|
+
repaired += `\\u${unicodeDigits}`;
|
|
64
|
+
index += 5;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (VALID_JSON_ESCAPES.has(nextChar)) {
|
|
70
|
+
repaired += `\\${nextChar}`;
|
|
71
|
+
index += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
repaired += "\\\\";
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
repaired += isControlCharacter(char) ? escapeControlCharacter(char) : char;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return repaired;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseJsonWithRepair<T>(json: string): T {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(json) as T;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const repairedJson = repairJson(json);
|
|
90
|
+
if (repairedJson !== json) {
|
|
91
|
+
return JSON.parse(repairedJson) as T;
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Attempts to parse potentially incomplete JSON during streaming.
|
|
99
|
+
* Always returns a valid object, even if the JSON is incomplete.
|
|
100
|
+
*
|
|
101
|
+
* @param partialJson The partial JSON string from streaming
|
|
102
|
+
* @returns Parsed object or empty object if parsing fails
|
|
103
|
+
*/
|
|
104
|
+
export function parseStreamingJson<T = Record<string, unknown>>(partialJson: string | undefined): T {
|
|
105
|
+
if (!partialJson || partialJson.trim() === "") {
|
|
106
|
+
return {} as T;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
return parseJsonWithRepair<T>(partialJson);
|
|
111
|
+
} catch {
|
|
112
|
+
try {
|
|
113
|
+
const result = partialParse(partialJson);
|
|
114
|
+
return (result ?? {}) as T;
|
|
115
|
+
} catch {
|
|
116
|
+
try {
|
|
117
|
+
const result = partialParse(repairJson(partialJson));
|
|
118
|
+
return (result ?? {}) as T;
|
|
119
|
+
} catch {
|
|
120
|
+
return {} as T;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Api,
|
|
3
|
+
type AssistantMessageEventStream as HostAssistantMessageEventStream,
|
|
3
4
|
type AssistantMessage,
|
|
4
|
-
type AssistantMessageEventStream,
|
|
5
|
-
createAssistantMessageEventStream,
|
|
6
|
-
type Context,
|
|
7
5
|
type ImageContent,
|
|
8
6
|
type Message,
|
|
9
7
|
type Model,
|
|
10
|
-
parseStreamingJson,
|
|
11
8
|
type SimpleStreamOptions,
|
|
12
9
|
type StopReason,
|
|
13
10
|
type TextContent,
|
|
@@ -15,7 +12,11 @@ import {
|
|
|
15
12
|
type Tool,
|
|
16
13
|
type ToolCall,
|
|
17
14
|
type ToolResultMessage,
|
|
15
|
+
type TranscriptContext,
|
|
18
16
|
} from "@earendil-works/pi-ai";
|
|
17
|
+
import { type AssistantMessageEventStream, createAssistantMessageEventStream } from "./eventStream.ts";
|
|
18
|
+
import { hostSystemPrompt, hostTools } from "./hostContext.ts";
|
|
19
|
+
import { parseStreamingJson } from "./jsonParse.ts";
|
|
19
20
|
import { messages as sdkMessages, native } from "@valni/switchboard";
|
|
20
21
|
|
|
21
22
|
const SSE_DATA_PREFIX = "data:";
|
|
@@ -111,6 +112,7 @@ export function normalizeContextMessages(model: Model<KnownApi>, contextMessages
|
|
|
111
112
|
|
|
112
113
|
const transformed: Message[] = [];
|
|
113
114
|
for (const message of imageAware) {
|
|
115
|
+
if (message.role === "system") continue;
|
|
114
116
|
if (message.role === "user") {
|
|
115
117
|
transformed.push(message);
|
|
116
118
|
continue;
|
|
@@ -246,14 +248,16 @@ function chatUserParts(content: (TextContent | ImageContent)[]): sdkMessages.Par
|
|
|
246
248
|
|
|
247
249
|
function buildOpenAIChatRequest(
|
|
248
250
|
model: Model<KnownApi>,
|
|
249
|
-
context:
|
|
251
|
+
context: TranscriptContext,
|
|
250
252
|
options: SimpleStreamOptions | undefined,
|
|
251
253
|
): native.OpenAIChatRequest {
|
|
252
254
|
const normalized = normalizeContextMessages(model, context.messages);
|
|
253
255
|
const requestMessages: sdkMessages.Message[] = [];
|
|
256
|
+
const systemPrompt = hostSystemPrompt(context);
|
|
257
|
+
const tools = hostTools(context);
|
|
254
258
|
|
|
255
|
-
if (
|
|
256
|
-
requestMessages.push(sdkMessages.Message.system(sanitizeText(
|
|
259
|
+
if (systemPrompt) {
|
|
260
|
+
requestMessages.push(sdkMessages.Message.system(sanitizeText(systemPrompt)));
|
|
257
261
|
}
|
|
258
262
|
|
|
259
263
|
for (let index = 0; index < normalized.length; index++) {
|
|
@@ -320,7 +324,7 @@ function buildOpenAIChatRequest(
|
|
|
320
324
|
messages: requestMessages,
|
|
321
325
|
max_tokens: resolveMaxTokens(model, options),
|
|
322
326
|
temperature: options?.temperature ?? null,
|
|
323
|
-
tools:
|
|
327
|
+
tools: tools.length > 0 ? tools.map((tool) => new JsonSchemaTool(tool)) : null,
|
|
324
328
|
reasoning_effort: reasoningLevel ?? null,
|
|
325
329
|
});
|
|
326
330
|
}
|
|
@@ -520,9 +524,9 @@ function completionsUrl(baseUrl: string): string {
|
|
|
520
524
|
|
|
521
525
|
export function streamLocalModel(
|
|
522
526
|
model: Model<Api>,
|
|
523
|
-
context:
|
|
527
|
+
context: TranscriptContext,
|
|
524
528
|
options?: SimpleStreamOptions,
|
|
525
|
-
):
|
|
529
|
+
): HostAssistantMessageEventStream {
|
|
526
530
|
const stream = createAssistantMessageEventStream();
|
|
527
531
|
|
|
528
532
|
(async () => {
|
|
@@ -579,7 +583,7 @@ export function streamLocalModel(
|
|
|
579
583
|
}
|
|
580
584
|
|
|
581
585
|
if (options?.signal?.aborted) throw new Error("Request was aborted");
|
|
582
|
-
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
586
|
+
if (output.stopReason === "aborted" || output.stopReason === "error" || output.stopReason === "pending") {
|
|
583
587
|
throw new Error(output.errorMessage || "An unknown error occurred");
|
|
584
588
|
}
|
|
585
589
|
if (!state.sawTerminalEvent) throw new Error("The stream ended before a terminal event");
|
|
@@ -598,5 +602,9 @@ export function streamLocalModel(
|
|
|
598
602
|
}
|
|
599
603
|
})();
|
|
600
604
|
|
|
601
|
-
|
|
605
|
+
// The host's stream type has private members, so only its own class satisfies
|
|
606
|
+
// it nominally. The object here is the same shape and the host only iterates
|
|
607
|
+
// it, so the cast sits at this one boundary rather than importing the host's
|
|
608
|
+
// constructor and naming its package at runtime.
|
|
609
|
+
return stream as unknown as HostAssistantMessageEventStream;
|
|
602
610
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Promplate Dev Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Partial JSON Parser
|
|
2
|
+
|
|
3
|
+
Sometimes we need **LLM (Large Language Models)** to produce **structural information** instead of natural language. The easiest way is to use JSON.
|
|
4
|
+
|
|
5
|
+
But before receiving the last token of response, the JSON is broken, which means you can't use `JSON.parse` to decode it. But we still want to stream the data to the user.
|
|
6
|
+
|
|
7
|
+
Here comes `partial-json`, a lightweight and customizable library for parsing partial JSON strings. Here is a [demo](https://promplate.dev/partial-json-parser).
|
|
8
|
+
|
|
9
|
+
(Note that there is [a Python implementation](https://github.com/promplate/partial-json-parser) too)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm i partial-json # or pnpm / bun / yarn
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`partial-json` is implemented purely in JavaScript, and have both `commonjs` and `esm` builds.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Importing the library
|
|
22
|
+
|
|
23
|
+
You can import the `parse` function and the `Allow` object from the library like this:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { parse, Allow } from "partial-json";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The `Allow` object is just an Enum for options. It determines what types can be partial. types not included in `allow` only appears after its completion can be ensured.
|
|
30
|
+
|
|
31
|
+
### Parsing complete / partial JSON strings
|
|
32
|
+
|
|
33
|
+
The `parse` function works just like the built-in `JSON.parse` when parsing a complete JSON string:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
let result = parse('{"key":"value"}');
|
|
37
|
+
console.log(result); // Outputs: { key: 'value' }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
You can parse a partial JSON string by passing an additional parameter to the `parse` function. This parameter is a **bitwise OR** of the constants from the `Allow` object:
|
|
41
|
+
|
|
42
|
+
(Note that you can directly import the constants you need from `partial-json`)
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { parse, STR, OBJ } from "partial-json";
|
|
46
|
+
|
|
47
|
+
result = parse('{"key": "v', STR | OBJ);
|
|
48
|
+
console.log(result); // Outputs: { key: 'v' }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
In this example, `Allow.STR` tells the parser that it's okay if a string is incomplete, and `Allow.OBJ` tells the parser so as an object. The parser then try to return as much data as it can.
|
|
52
|
+
|
|
53
|
+
If you don't allow partial strings, then it will not add `"key"` to the object because `"v` is not close:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
result = parse('{"key": "v', OBJ);
|
|
57
|
+
console.log(result); // Outputs: {}
|
|
58
|
+
|
|
59
|
+
result = parse('{"key": "value"', OBJ);
|
|
60
|
+
console.log(result); // Outputs: { key: 'value' }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Similarity, you can parse partial arrays or even partial special values if you allow it:
|
|
64
|
+
|
|
65
|
+
(Note that `allow` defaults to `Allow.ALL`)
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
result = parse('[ {"key1": "value1", "key2": [ "value2');
|
|
69
|
+
console.log(result); // Outputs: [ { key1: 'value1', key2: [ 'value2' ] } ]
|
|
70
|
+
|
|
71
|
+
result = parse("-Inf");
|
|
72
|
+
console.log(result); // Outputs: -Infinity
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Handling malformed JSON
|
|
76
|
+
|
|
77
|
+
If the JSON string is malformed, the `parse` function will throw an error:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
parse("wrong"); // MalformedJSON [Error]: SyntaxError: Unexpected token 'w', "wrong" is not valid JSON at position 0
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## API Reference
|
|
84
|
+
|
|
85
|
+
### parse(jsonString, [allowPartial])
|
|
86
|
+
|
|
87
|
+
- `jsonString` `<string>`: The JSON string to parse.
|
|
88
|
+
- `allowPartial` `<number>`: Specify what kind of partialness is allowed during JSON parsing (default: `Allow.ALL`).
|
|
89
|
+
|
|
90
|
+
Returns the parsed JavaScript value.
|
|
91
|
+
|
|
92
|
+
### Allow
|
|
93
|
+
|
|
94
|
+
An object that specifies what kind of partialness is allowed during JSON parsing. It has the following properties:
|
|
95
|
+
|
|
96
|
+
- `STR`: Allow partial string.
|
|
97
|
+
- `NUM`: Allow partial number.
|
|
98
|
+
- `ARR`: Allow partial array.
|
|
99
|
+
- `OBJ`: Allow partial object.
|
|
100
|
+
- `NULL`: Allow partial null.
|
|
101
|
+
- `BOOL`: Allow partial boolean.
|
|
102
|
+
- `NAN`: Allow partial NaN.
|
|
103
|
+
- `INFINITY`: Allow partial Infinity.
|
|
104
|
+
- `_INFINITY`: Allow partial -Infinity.
|
|
105
|
+
- `INF`: Allow both partial Infinity and -Infinity.
|
|
106
|
+
- `SPECIAL`: Allow all special values.
|
|
107
|
+
- `ATOM`: Allow all atomic values.
|
|
108
|
+
- `COLLECTION`: Allow all collection values.
|
|
109
|
+
- `ALL`: Allow all values.
|
|
110
|
+
|
|
111
|
+
## Testing
|
|
112
|
+
|
|
113
|
+
To run the tests for this library, you should clone the repository and install the dependencies:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
git clone https://github.com/promplate/partial-json-parser-js.git
|
|
117
|
+
cd partial-json-parser-js
|
|
118
|
+
npm i
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Then, you can run the tests using [Vitest](https://vitest.dev/):
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
npm run test
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Please note that while we strive to cover as many edge cases as possible, it's always possible that some cases might not be covered.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.Allow = exports.MalformedJSON = exports.PartialJSON = exports.parseJSON = exports.parse = void 0;
|
|
18
|
+
const options_1 = require("./options");
|
|
19
|
+
Object.defineProperty(exports, "Allow", { enumerable: true, get: function () { return options_1.Allow; } });
|
|
20
|
+
__exportStar(require("./options"), exports);
|
|
21
|
+
class PartialJSON extends Error {
|
|
22
|
+
}
|
|
23
|
+
exports.PartialJSON = PartialJSON;
|
|
24
|
+
class MalformedJSON extends Error {
|
|
25
|
+
}
|
|
26
|
+
exports.MalformedJSON = MalformedJSON;
|
|
27
|
+
/**
|
|
28
|
+
* Parse incomplete JSON
|
|
29
|
+
* @param {string} jsonString Partial JSON to be parsed
|
|
30
|
+
* @param {number} allowPartial Specify what types are allowed to be partial, see {@link Allow} for details
|
|
31
|
+
* @returns The parsed JSON
|
|
32
|
+
* @throws {PartialJSON} If the JSON is incomplete (related to the `allow` parameter)
|
|
33
|
+
* @throws {MalformedJSON} If the JSON is malformed
|
|
34
|
+
*/
|
|
35
|
+
function parseJSON(jsonString, allowPartial = options_1.Allow.ALL) {
|
|
36
|
+
if (typeof jsonString !== "string") {
|
|
37
|
+
throw new TypeError(`expecting str, got ${typeof jsonString}`);
|
|
38
|
+
}
|
|
39
|
+
if (!jsonString.trim()) {
|
|
40
|
+
throw new Error(`${jsonString} is empty`);
|
|
41
|
+
}
|
|
42
|
+
return _parseJSON(jsonString.trim(), allowPartial);
|
|
43
|
+
}
|
|
44
|
+
exports.parseJSON = parseJSON;
|
|
45
|
+
;
|
|
46
|
+
const _parseJSON = (jsonString, allow) => {
|
|
47
|
+
const length = jsonString.length;
|
|
48
|
+
let index = 0;
|
|
49
|
+
const markPartialJSON = (msg) => {
|
|
50
|
+
throw new PartialJSON(`${msg} at position ${index}`);
|
|
51
|
+
};
|
|
52
|
+
const throwMalformedError = (msg) => {
|
|
53
|
+
throw new MalformedJSON(`${msg} at position ${index}`);
|
|
54
|
+
};
|
|
55
|
+
const parseAny = () => {
|
|
56
|
+
skipBlank();
|
|
57
|
+
if (index >= length)
|
|
58
|
+
markPartialJSON("Unexpected end of input");
|
|
59
|
+
if (jsonString[index] === '"')
|
|
60
|
+
return parseStr();
|
|
61
|
+
if (jsonString[index] === "{")
|
|
62
|
+
return parseObj();
|
|
63
|
+
if (jsonString[index] === "[")
|
|
64
|
+
return parseArr();
|
|
65
|
+
if (jsonString.substring(index, index + 4) === "null" || (options_1.Allow.NULL & allow && length - index < 4 && "null".startsWith(jsonString.substring(index)))) {
|
|
66
|
+
index += 4;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (jsonString.substring(index, index + 4) === "true" || (options_1.Allow.BOOL & allow && length - index < 4 && "true".startsWith(jsonString.substring(index)))) {
|
|
70
|
+
index += 4;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (jsonString.substring(index, index + 5) === "false" || (options_1.Allow.BOOL & allow && length - index < 5 && "false".startsWith(jsonString.substring(index)))) {
|
|
74
|
+
index += 5;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (jsonString.substring(index, index + 8) === "Infinity" || (options_1.Allow.INFINITY & allow && length - index < 8 && "Infinity".startsWith(jsonString.substring(index)))) {
|
|
78
|
+
index += 8;
|
|
79
|
+
return Infinity;
|
|
80
|
+
}
|
|
81
|
+
if (jsonString.substring(index, index + 9) === "-Infinity" || (options_1.Allow._INFINITY & allow && 1 < length - index && length - index < 9 && "-Infinity".startsWith(jsonString.substring(index)))) {
|
|
82
|
+
index += 9;
|
|
83
|
+
return -Infinity;
|
|
84
|
+
}
|
|
85
|
+
if (jsonString.substring(index, index + 3) === "NaN" || (options_1.Allow.NAN & allow && length - index < 3 && "NaN".startsWith(jsonString.substring(index)))) {
|
|
86
|
+
index += 3;
|
|
87
|
+
return NaN;
|
|
88
|
+
}
|
|
89
|
+
return parseNum();
|
|
90
|
+
};
|
|
91
|
+
const parseStr = () => {
|
|
92
|
+
const start = index;
|
|
93
|
+
let escape = false;
|
|
94
|
+
index++; // skip initial quote
|
|
95
|
+
while (index < length && (jsonString[index] !== '"' || (escape && jsonString[index - 1] === "\\"))) {
|
|
96
|
+
escape = jsonString[index] === "\\" ? !escape : false;
|
|
97
|
+
index++;
|
|
98
|
+
}
|
|
99
|
+
if (jsonString.charAt(index) == '"') {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(jsonString.substring(start, ++index - Number(escape)));
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
throwMalformedError(String(e));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (options_1.Allow.STR & allow) {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(jsonString.substring(start, index - Number(escape)) + '"');
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
// SyntaxError: Invalid escape sequence
|
|
113
|
+
return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("\\")) + '"');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
markPartialJSON("Unterminated string literal");
|
|
117
|
+
};
|
|
118
|
+
const parseObj = () => {
|
|
119
|
+
index++; // skip initial brace
|
|
120
|
+
skipBlank();
|
|
121
|
+
const obj = {};
|
|
122
|
+
try {
|
|
123
|
+
while (jsonString[index] !== "}") {
|
|
124
|
+
skipBlank();
|
|
125
|
+
if (index >= length && options_1.Allow.OBJ & allow)
|
|
126
|
+
return obj;
|
|
127
|
+
const key = parseStr();
|
|
128
|
+
skipBlank();
|
|
129
|
+
index++; // skip colon
|
|
130
|
+
try {
|
|
131
|
+
const value = parseAny();
|
|
132
|
+
obj[key] = value;
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
if (options_1.Allow.OBJ & allow)
|
|
136
|
+
return obj;
|
|
137
|
+
else
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
140
|
+
skipBlank();
|
|
141
|
+
if (jsonString[index] === ",")
|
|
142
|
+
index++; // skip comma
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
if (options_1.Allow.OBJ & allow)
|
|
147
|
+
return obj;
|
|
148
|
+
else
|
|
149
|
+
markPartialJSON("Expected '}' at end of object");
|
|
150
|
+
}
|
|
151
|
+
index++; // skip final brace
|
|
152
|
+
return obj;
|
|
153
|
+
};
|
|
154
|
+
const parseArr = () => {
|
|
155
|
+
index++; // skip initial bracket
|
|
156
|
+
const arr = [];
|
|
157
|
+
try {
|
|
158
|
+
while (jsonString[index] !== "]") {
|
|
159
|
+
arr.push(parseAny());
|
|
160
|
+
skipBlank();
|
|
161
|
+
if (jsonString[index] === ",") {
|
|
162
|
+
index++; // skip comma
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
if (options_1.Allow.ARR & allow) {
|
|
168
|
+
return arr;
|
|
169
|
+
}
|
|
170
|
+
markPartialJSON("Expected ']' at end of array");
|
|
171
|
+
}
|
|
172
|
+
index++; // skip final bracket
|
|
173
|
+
return arr;
|
|
174
|
+
};
|
|
175
|
+
const parseNum = () => {
|
|
176
|
+
if (index === 0) {
|
|
177
|
+
if (jsonString === "-")
|
|
178
|
+
throwMalformedError("Not sure what '-' is");
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(jsonString);
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
if (options_1.Allow.NUM & allow)
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(jsonString.substring(0, jsonString.lastIndexOf("e")));
|
|
186
|
+
}
|
|
187
|
+
catch (e) { }
|
|
188
|
+
throwMalformedError(String(e));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const start = index;
|
|
192
|
+
if (jsonString[index] === "-")
|
|
193
|
+
index++;
|
|
194
|
+
while (jsonString[index] && ",]}".indexOf(jsonString[index]) === -1)
|
|
195
|
+
index++;
|
|
196
|
+
if (index == length && !(options_1.Allow.NUM & allow))
|
|
197
|
+
markPartialJSON("Unterminated number literal");
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(jsonString.substring(start, index));
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
if (jsonString.substring(start, index) === "-")
|
|
203
|
+
markPartialJSON("Not sure what '-' is");
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("e")));
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
throwMalformedError(String(e));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const skipBlank = () => {
|
|
213
|
+
while (index < length && " \n\r\t".includes(jsonString[index])) {
|
|
214
|
+
index++;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
return parseAny();
|
|
218
|
+
};
|
|
219
|
+
const parse = parseJSON;
|
|
220
|
+
exports.parse = parse;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sometimes you don't allow every type to be partially parsed.
|
|
4
|
+
* For example, you may not want a partial number because it may increase its size gradually before it's complete.
|
|
5
|
+
* In this case, you can use the `Allow` object to control what types you allow to be partially parsed.
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.Allow = exports.ALL = exports.COLLECTION = exports.ATOM = exports.SPECIAL = exports.INF = exports._INFINITY = exports.INFINITY = exports.NAN = exports.BOOL = exports.NULL = exports.OBJ = exports.ARR = exports.NUM = exports.STR = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* allow partial strings like `"hello \u12` to be parsed as `"hello "`
|
|
12
|
+
*/
|
|
13
|
+
exports.STR = 0b000000001;
|
|
14
|
+
/**
|
|
15
|
+
* allow partial numbers like `123.` to be parsed as `123`
|
|
16
|
+
*/
|
|
17
|
+
exports.NUM = 0b000000010;
|
|
18
|
+
/**
|
|
19
|
+
* allow partial arrays like `[1, 2,` to be parsed as `[1, 2]`
|
|
20
|
+
*/
|
|
21
|
+
exports.ARR = 0b000000100;
|
|
22
|
+
/**
|
|
23
|
+
* allow partial objects like `{"a": 1, "b":` to be parsed as `{"a": 1}`
|
|
24
|
+
*/
|
|
25
|
+
exports.OBJ = 0b000001000;
|
|
26
|
+
/**
|
|
27
|
+
* allow `nu` to be parsed as `null`
|
|
28
|
+
*/
|
|
29
|
+
exports.NULL = 0b000010000;
|
|
30
|
+
/**
|
|
31
|
+
* allow `tr` to be parsed as `true`, and `fa` to be parsed as `false`
|
|
32
|
+
*/
|
|
33
|
+
exports.BOOL = 0b000100000;
|
|
34
|
+
/**
|
|
35
|
+
* allow `Na` to be parsed as `NaN`
|
|
36
|
+
*/
|
|
37
|
+
exports.NAN = 0b001000000;
|
|
38
|
+
/**
|
|
39
|
+
* allow `Inf` to be parsed as `Infinity`
|
|
40
|
+
*/
|
|
41
|
+
exports.INFINITY = 0b010000000;
|
|
42
|
+
/**
|
|
43
|
+
* allow `-Inf` to be parsed as `-Infinity`
|
|
44
|
+
*/
|
|
45
|
+
exports._INFINITY = 0b100000000;
|
|
46
|
+
exports.INF = exports.INFINITY | exports._INFINITY;
|
|
47
|
+
exports.SPECIAL = exports.NULL | exports.BOOL | exports.INF | exports.NAN;
|
|
48
|
+
exports.ATOM = exports.STR | exports.NUM | exports.SPECIAL;
|
|
49
|
+
exports.COLLECTION = exports.ARR | exports.OBJ;
|
|
50
|
+
exports.ALL = exports.ATOM | exports.COLLECTION;
|
|
51
|
+
/**
|
|
52
|
+
* Control what types you allow to be partially parsed.
|
|
53
|
+
* The default is to allow all types to be partially parsed, which in most casees is the best option.
|
|
54
|
+
* @example
|
|
55
|
+
* If you don't want to allow partial objects, you can use the following code:
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { Allow, parse } from "partial-json";
|
|
58
|
+
* parse(`[{"a": 1, "b": 2}, {"a": 3,`, Allow.ARR); // [ { a: 1, b: 2 } ]
|
|
59
|
+
* ```
|
|
60
|
+
* Or you can use `~` to disallow a type:
|
|
61
|
+
* ```ts
|
|
62
|
+
* parse(`[{"a": 1, "b": 2}, {"a": 3,`, ~Allow.OBJ); // [ { a: 1, b: 2 } ]
|
|
63
|
+
* ```
|
|
64
|
+
* @example
|
|
65
|
+
* If you don't want to allow partial strings, you can use the following code:
|
|
66
|
+
* ```ts
|
|
67
|
+
* import { Allow, parse } from "partial-json";
|
|
68
|
+
* parse(`["complete string", "incompl`, ~Allow.STR); // [ 'complete string' ]
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
exports.Allow = { STR: exports.STR, NUM: exports.NUM, ARR: exports.ARR, OBJ: exports.OBJ, NULL: exports.NULL, BOOL: exports.BOOL, NAN: exports.NAN, INFINITY: exports.INFINITY, _INFINITY: exports._INFINITY, INF: exports.INF, SPECIAL: exports.SPECIAL, ATOM: exports.ATOM, COLLECTION: exports.COLLECTION, ALL: exports.ALL };
|
|
72
|
+
exports.default = exports.Allow;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "partial-json",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "Parse partial JSON generated by LLM",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"JSON",
|
|
7
|
+
"parser",
|
|
8
|
+
"LLM",
|
|
9
|
+
"nlp"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://promplate.dev/partial-json-parser",
|
|
12
|
+
"repository": "https://github.com/promplate/partial-json-parser-js",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Muspi Merol <me@promplate.dev>",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./options": {
|
|
21
|
+
"types": "./dist/options.d.ts",
|
|
22
|
+
"default": "./dist/options.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"main": "dist/index.js",
|
|
26
|
+
"module": "dist/index.js",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"coverage": "vitest run --coverage",
|
|
34
|
+
"test": "vitest",
|
|
35
|
+
"test:ui": "vitest --ui"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@vitest/coverage-istanbul": "^1.2.2",
|
|
39
|
+
"@vitest/ui": "^1.2.2",
|
|
40
|
+
"typescript": "^5.3.3",
|
|
41
|
+
"vitest": "^1.2.2"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard-local",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local models in the pi model picker, managed end to end",
|
|
6
6
|
"keywords": [
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
]
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@valni/switchboard": "git+https://github.com/Valni-Labs/switchboard-ts.git#d132540a1fd86491ffabb9327ec38bb903f709a3"
|
|
27
|
+
"@valni/switchboard": "git+https://github.com/Valni-Labs/switchboard-ts.git#d132540a1fd86491ffabb9327ec38bb903f709a3",
|
|
28
|
+
"partial-json": "0.1.7"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@earendil-works/pi-ai": "0.
|
|
31
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
31
|
+
"@earendil-works/pi-ai": ">=1.0.0",
|
|
32
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
32
33
|
"typescript": "5.9.3"
|
|
33
34
|
}
|
|
34
35
|
}
|
|
Binary file
|
package/runtime/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valni/cli",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.7",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"valniConfig": {
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@valni/agent-core": "^0.1.
|
|
42
|
-
"@valni/ai": "^0.1.
|
|
43
|
-
"@valni/tui": "^0.1.
|
|
44
|
-
"pi-switchboard": "git+https://github.com/Valni-Labs/pi-switchboard.git#
|
|
45
|
-
"switchboard-local": "git+https://github.com/Valni-Labs/switchboard-local.git#
|
|
41
|
+
"@valni/agent-core": "^0.1.7",
|
|
42
|
+
"@valni/ai": "^0.1.7",
|
|
43
|
+
"@valni/tui": "^0.1.7",
|
|
44
|
+
"pi-switchboard": "git+https://github.com/Valni-Labs/pi-switchboard.git#774f1a7985a755fcd483313b729864aa8a9418c3",
|
|
45
|
+
"switchboard-local": "git+https://github.com/Valni-Labs/switchboard-local.git#4ec07263591a4f329fd12eab04d62d6c455be666",
|
|
46
46
|
"@silvia-odwyer/photon-node": "0.3.4",
|
|
47
47
|
"chalk": "5.6.2",
|
|
48
48
|
"cross-spawn": "7.0.6",
|
package/runtime/valni
CHANGED