@averyyy/pi-server 1.0.0-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/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 { streamSimple } from "@earendil-works/pi-ai/compat";
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 requestStream = (requestModel, context, streamOptions) => streamSimple(requestModel, context, {
20
- ...streamOptions,
21
- ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
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
- apiKey: {
29
- name: "pi-server request auth",
30
- resolve: async () => ({ auth: { apiKey: options.apiKey, headers: options.headers } }),
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: requestStream,
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
- for (const [key, run] of streamRuns) {
248
- if (run.sessionId === sessionId)
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, createRequestModels(body.model, prepared.options), body.model, body.customInstructions, run?.controller.signal, prepared.options.reasoning, undefined, undefined, prepared.cacheContext, prepared.options, prepared.cacheFallbackReason);
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
- stream = streamSimple(resolvedModel, context, streamOptions);
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
- export function createPiServer(configOverride) {
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: