@skydiveai/pi-server 0.1.0-beta.1738 → 0.1.0-beta.2448

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.
Files changed (2) hide show
  1. package/dist/index.mjs +72 -35
  2. package/package.json +1 -7
package/dist/index.mjs CHANGED
@@ -259,7 +259,7 @@ function parseShellEnv(request) {
259
259
  const env = {};
260
260
  for (const [k, v] of Object.entries(parsed)) if (typeof v === "string") env[k] = v;
261
261
  return Object.keys(env).length > 0 ? env : null;
262
- } catch {
262
+ } catch (_err) {
263
263
  return null;
264
264
  }
265
265
  }
@@ -283,12 +283,12 @@ function createSSEResponse(handler) {
283
283
  write(chunk) {
284
284
  try {
285
285
  controller.enqueue(encoder.encode(chunk));
286
- } catch {}
286
+ } catch (_err) {}
287
287
  },
288
288
  close() {
289
289
  try {
290
290
  controller.close();
291
- } catch {}
291
+ } catch (_err) {}
292
292
  }
293
293
  };
294
294
  const keepalive = setInterval(() => {
@@ -298,13 +298,13 @@ function createSSEResponse(handler) {
298
298
  clearInterval(keepalive);
299
299
  try {
300
300
  controller.close();
301
- } catch {}
301
+ } catch (_err) {}
302
302
  }, (err) => {
303
303
  logger.error({ err }, "SSE handler crashed — stream closed without response");
304
304
  clearInterval(keepalive);
305
305
  try {
306
306
  controller.close();
307
- } catch {}
307
+ } catch (_err) {}
308
308
  });
309
309
  return new Response(stream, {
310
310
  status: 200,
@@ -470,7 +470,10 @@ async function extractParts(parts, log) {
470
470
  }
471
471
  if (part.url != null && part.mediaType?.startsWith("image/")) {
472
472
  try {
473
- images.push(await fetchImageAsBase64({ url: part.url }));
473
+ images.push(await fetchImageAsBase64({
474
+ url: part.url,
475
+ userAgent: null
476
+ }));
474
477
  } catch (err) {
475
478
  log.warn({
476
479
  event: "a2a_image_fetch_failed",
@@ -999,7 +1002,10 @@ async function extractContent$1(content, log) {
999
1002
  data: src.data
1000
1003
  });
1001
1004
  else if (src?.type === "url" && src.url) try {
1002
- images.push(await fetchImageAsBase64({ url: src.url }));
1005
+ images.push(await fetchImageAsBase64({
1006
+ url: src.url,
1007
+ userAgent: null
1008
+ }));
1003
1009
  } catch (err) {
1004
1010
  log.warn({
1005
1011
  event: "image_fetch_failed",
@@ -1088,7 +1094,7 @@ async function handleMessages(request, ctx, registry) {
1088
1094
  let parsed;
1089
1095
  try {
1090
1096
  parsed = JSON.parse(await request.text());
1091
- } catch {
1097
+ } catch (_err) {
1092
1098
  return jsonError(400, "invalid json");
1093
1099
  }
1094
1100
  const { messages, stream = false, model: modelInput, system, x_model: modelSpecInput } = parsed ?? {};
@@ -1111,7 +1117,7 @@ async function handleMessages(request, ctx, registry) {
1111
1117
  const baseSystemPrompt = extractSystemPrompt(system);
1112
1118
  const { sessionId } = parseSessionId(request);
1113
1119
  const shellEnv = parseShellEnv(request);
1114
- const { session, sessionManager: sm } = await ctx.createSession({
1120
+ const { session } = await ctx.createSession({
1115
1121
  cwd: ctx.cwd,
1116
1122
  sessionId,
1117
1123
  perRequestApiKeys: withSpecApiKey(resolvePerRequestApiKeys(request), modelSpec),
@@ -1134,7 +1140,6 @@ async function handleMessages(request, ctx, registry) {
1134
1140
  if (stream) {
1135
1141
  const response = runStream$2({
1136
1142
  session,
1137
- sm,
1138
1143
  prompt,
1139
1144
  images,
1140
1145
  id,
@@ -1150,7 +1155,6 @@ async function handleMessages(request, ctx, registry) {
1150
1155
  }
1151
1156
  const response = await runBlocking$2({
1152
1157
  session,
1153
- sm,
1154
1158
  prompt,
1155
1159
  images,
1156
1160
  id,
@@ -1164,7 +1168,7 @@ async function handleMessages(request, ctx, registry) {
1164
1168
  if (traceId) response.headers.set("x-trace-id", traceId);
1165
1169
  return response;
1166
1170
  }
1167
- function runStream$2({ session, sm, prompt, images, id, sessionId, modelName, log, postPrompt, registry }) {
1171
+ function runStream$2({ session, prompt, images, id, sessionId, modelName, log, postPrompt, registry }) {
1168
1172
  return createSSEResponse(async (writer) => {
1169
1173
  let contentBlockIndex = 0;
1170
1174
  let textBlockOpen = false;
@@ -1322,7 +1326,7 @@ function runStream$2({ session, sm, prompt, images, id, sessionId, modelName, lo
1322
1326
  sseEvent(writer, "message_stop", { type: "message_stop" });
1323
1327
  });
1324
1328
  }
1325
- async function runBlocking$2({ session, sm, prompt, images, id, sessionId, modelName, log, postPrompt, registry }) {
1329
+ async function runBlocking$2({ session, prompt, images, id, sessionId, modelName, log, postPrompt, registry }) {
1326
1330
  let text = "";
1327
1331
  const toolUseBlocks = [];
1328
1332
  const tcByContentIdx = /* @__PURE__ */ new Map();
@@ -1373,7 +1377,7 @@ async function runBlocking$2({ session, sm, prompt, images, id, sessionId, model
1373
1377
  }
1374
1378
  for (const block of toolUseBlocks) if (block.type === "tool_use" && typeof block.input === "string") try {
1375
1379
  block.input = JSON.parse(block.input);
1376
- } catch {
1380
+ } catch (_err) {
1377
1381
  block.input = {};
1378
1382
  }
1379
1383
  const content = [];
@@ -1464,13 +1468,34 @@ const nestedProxyEnvelopeSchema = z.object({
1464
1468
  blockReason: z.enum(BILLING_BLOCK_REASONS),
1465
1469
  message: z.string()
1466
1470
  }).strict();
1471
+ /**
1472
+ * pi-ai's openai-compatible providers (openrouter, xai, groq, deepseek, …) can
1473
+ * NOT fold a proxy's non-2xx body into `error.message`, so `formatProviderError`
1474
+ * (@earendil-works/pi-ai utils/error-body) composes the display string as
1475
+ * `"<status>: <body>"` or, with a provider label, `"<prefix> (<status>): <body>"`.
1476
+ * The billing gate's 402 body therefore reaches us wrapped, e.g.
1477
+ * `"402: {\"error\":{...},\"blockReason\":\"insufficient_balance\",\"message\":...}"`
1478
+ * or `"OpenRouter (402): {...}"`. Neither the bare `JSON.parse` nor the `"402 "`
1479
+ * (space) strip below recognizes that, so a real billing block from an
1480
+ * openrouter-routed model degrades to the generic model-provider error. Peel a
1481
+ * single leading `"<status>: "` / `"<prefix> (<status>): "` wrapper off the
1482
+ * front so the recovered body flows through the existing shape checks. Returns
1483
+ * the message unchanged when no wrapper is present.
1484
+ */
1485
+ function unwrapOpenAICompatStatusPrefix(message) {
1486
+ const withPrefix = message.match(/^.+ \(\d{3}\): ([\s\S]+)$/);
1487
+ if (withPrefix?.[1] !== void 0) return withPrefix[1];
1488
+ const bare = message.match(/^\d{3}: ([\s\S]+)$/);
1489
+ if (bare?.[1] !== void 0) return bare[1];
1490
+ return message;
1491
+ }
1467
1492
  function parsePrefixedSignal(message) {
1468
1493
  const normalized = message.startsWith("402 ") ? message.slice(4) : message;
1469
1494
  if (!normalized.startsWith(BILLING_BLOCKED_PROVIDER_SIGNAL_PREFIX)) return null;
1470
1495
  try {
1471
1496
  const parsed = payloadSchema.safeParse(JSON.parse(normalized.slice(26)));
1472
1497
  return parsed.success ? parsed.data : null;
1473
- } catch {
1498
+ } catch (_err) {
1474
1499
  return null;
1475
1500
  }
1476
1501
  }
@@ -1480,16 +1505,17 @@ function parsePrefixedSignal(message) {
1480
1505
  * proxy response as JSON, so that outer shape is validated separately.
1481
1506
  */
1482
1507
  function parseBillingBlockedProviderSignal(message) {
1483
- const direct = parsePrefixedSignal(message);
1508
+ const unwrapped = unwrapOpenAICompatStatusPrefix(message);
1509
+ const direct = parsePrefixedSignal(unwrapped);
1484
1510
  if (direct) return direct;
1485
1511
  try {
1486
- const envelope = nestedProxyEnvelopeSchema.safeParse(JSON.parse(message));
1512
+ const envelope = nestedProxyEnvelopeSchema.safeParse(JSON.parse(unwrapped));
1487
1513
  if (!envelope.success) return null;
1488
1514
  const nested = parsePrefixedSignal(envelope.data.error.message);
1489
1515
  const topLevel = parsePrefixedSignal(envelope.data.message);
1490
1516
  if (!nested || nested.blockReason !== envelope.data.blockReason || envelope.data.message !== nested.message && (!topLevel || topLevel.blockReason !== nested.blockReason || topLevel.message !== nested.message)) return null;
1491
1517
  return nested;
1492
- } catch {
1518
+ } catch (_err) {
1493
1519
  return null;
1494
1520
  }
1495
1521
  }
@@ -1678,7 +1704,7 @@ async function loadHistoryIntoSession({ sm, messages, upToIdxExclusive, modelNam
1678
1704
  let args = {};
1679
1705
  try {
1680
1706
  args = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
1681
- } catch {
1707
+ } catch (_err) {
1682
1708
  args = { _raw: tc.function.arguments };
1683
1709
  }
1684
1710
  contentArr.push({
@@ -1720,6 +1746,15 @@ async function loadHistoryIntoSession({ sm, messages, upToIdxExclusive, modelNam
1720
1746
  }
1721
1747
  }
1722
1748
  }
1749
+ /**
1750
+ * Each pi/session message carries the real wall-clock `timestamp` the
1751
+ * provider stamped when it produced that message (see @earendil-works/pi-ai
1752
+ * Message). Forward it verbatim as `x_created_at` (epoch ms) on the OAI
1753
+ * trailer message so the consumer (anyone-messaging completeRun) can stamp
1754
+ * each persisted segment at its true emit time and interleave a mid-run steer
1755
+ * by createdAt — instead of positionally reconstructing per-turn times, which
1756
+ * drifts on parallel tool calls and provider-split replies.
1757
+ */
1723
1758
  function messageTimestampMs(m) {
1724
1759
  const ts = m?.timestamp;
1725
1760
  return typeof ts === "number" && Number.isFinite(ts) ? ts : null;
@@ -1781,7 +1816,7 @@ async function handleChatCompletions(request, ctx, registry, pendingSteerIds) {
1781
1816
  let parsed;
1782
1817
  try {
1783
1818
  parsed = JSON.parse(await request.text());
1784
- } catch {
1819
+ } catch (_err) {
1785
1820
  return jsonError(400, "invalid json");
1786
1821
  }
1787
1822
  const { messages, stream = false, model: modelInput, thinkingLevel: thinkingInput, context_window: contextWindowInput, x_model: modelSpecInput } = parsed ?? {};
@@ -1793,7 +1828,7 @@ async function handleChatCompletions(request, ctx, registry, pendingSteerIds) {
1793
1828
  }
1794
1829
  if (lastUserIdx < 0) return jsonError(400, "no user message in history");
1795
1830
  const lastUser = messages[lastUserIdx];
1796
- const reqUA = request.headers.get("user-agent") ?? void 0;
1831
+ const reqUA = request.headers.get("user-agent") ?? null;
1797
1832
  const { text: prompt, images } = await extractContent(lastUser.content, {
1798
1833
  userAgent: reqUA,
1799
1834
  log
@@ -1824,7 +1859,7 @@ async function handleChatCompletions(request, ctx, registry, pendingSteerIds) {
1824
1859
  const sessionId = parsedSession.source === "header" ? parsedSession.sessionId : `chatcmpl-${parsedSession.sessionId}`;
1825
1860
  const shellEnv = parseShellEnv(request);
1826
1861
  const sessionSetupStart = performance.now();
1827
- const [{ session, sessionManager: sm }, customTools] = await Promise.all([ctx.createSession({
1862
+ const [{ session, sessionManager: sm }] = await Promise.all([ctx.createSession({
1828
1863
  cwd: ctx.cwd,
1829
1864
  sessionId,
1830
1865
  perRequestApiKeys: withSpecApiKey(resolvePerRequestApiKeys(request), modelSpec),
@@ -1936,7 +1971,7 @@ function runStream$1({ session, sm, prompt, images, sessionId, created, reqStart
1936
1971
  };
1937
1972
  const ensureRole = () => {
1938
1973
  if (roleEmitted) return;
1939
- sendChunk({ role: "assistant" });
1974
+ sendChunk({ role: "assistant" }, null);
1940
1975
  roleEmitted = true;
1941
1976
  };
1942
1977
  let _evCount = 0;
@@ -1960,7 +1995,7 @@ function runStream$1({ session, sm, prompt, images, sessionId, created, reqStart
1960
1995
  steering,
1961
1996
  follow_up: followUp,
1962
1997
  ...consumedIds.length > 0 ? { consumed_steer_ids: consumedIds } : {}
1963
- } });
1998
+ } }, null);
1964
1999
  return;
1965
2000
  }
1966
2001
  _evCount++;
@@ -1983,7 +2018,7 @@ function runStream$1({ session, sm, prompt, images, sessionId, created, reqStart
1983
2018
  ...ev.type === "message_start" || ev.type === "message_end" ? { messageJson: JSON.stringify(ev.message).slice(0, 500) } : {}
1984
2019
  }, "harness session event");
1985
2020
  if (ev.type === "tool_execution_start") {
1986
- sendChunk({ x_tool_execution_start: { tool_call_id: String(ev.toolCallId ?? "") } });
2021
+ sendChunk({ x_tool_execution_start: { tool_call_id: String(ev.toolCallId ?? "") } }, null);
1987
2022
  return;
1988
2023
  }
1989
2024
  if (ev.type === "tool_execution_end") {
@@ -1992,7 +2027,7 @@ function runStream$1({ session, sm, prompt, images, sessionId, created, reqStart
1992
2027
  tool_call_id: String(ev.toolCallId ?? ""),
1993
2028
  content: text,
1994
2029
  ...ev.isError ? { is_error: true } : {}
1995
- } });
2030
+ } }, null);
1996
2031
  return;
1997
2032
  }
1998
2033
  if (ev.type === "agent_end") {
@@ -2004,11 +2039,11 @@ function runStream$1({ session, sm, prompt, images, sessionId, created, reqStart
2004
2039
  const t = inner?.type;
2005
2040
  if (t === "text_delta") {
2006
2041
  ensureRole();
2007
- sendChunk({ content: inner.delta ?? "" });
2042
+ sendChunk({ content: inner.delta ?? "" }, null);
2008
2043
  return;
2009
2044
  }
2010
2045
  if (t === "thinking_delta") {
2011
- sendChunk({ x_thinking_delta: inner.delta ?? "" });
2046
+ sendChunk({ x_thinking_delta: inner.delta ?? "" }, null);
2012
2047
  return;
2013
2048
  }
2014
2049
  if (t === "toolcall_start") {
@@ -2026,7 +2061,7 @@ function runStream$1({ session, sm, prompt, images, sessionId, created, reqStart
2026
2061
  name: tc?.name ?? "",
2027
2062
  arguments: ""
2028
2063
  }
2029
- }] });
2064
+ }] }, null);
2030
2065
  return;
2031
2066
  }
2032
2067
  if (t === "toolcall_delta") {
@@ -2035,7 +2070,7 @@ function runStream$1({ session, sm, prompt, images, sessionId, created, reqStart
2035
2070
  sendChunk({ tool_calls: [{
2036
2071
  index: openaiIdx,
2037
2072
  function: { arguments: inner.delta ?? "" }
2038
- }] });
2073
+ }] }, null);
2039
2074
  }
2040
2075
  });
2041
2076
  try {
@@ -2301,7 +2336,6 @@ const steerRequestSchema = z.object({
2301
2336
  mode: z.enum(["steer", "follow-up"]).default("steer"),
2302
2337
  text: z.string().optional()
2303
2338
  }).refine((v) => v.content !== void 0 || v.text !== void 0, { message: "content or text is required" });
2304
- z.string().uuid();
2305
2339
  async function handleSteer(request, sessionId, registry, pendingSteerIds) {
2306
2340
  const { log } = requestLogger(request);
2307
2341
  const session = registry.get(sessionId);
@@ -2309,14 +2343,14 @@ async function handleSteer(request, sessionId, registry, pendingSteerIds) {
2309
2343
  let body;
2310
2344
  try {
2311
2345
  body = JSON.parse(await request.text());
2312
- } catch {
2346
+ } catch (_err) {
2313
2347
  return jsonError(400, "invalid json");
2314
2348
  }
2315
2349
  const parsed = steerRequestSchema.safeParse(body);
2316
2350
  if (!parsed.success) return jsonError(400, parsed.error.message);
2317
2351
  const { mode } = parsed.data;
2318
2352
  const { text, images } = await extractContent(parsed.data.content ?? parsed.data.text ?? "", {
2319
- userAgent: request.headers.get("user-agent") ?? void 0,
2353
+ userAgent: request.headers.get("user-agent") ?? null,
2320
2354
  log
2321
2355
  });
2322
2356
  if (!text && images.length === 0) return jsonError(400, "steer has no content");
@@ -2469,7 +2503,10 @@ async function extractImagesFromContent(content, log) {
2469
2503
  for (const part of content) if (part?.type === "input_image") {
2470
2504
  const url = part.image_url ?? part.url;
2471
2505
  if (typeof url === "string" && /^https?:\/\//i.test(url)) try {
2472
- images.push(await fetchImageAsBase64({ url }));
2506
+ images.push(await fetchImageAsBase64({
2507
+ url,
2508
+ userAgent: null
2509
+ }));
2473
2510
  } catch (err) {
2474
2511
  log.warn({
2475
2512
  event: "image_fetch_failed",
@@ -2490,7 +2527,7 @@ async function handleResponses(request, ctx, registry) {
2490
2527
  let parsed;
2491
2528
  try {
2492
2529
  parsed = JSON.parse(await request.text());
2493
- } catch {
2530
+ } catch (_err) {
2494
2531
  return jsonError(400, "invalid json");
2495
2532
  }
2496
2533
  const { input, stream = false, model: modelInput, instructions, x_model: modelSpecInput } = parsed ?? {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skydiveai/pi-server",
3
- "version": "0.1.0-beta.1738",
3
+ "version": "0.1.0-beta.2448",
4
4
  "homepage": "https://skydive.com",
5
5
  "license": "MIT",
6
6
  "author": "Create, Inc.",
@@ -16,12 +16,6 @@
16
16
  }
17
17
  },
18
18
  "publishConfig": {
19
- "exports": {
20
- ".": {
21
- "types": "./dist/index.d.mts",
22
- "default": "./dist/index.mjs"
23
- }
24
- },
25
19
  "access": "public",
26
20
  "registry": "https://registry.npmjs.org"
27
21
  },