@averyyy/pi-server 0.99.1-piclient.1 → 1.0.0-piclient.2
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/CHANGELOG.md +7 -0
- package/README.md +5 -1
- package/dist/pi-server-protocol.d.ts +12 -1
- package/dist/pi-server-protocol.d.ts.map +1 -1
- package/dist/pi-server-protocol.js +1 -0
- package/dist/pi-server-protocol.js.map +1 -1
- package/dist/request-chunks.d.ts.map +1 -1
- package/dist/request-chunks.js +4 -0
- package/dist/request-chunks.js.map +1 -1
- package/dist/server.d.ts +9 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +367 -29
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/dist/server.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { createServer } from "node:http";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { compactLegacy, convertToLlm, DEFAULT_COMPACTION_SETTINGS, prepareLegacyCompaction, } from "@earendil-works/pi-agent-core";
|
|
4
5
|
import { createModels, createProvider, } from "@earendil-works/pi-ai";
|
|
5
|
-
import {
|
|
6
|
+
import { cloudflareWorkersAISystemOneApi } from "@earendil-works/pi-ai/api/cloudflare-workers-ai-system-one.lazy";
|
|
7
|
+
import { llamaCppClassifyApi } from "@earendil-works/pi-ai/api/llama-cpp-classify.lazy";
|
|
8
|
+
import { typesafeSystemOneApi } from "@earendil-works/pi-ai/api/typesafe-system-one.lazy";
|
|
9
|
+
import { getBuiltinProviderForModel, getEnvApiKey, getImagesApiProvider, stream as streamApi, streamSimple, } from "@earendil-works/pi-ai/compat";
|
|
10
|
+
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
|
|
11
|
+
import { getProviderEnvValue } from "@earendil-works/pi-ai/utils/provider-env";
|
|
6
12
|
import { loadConfig } from "./config.js";
|
|
7
13
|
import { PiServerError, PiServerErrorCode } from "./error-codes.js";
|
|
8
14
|
import { encodeErrorEvent, encodeProxyEvent } from "./event-encoding.js";
|
|
@@ -16,22 +22,50 @@ const packageMetadata = createRequire(import.meta.url)("../package.json");
|
|
|
16
22
|
const PI_SERVER_VERSION = packageMetadata.version;
|
|
17
23
|
function createRequestModels(model, options) {
|
|
18
24
|
const models = createModels();
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
const builtin = getBuiltinProviderForModel(model);
|
|
26
|
+
const requestApiStream = (requestModel, context, streamOptions) => builtin
|
|
27
|
+
? builtin.stream(requestModel, context, streamOptions)
|
|
28
|
+
: streamApi(requestModel, context, streamOptions);
|
|
29
|
+
const requestStream = (requestModel, context, streamOptions) => {
|
|
30
|
+
const requestOptions = {
|
|
31
|
+
...streamOptions,
|
|
32
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
33
|
+
};
|
|
34
|
+
return builtin
|
|
35
|
+
? builtin.streamSimple(requestModel, context, requestOptions)
|
|
36
|
+
: streamSimple(requestModel, context, requestOptions);
|
|
37
|
+
};
|
|
23
38
|
models.setProvider(createProvider({
|
|
24
39
|
id: model.provider,
|
|
25
40
|
name: model.provider,
|
|
26
41
|
models: [model],
|
|
27
|
-
auth:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
auth: builtin
|
|
43
|
+
? {
|
|
44
|
+
...builtin.auth,
|
|
45
|
+
apiKey: {
|
|
46
|
+
name: builtin.auth.apiKey?.name ?? "pi-server request auth",
|
|
47
|
+
resolve: async (request) => {
|
|
48
|
+
const resolved = await builtin.auth.apiKey?.resolve(request);
|
|
49
|
+
if (resolved)
|
|
50
|
+
return resolved;
|
|
51
|
+
// Declared request credentials are validated by the native provider.
|
|
52
|
+
if (options.apiKey !== undefined ||
|
|
53
|
+
options.headers !== undefined ||
|
|
54
|
+
model.headers !== undefined) {
|
|
55
|
+
return { auth: { apiKey: options.apiKey, headers: options.headers }, env: options.env };
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
: {
|
|
62
|
+
apiKey: {
|
|
63
|
+
name: "pi-server request auth",
|
|
64
|
+
resolve: async () => ({ auth: { apiKey: options.apiKey, headers: options.headers } }),
|
|
65
|
+
},
|
|
31
66
|
},
|
|
32
|
-
},
|
|
33
67
|
api: {
|
|
34
|
-
stream:
|
|
68
|
+
stream: requestApiStream,
|
|
35
69
|
streamSimple: requestStream,
|
|
36
70
|
},
|
|
37
71
|
}));
|
|
@@ -102,6 +136,8 @@ function createRunRecord(sessionId, runId, kind) {
|
|
|
102
136
|
resolveSettled = resolve;
|
|
103
137
|
});
|
|
104
138
|
return {
|
|
139
|
+
callbacks: new Map(),
|
|
140
|
+
callbackConnectionClosed: false,
|
|
105
141
|
sessionId,
|
|
106
142
|
runId,
|
|
107
143
|
kind,
|
|
@@ -117,6 +153,7 @@ function createRunRecord(sessionId, runId, kind) {
|
|
|
117
153
|
function settleRun(run) {
|
|
118
154
|
if (!run)
|
|
119
155
|
return;
|
|
156
|
+
rejectProviderCallbacks(run, new Error("Provider run settled before callback reply"));
|
|
120
157
|
const resolve = run.resolveSettled;
|
|
121
158
|
run.resolveSettled = undefined;
|
|
122
159
|
resolve?.();
|
|
@@ -127,7 +164,9 @@ function startStreamRun(sessionId, runId, kind = "stream") {
|
|
|
127
164
|
return existing;
|
|
128
165
|
const run = existing ?? createRunRecord(sessionId, runId, kind);
|
|
129
166
|
if (existing) {
|
|
167
|
+
run.callbackConnectionClosed = false;
|
|
130
168
|
run.message = undefined;
|
|
169
|
+
run.cacheRetention = undefined;
|
|
131
170
|
run.errorMessage = undefined;
|
|
132
171
|
run.cancelRequested = false;
|
|
133
172
|
run.controller = new AbortController();
|
|
@@ -151,7 +190,9 @@ function streamRunResponseBody(run) {
|
|
|
151
190
|
createdAt: run.createdAt,
|
|
152
191
|
updatedAt: run.updatedAt,
|
|
153
192
|
...(run.message ? { message: run.message } : {}),
|
|
193
|
+
...(run.cacheRetention !== undefined ? { cacheRetention: run.cacheRetention } : {}),
|
|
154
194
|
...(run.compactResult ? { compactResult: run.compactResult } : {}),
|
|
195
|
+
...(run.operationResult ? { operationResult: run.operationResult } : {}),
|
|
155
196
|
...(run.errorMessage ? { errorMessage: run.errorMessage } : {}),
|
|
156
197
|
};
|
|
157
198
|
}
|
|
@@ -204,7 +245,7 @@ function completeStreamRun(run, message) {
|
|
|
204
245
|
run.errorMessage = undefined;
|
|
205
246
|
run.updatedAt = Date.now();
|
|
206
247
|
}
|
|
207
|
-
function failStreamRun(run, errorMessage) {
|
|
248
|
+
function failStreamRun(run, errorMessage, message) {
|
|
208
249
|
if (!run)
|
|
209
250
|
return;
|
|
210
251
|
if (run.cancelRequested) {
|
|
@@ -214,6 +255,7 @@ function failStreamRun(run, errorMessage) {
|
|
|
214
255
|
return;
|
|
215
256
|
}
|
|
216
257
|
run.status = "failed";
|
|
258
|
+
run.message = message;
|
|
217
259
|
run.errorMessage = errorMessage;
|
|
218
260
|
run.updatedAt = Date.now();
|
|
219
261
|
}
|
|
@@ -221,6 +263,94 @@ function touchStreamRun(run) {
|
|
|
221
263
|
if (run)
|
|
222
264
|
run.updatedAt = Date.now();
|
|
223
265
|
}
|
|
266
|
+
function rejectProviderCallbacks(run, error) {
|
|
267
|
+
const pending = [...run.callbacks.values()];
|
|
268
|
+
run.callbacks.clear();
|
|
269
|
+
for (const callback of pending)
|
|
270
|
+
callback.reject(error);
|
|
271
|
+
}
|
|
272
|
+
function createProviderCallbacks(run, res, callbacks) {
|
|
273
|
+
if (!callbacks?.onPayload && !callbacks?.onResponse)
|
|
274
|
+
return {};
|
|
275
|
+
res.once("close", () => {
|
|
276
|
+
run.callbackConnectionClosed = true;
|
|
277
|
+
rejectProviderCallbacks(run, new Error("Provider callback connection closed"));
|
|
278
|
+
});
|
|
279
|
+
run.controller.signal.addEventListener("abort", () => rejectProviderCallbacks(run, new DOMException("Provider run aborted", "AbortError")), { once: true });
|
|
280
|
+
const requestCallback = (request) => {
|
|
281
|
+
if (run.controller.signal.aborted)
|
|
282
|
+
return Promise.reject(new DOMException("Provider run aborted", "AbortError"));
|
|
283
|
+
if (run.callbackConnectionClosed || res.destroyed || res.writableEnded)
|
|
284
|
+
return Promise.reject(new Error("Provider callback connection closed"));
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
run.callbacks.set(request.callbackId, { resolve, reject });
|
|
287
|
+
try {
|
|
288
|
+
writeServerSentEvent(res, "provider_callback", request);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
run.callbacks.delete(request.callbackId);
|
|
292
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
return {
|
|
297
|
+
...(callbacks.onPayload
|
|
298
|
+
? {
|
|
299
|
+
onPayload: (payload, model) => requestCallback({
|
|
300
|
+
callbackId: randomUUID(),
|
|
301
|
+
kind: "payload",
|
|
302
|
+
model: { ...model, headers: undefined },
|
|
303
|
+
payload,
|
|
304
|
+
}),
|
|
305
|
+
}
|
|
306
|
+
: {}),
|
|
307
|
+
...(callbacks.onResponse
|
|
308
|
+
? {
|
|
309
|
+
onResponse: async (response, model) => {
|
|
310
|
+
await requestCallback({
|
|
311
|
+
callbackId: randomUUID(),
|
|
312
|
+
kind: "response",
|
|
313
|
+
model: { ...model, headers: undefined },
|
|
314
|
+
response,
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
}
|
|
318
|
+
: {}),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function handleProviderCallbackReply(body, res) {
|
|
322
|
+
if (!body || typeof body !== "object") {
|
|
323
|
+
sendError(res, 400, "Provider callback reply must be an object", PiServerErrorCode.INVALID_REQUEST);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const reply = body;
|
|
327
|
+
if (typeof reply.sessionId !== "string" || typeof reply.runId !== "string" || typeof reply.callbackId !== "string") {
|
|
328
|
+
sendError(res, 400, "sessionId, runId and callbackId are required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const run = getStreamRun(reply.sessionId, reply.runId);
|
|
332
|
+
const pending = run?.callbacks.get(reply.callbackId);
|
|
333
|
+
if (!run || run.status !== "running" || !pending) {
|
|
334
|
+
sendError(res, 409, "Provider callback is no longer pending", PiServerErrorCode.INVALID_REQUEST);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
let error;
|
|
338
|
+
if (reply.error !== undefined) {
|
|
339
|
+
const remoteError = reply.error;
|
|
340
|
+
if (!remoteError || typeof remoteError.name !== "string" || typeof remoteError.message !== "string") {
|
|
341
|
+
sendError(res, 400, "Provider callback error requires string name and message", PiServerErrorCode.INVALID_REQUEST);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
error = new Error(remoteError.message);
|
|
345
|
+
error.name = remoteError.name;
|
|
346
|
+
}
|
|
347
|
+
run.callbacks.delete(reply.callbackId);
|
|
348
|
+
sendJson(res, 200, { accepted: true, callbackId: reply.callbackId, runId: reply.runId });
|
|
349
|
+
if (error)
|
|
350
|
+
pending.reject(error);
|
|
351
|
+
else
|
|
352
|
+
pending.resolve(reply.result);
|
|
353
|
+
}
|
|
224
354
|
function writeStreamEvent(res, event) {
|
|
225
355
|
if (!res.writableEnded && !res.destroyed) {
|
|
226
356
|
res.write(encodeProxyEvent(event));
|
|
@@ -243,9 +373,19 @@ function cleanupExpiredStreamRuns(nowMs) {
|
|
|
243
373
|
}
|
|
244
374
|
}
|
|
245
375
|
}
|
|
246
|
-
function deleteStreamRunsForSession(sessionId) {
|
|
247
|
-
|
|
248
|
-
|
|
376
|
+
async function deleteStreamRunsForSession(sessionId) {
|
|
377
|
+
const runs = [...streamRuns.entries()].filter(([, run]) => run.sessionId === sessionId);
|
|
378
|
+
for (const [, run] of runs) {
|
|
379
|
+
if (run.status !== "running")
|
|
380
|
+
continue;
|
|
381
|
+
run.cancelRequested = true;
|
|
382
|
+
run.updatedAt = Date.now();
|
|
383
|
+
run.controller.abort();
|
|
384
|
+
rejectProviderCallbacks(run, new DOMException("Provider session deleted", "AbortError"));
|
|
385
|
+
}
|
|
386
|
+
await Promise.all(runs.map(([, run]) => run.settled));
|
|
387
|
+
for (const [key, run] of runs) {
|
|
388
|
+
if (streamRuns.get(key) === run)
|
|
249
389
|
streamRuns.delete(key);
|
|
250
390
|
}
|
|
251
391
|
}
|
|
@@ -411,7 +551,7 @@ function handleSessionTreeSwitch(config, body, res) {
|
|
|
411
551
|
}
|
|
412
552
|
}
|
|
413
553
|
}
|
|
414
|
-
function prepareSessionCompact(body) {
|
|
554
|
+
function prepareSessionCompact(body, models) {
|
|
415
555
|
if (!body.sessionId) {
|
|
416
556
|
return { status: 400, body: { error: "sessionId is required" } };
|
|
417
557
|
}
|
|
@@ -491,6 +631,7 @@ function prepareSessionCompact(body) {
|
|
|
491
631
|
}
|
|
492
632
|
}
|
|
493
633
|
return {
|
|
634
|
+
models: models ?? createRequestModels(body.model, options),
|
|
494
635
|
session,
|
|
495
636
|
sessionId: session.sessionId,
|
|
496
637
|
revision: session.revision,
|
|
@@ -522,7 +663,7 @@ function sessionCompactConflict(body, prepared, currentSession) {
|
|
|
522
663
|
};
|
|
523
664
|
}
|
|
524
665
|
async function completeSessionCompact(config, body, prepared, run) {
|
|
525
|
-
const result = await compactLegacy(prepared.preparation,
|
|
666
|
+
const result = await compactLegacy(prepared.preparation, prepared.models, body.model, body.customInstructions, run?.controller.signal, prepared.options.reasoning, undefined, undefined, prepared.cacheContext, prepared.options, prepared.cacheFallbackReason);
|
|
526
667
|
if (!result.ok) {
|
|
527
668
|
return { status: 500, body: { error: result.error.message, code: PiServerErrorCode.INTERNAL_ERROR } };
|
|
528
669
|
}
|
|
@@ -666,7 +807,7 @@ function finishCompactRun(run, result) {
|
|
|
666
807
|
}
|
|
667
808
|
run.updatedAt = Date.now();
|
|
668
809
|
}
|
|
669
|
-
async function handleSessionCompact(config, body, res) {
|
|
810
|
+
async function handleSessionCompact(config, body, res, models) {
|
|
670
811
|
const existingRun = body.runId ? getStreamRun(body.sessionId, body.runId) : undefined;
|
|
671
812
|
if (existingRun?.status === "aborted") {
|
|
672
813
|
sendJson(res, 409, { error: "Compaction run was aborted", code: PiServerErrorCode.INVALID_REQUEST });
|
|
@@ -676,7 +817,7 @@ async function handleSessionCompact(config, body, res) {
|
|
|
676
817
|
sendJson(res, 200, existingRun.compactResult);
|
|
677
818
|
return;
|
|
678
819
|
}
|
|
679
|
-
const prepared = prepareSessionCompact(body);
|
|
820
|
+
const prepared = prepareSessionCompact(body, models);
|
|
680
821
|
if ("status" in prepared) {
|
|
681
822
|
sendJson(res, prepared.status, prepared.body);
|
|
682
823
|
return;
|
|
@@ -749,6 +890,7 @@ async function handleSessionRunAbort(sessionId, runId, res) {
|
|
|
749
890
|
run.cancelRequested = true;
|
|
750
891
|
run.updatedAt = Date.now();
|
|
751
892
|
run.controller.abort();
|
|
893
|
+
rejectProviderCallbacks(run, new DOMException("Provider run aborted", "AbortError"));
|
|
752
894
|
await run.settled;
|
|
753
895
|
sendJson(res, 200, streamRunResponseBody(run));
|
|
754
896
|
}
|
|
@@ -760,7 +902,7 @@ export function buildStreamContext(session, body) {
|
|
|
760
902
|
tools: session.staticContext?.tools,
|
|
761
903
|
};
|
|
762
904
|
}
|
|
763
|
-
function handleStream(config, body, res) {
|
|
905
|
+
function handleStream(config, body, res, models) {
|
|
764
906
|
if (!body.sessionId) {
|
|
765
907
|
sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
766
908
|
return;
|
|
@@ -769,6 +911,10 @@ function handleStream(config, body, res) {
|
|
|
769
911
|
sendError(res, 400, "model is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
770
912
|
return;
|
|
771
913
|
}
|
|
914
|
+
if ((body.callbacks?.onPayload || body.callbacks?.onResponse) && !body.runId) {
|
|
915
|
+
sendError(res, 400, "runId is required for provider callbacks", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
772
918
|
cleanupExpiredStreamRuns(Date.now());
|
|
773
919
|
const session = getOrCreateSession(body.sessionId);
|
|
774
920
|
if (body.staticContext) {
|
|
@@ -798,6 +944,9 @@ function handleStream(config, body, res) {
|
|
|
798
944
|
});
|
|
799
945
|
res.flushHeaders();
|
|
800
946
|
res.write(STREAM_HEARTBEAT);
|
|
947
|
+
if (existingRun.cacheRetention !== undefined) {
|
|
948
|
+
writeServerSentEvent(res, "cache_policy", { cacheRetention: existingRun.cacheRetention });
|
|
949
|
+
}
|
|
801
950
|
for (const event of replayStreamRun(existingRun)) {
|
|
802
951
|
writeStreamEvent(res, event);
|
|
803
952
|
}
|
|
@@ -815,7 +964,31 @@ function handleStream(config, body, res) {
|
|
|
815
964
|
const run = body.runId ? startStreamRun(body.sessionId, body.runId) : undefined;
|
|
816
965
|
const streamOptions = {
|
|
817
966
|
...(body.options ?? {}),
|
|
967
|
+
transformPreparedStreamOptions: (prepared) => {
|
|
968
|
+
// Resolve after auth env merging, then pin the same policy for native dispatch.
|
|
969
|
+
const cacheRetention = prepared.cacheRetention ??
|
|
970
|
+
(getProviderEnvValue("PI_CACHE_RETENTION", prepared.env) === "long" ? "long" : "short");
|
|
971
|
+
if (cacheRetention !== "none" && cacheRetention !== "short" && cacheRetention !== "long") {
|
|
972
|
+
throw new Error("Invalid cache retention policy");
|
|
973
|
+
}
|
|
974
|
+
if (run)
|
|
975
|
+
run.cacheRetention = cacheRetention;
|
|
976
|
+
writeServerSentEvent(res, "cache_policy", { cacheRetention });
|
|
977
|
+
return { ...prepared, cacheRetention };
|
|
978
|
+
},
|
|
818
979
|
...(run ? { signal: run.controller.signal } : {}),
|
|
980
|
+
...(run ? createProviderCallbacks(run, res, body.callbacks) : {}),
|
|
981
|
+
...(body.observeProviderStreamEvents
|
|
982
|
+
? {
|
|
983
|
+
onProviderStreamEvent: (data, model) => {
|
|
984
|
+
writeStreamEvent(res, {
|
|
985
|
+
type: "provider_stream_event",
|
|
986
|
+
data,
|
|
987
|
+
model: { ...model, headers: undefined },
|
|
988
|
+
});
|
|
989
|
+
},
|
|
990
|
+
}
|
|
991
|
+
: {}),
|
|
819
992
|
};
|
|
820
993
|
res.writeHead(200, {
|
|
821
994
|
"Content-Type": "text/event-stream",
|
|
@@ -833,7 +1006,18 @@ function handleStream(config, body, res) {
|
|
|
833
1006
|
heartbeat.unref();
|
|
834
1007
|
let stream;
|
|
835
1008
|
try {
|
|
836
|
-
|
|
1009
|
+
if (body.requestKind === "deferred") {
|
|
1010
|
+
if (!body.deferredHandle)
|
|
1011
|
+
throw new Error("deferredHandle is required for deferred fetch");
|
|
1012
|
+
stream = (models ?? builtinModels()).streamDeferred(resolvedModel, body.deferredHandle, streamOptions);
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
const requestModels = models ?? createRequestModels(resolvedModel, streamOptions);
|
|
1016
|
+
stream =
|
|
1017
|
+
body.requestKind === "api"
|
|
1018
|
+
? requestModels.stream(resolvedModel, context, streamOptions)
|
|
1019
|
+
: requestModels.streamSimple(resolvedModel, context, streamOptions);
|
|
1020
|
+
}
|
|
837
1021
|
}
|
|
838
1022
|
catch (err) {
|
|
839
1023
|
clearInterval(heartbeat);
|
|
@@ -856,7 +1040,7 @@ function handleStream(config, body, res) {
|
|
|
856
1040
|
completeStreamRun(run, event.message);
|
|
857
1041
|
}
|
|
858
1042
|
else if (event.type === "error") {
|
|
859
|
-
failStreamRun(run, event.error.errorMessage ?? event.reason);
|
|
1043
|
+
failStreamRun(run, event.error.errorMessage ?? event.reason, event.error);
|
|
860
1044
|
}
|
|
861
1045
|
}
|
|
862
1046
|
}
|
|
@@ -875,7 +1059,23 @@ function handleStream(config, body, res) {
|
|
|
875
1059
|
}
|
|
876
1060
|
})();
|
|
877
1061
|
}
|
|
878
|
-
async function handlePostRequest(config, pathname, body, res) {
|
|
1062
|
+
async function handlePostRequest(config, pathname, body, res, models, streamModels) {
|
|
1063
|
+
if (pathname === "/api/provider-callback") {
|
|
1064
|
+
handleProviderCallbackReply(body, res);
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
if (pathname === "/api/generate-images") {
|
|
1068
|
+
await handleModelOperation(models, { ...body, kind: "image" }, res);
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
if (pathname === "/api/classify") {
|
|
1072
|
+
await handleModelOperation(models, { ...body, kind: "classifier" }, res);
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
if (pathname === "/api/cancel-deferred") {
|
|
1076
|
+
await handleModelOperation(models, { ...body, kind: "deferred-cancel" }, res);
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
879
1079
|
if (pathname === "/api/receive") {
|
|
880
1080
|
try {
|
|
881
1081
|
sendJson(res, 200, receiveUpload(config.uploadDir, body));
|
|
@@ -916,17 +1116,136 @@ async function handlePostRequest(config, pathname, body, res) {
|
|
|
916
1116
|
return true;
|
|
917
1117
|
}
|
|
918
1118
|
if (pathname === "/api/session/compact") {
|
|
919
|
-
await handleSessionCompact(config, body, res);
|
|
1119
|
+
await handleSessionCompact(config, body, res, streamModels);
|
|
920
1120
|
return true;
|
|
921
1121
|
}
|
|
922
1122
|
if (pathname === "/api/stream") {
|
|
923
|
-
handleStream(config, body, res);
|
|
1123
|
+
handleStream(config, body, res, streamModels);
|
|
924
1124
|
return true;
|
|
925
1125
|
}
|
|
926
1126
|
return false;
|
|
927
1127
|
}
|
|
928
|
-
|
|
1128
|
+
const CLASSIFIER_APIS = {
|
|
1129
|
+
"typesafe-system-one": typesafeSystemOneApi(),
|
|
1130
|
+
"cloudflare-workers-ai-system-one": cloudflareWorkersAISystemOneApi(),
|
|
1131
|
+
"llama-cpp-classify": llamaCppClassifyApi(),
|
|
1132
|
+
};
|
|
1133
|
+
async function handleModelOperation(models, body, res) {
|
|
1134
|
+
if (!body.sessionId ||
|
|
1135
|
+
!body.runId ||
|
|
1136
|
+
!body.model ||
|
|
1137
|
+
(body.kind === "deferred-cancel" ? !body.handle : !body.context)) {
|
|
1138
|
+
sendError(res, 400, "sessionId, runId, model and operation input are required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const existing = getStreamRun(body.sessionId, body.runId);
|
|
1142
|
+
if (existing && existing.kind !== body.kind) {
|
|
1143
|
+
sendError(res, 409, "runId belongs to a different operation", PiServerErrorCode.INVALID_REQUEST);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (existing?.status === "running") {
|
|
1147
|
+
sendError(res, 409, "A run with this runId is already in progress", PiServerErrorCode.RUN_IN_PROGRESS);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (existing?.status === "aborted") {
|
|
1151
|
+
sendError(res, 409, "Model operation was aborted", PiServerErrorCode.INVALID_REQUEST);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const run = existing?.operationResult ? existing : startStreamRun(body.sessionId, body.runId, body.kind);
|
|
1155
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
1156
|
+
res.flushHeaders();
|
|
1157
|
+
res.write(STREAM_HEARTBEAT);
|
|
1158
|
+
const heartbeat = setInterval(() => {
|
|
1159
|
+
touchStreamRun(run);
|
|
1160
|
+
if (!res.destroyed && !res.writableEnded)
|
|
1161
|
+
res.write(STREAM_HEARTBEAT);
|
|
1162
|
+
}, STREAM_HEARTBEAT_INTERVAL_MS);
|
|
1163
|
+
heartbeat.unref();
|
|
1164
|
+
try {
|
|
1165
|
+
if (!run.operationResult) {
|
|
1166
|
+
const requestModels = body.kind === "deferred-cancel" ? models : resolveOperationModels(models, body.model, body.options);
|
|
1167
|
+
const result = body.kind === "deferred-cancel"
|
|
1168
|
+
? await requestModels
|
|
1169
|
+
.cancelDeferred(body.model, body.handle, {
|
|
1170
|
+
...body.options,
|
|
1171
|
+
...createProviderCallbacks(run, res, body.callbacks),
|
|
1172
|
+
signal: run.controller.signal,
|
|
1173
|
+
})
|
|
1174
|
+
.then(() => ({ cancelled: true, stopReason: "stop" }))
|
|
1175
|
+
: body.kind === "image"
|
|
1176
|
+
? await requestModels.generateImages(body.model, body.context, {
|
|
1177
|
+
...body.options,
|
|
1178
|
+
...createProviderCallbacks(run, res, body.callbacks),
|
|
1179
|
+
signal: run.controller.signal,
|
|
1180
|
+
})
|
|
1181
|
+
: await requestModels.classify(body.model, body.context, {
|
|
1182
|
+
...body.options,
|
|
1183
|
+
...createProviderCallbacks(run, res, body.callbacks),
|
|
1184
|
+
signal: run.controller.signal,
|
|
1185
|
+
});
|
|
1186
|
+
run.operationResult = run.cancelRequested
|
|
1187
|
+
? { ...result, stopReason: "aborted", errorMessage: "Model operation aborted" }
|
|
1188
|
+
: result;
|
|
1189
|
+
run.status =
|
|
1190
|
+
run.cancelRequested || result.stopReason === "aborted"
|
|
1191
|
+
? "aborted"
|
|
1192
|
+
: result.stopReason === "error"
|
|
1193
|
+
? "failed"
|
|
1194
|
+
: "completed";
|
|
1195
|
+
run.errorMessage = "errorMessage" in result ? result.errorMessage : undefined;
|
|
1196
|
+
run.updatedAt = Date.now();
|
|
1197
|
+
}
|
|
1198
|
+
if (!res.destroyed && !res.writableEnded)
|
|
1199
|
+
writeServerSentEvent(res, "result", run.operationResult);
|
|
1200
|
+
}
|
|
1201
|
+
catch (error) {
|
|
1202
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1203
|
+
failStreamRun(run, message);
|
|
1204
|
+
if (!res.destroyed && !res.writableEnded)
|
|
1205
|
+
writeServerSentEvent(res, "error", { error: message });
|
|
1206
|
+
}
|
|
1207
|
+
finally {
|
|
1208
|
+
clearInterval(heartbeat);
|
|
1209
|
+
settleRun(run);
|
|
1210
|
+
endStreamResponse(res);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function resolveOperationModels(models, model, options) {
|
|
1214
|
+
const provider = models.getProvider(model.provider);
|
|
1215
|
+
const hasDeclaredApi = provider
|
|
1216
|
+
?.getAllModels?.()
|
|
1217
|
+
.some((candidate) => candidate.type === model.type && candidate.api === model.api);
|
|
1218
|
+
if (hasDeclaredApi &&
|
|
1219
|
+
((model.type === "image" && provider?.generateImages) || (model.type === "classifier" && provider?.classify)))
|
|
1220
|
+
return models;
|
|
1221
|
+
const requestModels = createModels();
|
|
1222
|
+
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider, options?.env);
|
|
1223
|
+
const auth = provider?.auth ?? {
|
|
1224
|
+
apiKey: {
|
|
1225
|
+
name: "pi-server request auth",
|
|
1226
|
+
resolve: async () => ({ auth: { apiKey, headers: options?.headers } }),
|
|
1227
|
+
},
|
|
1228
|
+
};
|
|
1229
|
+
if (model.type === "image") {
|
|
1230
|
+
const api = getImagesApiProvider(model.api);
|
|
1231
|
+
if (!api)
|
|
1232
|
+
throw new Error(`No image API registered on pi-server: ${model.api}`);
|
|
1233
|
+
requestModels.setProvider(createProvider({ id: model.provider, models: [model], auth, images: { [model.api]: api } }));
|
|
1234
|
+
}
|
|
1235
|
+
else if (model.type === "classifier") {
|
|
1236
|
+
const api = CLASSIFIER_APIS[model.api];
|
|
1237
|
+
if (!api)
|
|
1238
|
+
throw new Error(`No classifier API registered on pi-server: ${model.api}`);
|
|
1239
|
+
requestModels.setProvider(createProvider({ id: model.provider, models: [model], auth, classifiers: { [model.api]: api } }));
|
|
1240
|
+
}
|
|
1241
|
+
else {
|
|
1242
|
+
throw new Error("Image generation and classification require an operation-specific model type");
|
|
1243
|
+
}
|
|
1244
|
+
return requestModels;
|
|
1245
|
+
}
|
|
1246
|
+
export function createPiServer(configOverride, streamModels) {
|
|
929
1247
|
const config = loadConfig(configOverride);
|
|
1248
|
+
const models = streamModels ?? builtinModels();
|
|
930
1249
|
loadPersistedSessions(config.sessionStoreDir);
|
|
931
1250
|
const server = createServer(async (req, res) => {
|
|
932
1251
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
@@ -945,6 +1264,24 @@ export function createPiServer(configOverride) {
|
|
|
945
1264
|
sendJson(res, 200, { sessions: listSessions() });
|
|
946
1265
|
return;
|
|
947
1266
|
}
|
|
1267
|
+
if (req.method === "GET" && url.pathname === "/api/models") {
|
|
1268
|
+
try {
|
|
1269
|
+
const stripHeaders = (model) => ({ ...model, headers: undefined });
|
|
1270
|
+
sendJson(res, 200, {
|
|
1271
|
+
models: models.getAllModels().map(stripHeaders),
|
|
1272
|
+
available: (await models.getAllAvailable()).map(stripHeaders),
|
|
1273
|
+
providers: models.getProviders().map((provider) => ({
|
|
1274
|
+
id: provider.id,
|
|
1275
|
+
fetchDeferred: provider.fetchDeferred !== undefined,
|
|
1276
|
+
cancelDeferred: provider.cancelDeferred !== undefined,
|
|
1277
|
+
})),
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
catch (error) {
|
|
1281
|
+
sendError(res, 500, error instanceof Error ? error.message : String(error), PiServerErrorCode.INTERNAL_ERROR);
|
|
1282
|
+
}
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
948
1285
|
const runMatch = /^\/api\/session\/([^/]+)\/runs\/([^/]+)$/.exec(url.pathname);
|
|
949
1286
|
if (req.method === "GET" && runMatch) {
|
|
950
1287
|
handleSessionRun(decodeURIComponent(runMatch[1]), decodeURIComponent(runMatch[2]), res);
|
|
@@ -987,7 +1324,7 @@ export function createPiServer(configOverride) {
|
|
|
987
1324
|
sendJson(res, 200, chunkResult.ack);
|
|
988
1325
|
return;
|
|
989
1326
|
}
|
|
990
|
-
const handled = await handlePostRequest(config, chunkResult.target, JSON.parse(chunkResult.bodyJson), res);
|
|
1327
|
+
const handled = await handlePostRequest(config, chunkResult.target, JSON.parse(chunkResult.bodyJson), res, models, streamModels);
|
|
991
1328
|
if (!handled && !res.headersSent) {
|
|
992
1329
|
sendError(res, 404, "Not found", PiServerErrorCode.INVALID_REQUEST);
|
|
993
1330
|
}
|
|
@@ -1007,7 +1344,7 @@ export function createPiServer(configOverride) {
|
|
|
1007
1344
|
if (req.method === "POST") {
|
|
1008
1345
|
try {
|
|
1009
1346
|
const body = JSON.parse(await readBody(req));
|
|
1010
|
-
if (await handlePostRequest(config, url.pathname, body, res))
|
|
1347
|
+
if (await handlePostRequest(config, url.pathname, body, res, models, streamModels))
|
|
1011
1348
|
return;
|
|
1012
1349
|
if (!res.headersSent) {
|
|
1013
1350
|
sendError(res, 404, "Not found", PiServerErrorCode.INVALID_REQUEST);
|
|
@@ -1029,8 +1366,8 @@ export function createPiServer(configOverride) {
|
|
|
1029
1366
|
if (req.method === "DELETE" && url.pathname.startsWith("/api/session/")) {
|
|
1030
1367
|
const sessionId = decodeURIComponent(url.pathname.slice("/api/session/".length));
|
|
1031
1368
|
deleteSessionFromStore(sessionId);
|
|
1032
|
-
deleteStreamRunsForSession(sessionId);
|
|
1033
1369
|
deletePersistedSession(config.sessionStoreDir, sessionId);
|
|
1370
|
+
await deleteStreamRunsForSession(sessionId);
|
|
1034
1371
|
sendJson(res, 200, { deleted: sessionId });
|
|
1035
1372
|
return;
|
|
1036
1373
|
}
|
|
@@ -1094,6 +1431,7 @@ function toProxyEvent(event) {
|
|
|
1094
1431
|
type: "error",
|
|
1095
1432
|
reason: event.reason,
|
|
1096
1433
|
errorMessage: event.error.errorMessage,
|
|
1434
|
+
message: event.error,
|
|
1097
1435
|
usage: event.error.usage,
|
|
1098
1436
|
};
|
|
1099
1437
|
default:
|