@valni/cli-darwin-x64 0.1.5 → 0.1.6
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/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
|
}
|
|
@@ -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.6",
|
|
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.6",
|
|
42
|
+
"@valni/ai": "^0.1.6",
|
|
43
|
+
"@valni/tui": "^0.1.6",
|
|
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