@openhoo/hoopilot 0.8.2 → 0.8.4

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/index.cjs CHANGED
@@ -276,6 +276,25 @@ function completionStreamFromChatStream(chatStream) {
276
276
  }
277
277
  });
278
278
  }
279
+ function completionSseTextFromChatSseText(text) {
280
+ const chunks = [];
281
+ let sawTerminalEvent = false;
282
+ const enqueue = (data) => {
283
+ chunks.push(encodeDataSse(data));
284
+ };
285
+ const markTerminal = () => {
286
+ sawTerminalEvent = true;
287
+ };
288
+ for (const block of text.split(/\r?\n\r?\n/)) {
289
+ if (block.trim()) {
290
+ processCompletionSseBlock(block, enqueue, markTerminal);
291
+ }
292
+ }
293
+ if (!sawTerminalEvent) {
294
+ enqueue("[DONE]");
295
+ }
296
+ return chunks.join("");
297
+ }
279
298
  function normalizeModelsResponse(upstream) {
280
299
  const record = asRecord(upstream);
281
300
  const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
@@ -994,16 +1013,7 @@ function responsesStreamToAnthropicStream(stream, options) {
994
1013
  const decoder = new TextDecoder();
995
1014
  const encoder = new TextEncoder();
996
1015
  let buffer = "";
997
- const state = {
998
- blocks: /* @__PURE__ */ new Map(),
999
- completed: false,
1000
- messageId: options.messageId ?? `msg_${randomId2()}`,
1001
- model: options.model,
1002
- nextBlockIndex: 0,
1003
- sawToolUse: false,
1004
- started: false,
1005
- usage: anthropicUsage(void 0)
1006
- };
1016
+ const state = createAnthropicStreamState(options);
1007
1017
  return new ReadableStream({
1008
1018
  async start(controller) {
1009
1019
  const enqueue = (event, data) => {
@@ -1039,6 +1049,20 @@ function responsesStreamToAnthropicStream(stream, options) {
1039
1049
  }
1040
1050
  });
1041
1051
  }
1052
+ function responsesSseTextToAnthropicSseText(text, options) {
1053
+ const chunks = [];
1054
+ const state = createAnthropicStreamState(options);
1055
+ const enqueue = (event, data) => {
1056
+ chunks.push(encodeSse2(event, data));
1057
+ };
1058
+ for (const block of text.split(/\r?\n\r?\n/)) {
1059
+ if (block.trim()) {
1060
+ processResponsesSseBlock(block, state, enqueue);
1061
+ }
1062
+ }
1063
+ finishAnthropicStream(state, enqueue);
1064
+ return chunks.join("");
1065
+ }
1042
1066
  function estimateAnthropicMessageTokens(request) {
1043
1067
  const chars = estimatedTextSize(request.system) + estimatedTextSize(request.messages) + estimatedTextSize(request.tools) + estimatedTextSize(request.tool_choice) + estimatedTextSize(request.thinking);
1044
1068
  const messageCount = Array.isArray(request.messages) ? request.messages.length : 1;
@@ -1049,6 +1073,18 @@ function estimateAnthropicMessageTokens(request) {
1049
1073
  total_tokens: inputTokens
1050
1074
  };
1051
1075
  }
1076
+ function createAnthropicStreamState(options) {
1077
+ return {
1078
+ blocks: /* @__PURE__ */ new Map(),
1079
+ completed: false,
1080
+ messageId: options.messageId ?? `msg_${randomId2()}`,
1081
+ model: options.model,
1082
+ nextBlockIndex: 0,
1083
+ sawToolUse: false,
1084
+ started: false,
1085
+ usage: anthropicUsage(void 0)
1086
+ };
1087
+ }
1052
1088
  function anthropicMessagesToResponsesInput(messages) {
1053
1089
  if (!Array.isArray(messages)) {
1054
1090
  throw new AnthropicCompatibilityError("Anthropic Messages requests require messages[].");
@@ -2466,6 +2502,20 @@ function observeResponseUsage(response, fallbackModel, onUsage, signal) {
2466
2502
  statusText: response.statusText
2467
2503
  });
2468
2504
  }
2505
+ function recordResponseTextUsage(text, isSse, fallbackModel, onUsage) {
2506
+ const accumulator = createUsageAccumulator(fallbackModel, onUsage);
2507
+ if (isSse) {
2508
+ for (const line of text.split(/\r?\n/)) {
2509
+ considerSseLine(line, accumulator.consider);
2510
+ }
2511
+ } else {
2512
+ const parsed = safeParse(text);
2513
+ if (parsed !== void 0) {
2514
+ accumulator.consider(parsed);
2515
+ }
2516
+ }
2517
+ accumulator.finish();
2518
+ }
2469
2519
  async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal) {
2470
2520
  const reader = stream.getReader();
2471
2521
  const onAbort = () => {
@@ -2479,22 +2529,10 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal) {
2479
2529
  signal?.addEventListener("abort", onAbort, { once: true });
2480
2530
  }
2481
2531
  const decoder = new TextDecoder();
2482
- let model = fallbackModel;
2483
- let usage;
2532
+ const accumulator = createUsageAccumulator(fallbackModel, onUsage);
2484
2533
  let buffer = "";
2485
2534
  let bufferedBytes = 0;
2486
2535
  let overflowed = false;
2487
- const consider = (payload) => {
2488
- const record = asRecord(payload);
2489
- const found = extractTokenUsage(record.usage) ?? extractTokenUsage(asRecord(record.response).usage);
2490
- if (found) {
2491
- usage = found;
2492
- }
2493
- const candidateModel = modelText(record.model) || modelText(asRecord(record.response).model);
2494
- if (candidateModel) {
2495
- model = candidateModel;
2496
- }
2497
- };
2498
2536
  try {
2499
2537
  while (true) {
2500
2538
  const result = await reader.read();
@@ -2507,7 +2545,7 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal) {
2507
2545
  const lines = buffer.split(/\r?\n/);
2508
2546
  buffer = lines.pop() ?? "";
2509
2547
  for (const line of lines) {
2510
- considerSseLine(line, consider);
2548
+ considerSseLine(line, accumulator.consider);
2511
2549
  }
2512
2550
  if (buffer.length > USAGE_BUFFER_LIMIT_BYTES) {
2513
2551
  buffer = "";
@@ -2525,21 +2563,41 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal) {
2525
2563
  const finalBuffer = buffer + decoder.decode();
2526
2564
  if (isSse) {
2527
2565
  if (finalBuffer) {
2528
- considerSseLine(finalBuffer, consider);
2566
+ considerSseLine(finalBuffer, accumulator.consider);
2529
2567
  }
2530
2568
  } else if (!overflowed && finalBuffer) {
2531
2569
  const parsed = safeParse(finalBuffer);
2532
2570
  if (parsed !== void 0) {
2533
- consider(parsed);
2571
+ accumulator.consider(parsed);
2534
2572
  }
2535
2573
  }
2536
2574
  } finally {
2537
2575
  signal?.removeEventListener("abort", onAbort);
2538
2576
  reader.releaseLock();
2539
2577
  }
2540
- if (usage) {
2541
- onUsage(model, usage);
2542
- }
2578
+ accumulator.finish();
2579
+ }
2580
+ function createUsageAccumulator(fallbackModel, onUsage) {
2581
+ let model = fallbackModel;
2582
+ let usage;
2583
+ return {
2584
+ consider(payload) {
2585
+ const record = asRecord(payload);
2586
+ const found = extractTokenUsage(record.usage) ?? extractTokenUsage(asRecord(record.response).usage);
2587
+ if (found) {
2588
+ usage = found;
2589
+ }
2590
+ const candidateModel = modelText(record.model) || modelText(asRecord(record.response).model);
2591
+ if (candidateModel) {
2592
+ model = candidateModel;
2593
+ }
2594
+ },
2595
+ finish() {
2596
+ if (usage) {
2597
+ onUsage(model, usage);
2598
+ }
2599
+ }
2600
+ };
2543
2601
  }
2544
2602
  function considerSseLine(line, consider) {
2545
2603
  const trimmed = line.trim();
@@ -2586,6 +2644,10 @@ function formatNumber(value) {
2586
2644
  return Number.isInteger(value) ? value.toString() : String(value);
2587
2645
  }
2588
2646
 
2647
+ // src/version.ts
2648
+ var BAKED_VERSION = typeof HOOPILOT_VERSION !== "undefined" ? HOOPILOT_VERSION : void 0;
2649
+ var IS_STANDALONE_BINARY = BAKED_VERSION !== void 0;
2650
+
2589
2651
  // src/server.ts
2590
2652
  var DEFAULT_HOST = "127.0.0.1";
2591
2653
  var DEFAULT_PORT = 4141;
@@ -2609,6 +2671,8 @@ function createHoopilotHandler(options = {}) {
2609
2671
  const metrics = options.metrics ?? new MetricsRegistry();
2610
2672
  const readUsage = createUsageReader(client, metrics);
2611
2673
  const recordTokens = (model, usage) => metrics.recordTokens(model, usage);
2674
+ const streamingProxyMode = resolveStreamingProxyMode(options);
2675
+ const bufferProxyBodies = shouldBufferProxyBodies(streamingProxyMode);
2612
2676
  return async (request) => {
2613
2677
  const startedAt = performance.now();
2614
2678
  const url = new URL(request.url);
@@ -2628,7 +2692,9 @@ function createHoopilotHandler(options = {}) {
2628
2692
  metrics,
2629
2693
  requestId,
2630
2694
  route,
2631
- startedAt
2695
+ startedAt,
2696
+ closeConnection: bufferProxyBodies,
2697
+ trackStreamingBody: !bufferProxyBodies
2632
2698
  });
2633
2699
  const browserOrigin = forbiddenBrowserOrigin(request, apiKey);
2634
2700
  if (browserOrigin) {
@@ -2663,7 +2729,14 @@ function createHoopilotHandler(options = {}) {
2663
2729
  }
2664
2730
  if (request.method === "POST" && apiPath === "/v1/messages") {
2665
2731
  return finish(
2666
- await handleAnthropicMessages(client, metrics, recordTokens, request, requestLogger)
2732
+ await handleAnthropicMessages(
2733
+ client,
2734
+ metrics,
2735
+ recordTokens,
2736
+ request,
2737
+ requestLogger,
2738
+ bufferProxyBodies
2739
+ )
2667
2740
  );
2668
2741
  }
2669
2742
  if (request.method === "POST" && apiPath === "/v1/messages/count_tokens") {
@@ -2671,16 +2744,39 @@ function createHoopilotHandler(options = {}) {
2671
2744
  }
2672
2745
  if (request.method === "POST" && apiPath === "/v1/chat/completions") {
2673
2746
  return finish(
2674
- await handleChatCompletions(client, metrics, recordTokens, request, requestLogger)
2747
+ await handleChatCompletions(
2748
+ client,
2749
+ metrics,
2750
+ recordTokens,
2751
+ request,
2752
+ requestLogger,
2753
+ bufferProxyBodies
2754
+ )
2675
2755
  );
2676
2756
  }
2677
2757
  if (request.method === "POST" && apiPath === "/v1/completions") {
2678
2758
  return finish(
2679
- await handleCompletions(client, metrics, recordTokens, request, requestLogger)
2759
+ await handleCompletions(
2760
+ client,
2761
+ metrics,
2762
+ recordTokens,
2763
+ request,
2764
+ requestLogger,
2765
+ bufferProxyBodies
2766
+ )
2680
2767
  );
2681
2768
  }
2682
2769
  if (request.method === "POST" && apiPath === "/v1/responses") {
2683
- return finish(await handleResponses(client, metrics, recordTokens, request, requestLogger));
2770
+ return finish(
2771
+ await handleResponses(
2772
+ client,
2773
+ metrics,
2774
+ recordTokens,
2775
+ request,
2776
+ requestLogger,
2777
+ bufferProxyBodies
2778
+ )
2779
+ );
2684
2780
  }
2685
2781
  return finish(jsonError(404, "not_found", `No route for ${request.method} ${url.pathname}.`));
2686
2782
  } catch (error) {
@@ -2745,7 +2841,7 @@ function startHoopilotServer(options = {}) {
2745
2841
  url: `http://${urlHost(host)}:${server.port}`
2746
2842
  };
2747
2843
  }
2748
- async function handleAnthropicMessages(client, metrics, recordTokens, request, logger) {
2844
+ async function handleAnthropicMessages(client, metrics, recordTokens, request, logger, bufferProxyBodies) {
2749
2845
  const anthropicRequest = await readJson(request);
2750
2846
  const responsesRequest = anthropicMessagesToResponsesRequest(anthropicRequest);
2751
2847
  const upstream = await client.responses(JSON.stringify(responsesRequest), request.signal);
@@ -2756,6 +2852,13 @@ async function handleAnthropicMessages(client, metrics, recordTokens, request, l
2756
2852
  logUpstreamSuccess(logger, "/responses", upstream.status);
2757
2853
  const model = normalizeRequestedModel(responsesRequest.model);
2758
2854
  if (isStreamingResponse(upstream) && upstream.body) {
2855
+ if (bufferProxyBodies) {
2856
+ const text = await upstream.text();
2857
+ recordResponseTextUsage(text, true, model, recordTokens);
2858
+ return proxyResponse(
2859
+ responseFromText(upstream, responsesSseTextToAnthropicSseText(text, { model }))
2860
+ );
2861
+ }
2759
2862
  const observed = observeResponseUsage(upstream, model, recordTokens, request.signal);
2760
2863
  if (!observed.body) {
2761
2864
  return proxyResponse(observed);
@@ -2799,7 +2902,7 @@ async function handleModels(client, metrics, signal, logger) {
2799
2902
  logUpstreamSuccess(logger, "/models", upstream.status);
2800
2903
  return jsonResponse(normalizeModelsResponse(await upstream.json()));
2801
2904
  }
2802
- async function handleChatCompletions(client, metrics, recordTokens, request, logger) {
2905
+ async function handleChatCompletions(client, metrics, recordTokens, request, logger, bufferProxyBodies) {
2803
2906
  const chatRequest = normalizeChatCompletionRequest(await readJson(request));
2804
2907
  const upstream = await client.chatCompletions(chatRequest, request.signal);
2805
2908
  metrics.recordUpstream("/chat/completions", upstream.ok);
@@ -2808,9 +2911,17 @@ async function handleChatCompletions(client, metrics, recordTokens, request, log
2808
2911
  }
2809
2912
  logUpstreamSuccess(logger, "/chat/completions", upstream.status);
2810
2913
  const model = normalizeRequestedModel(chatRequest.model);
2811
- return proxyResponse(observeResponseUsage(upstream, model, recordTokens, request.signal));
2914
+ return proxyResponse(
2915
+ await responseWithObservedUsage(
2916
+ upstream,
2917
+ model,
2918
+ recordTokens,
2919
+ request.signal,
2920
+ bufferProxyBodies
2921
+ )
2922
+ );
2812
2923
  }
2813
- async function handleCompletions(client, metrics, recordTokens, request, logger) {
2924
+ async function handleCompletions(client, metrics, recordTokens, request, logger, bufferProxyBodies) {
2814
2925
  const body = await readJson(request);
2815
2926
  const upstream = await client.chatCompletions(
2816
2927
  completionsRequestToChatCompletion(body),
@@ -2823,6 +2934,12 @@ async function handleCompletions(client, metrics, recordTokens, request, logger)
2823
2934
  logUpstreamSuccess(logger, "/chat/completions", upstream.status);
2824
2935
  const model = normalizeRequestedModel(body.model);
2825
2936
  if (isStreamingResponse(upstream) && upstream.body) {
2937
+ if (bufferProxyBodies) {
2938
+ const upstreamText = await upstream.text();
2939
+ recordResponseTextUsage(upstreamText, true, model, recordTokens);
2940
+ const text = completionSseTextFromChatSseText(upstreamText);
2941
+ return proxyResponse(responseFromText(upstream, text));
2942
+ }
2826
2943
  return proxyResponse(
2827
2944
  observeResponseUsage(
2828
2945
  new Response(completionStreamFromChatStream(upstream.body), {
@@ -2844,7 +2961,7 @@ async function handleCompletions(client, metrics, recordTokens, request, logger)
2844
2961
  }
2845
2962
  return jsonResponse(chatCompletionToCompletion(completion));
2846
2963
  }
2847
- async function handleResponses(client, metrics, recordTokens, request, logger) {
2964
+ async function handleResponses(client, metrics, recordTokens, request, logger, bufferProxyBodies) {
2848
2965
  const body = await readJsonText(request);
2849
2966
  const upstream = await client.responses(body, request.signal);
2850
2967
  metrics.recordUpstream("/responses", upstream.ok);
@@ -2853,7 +2970,31 @@ async function handleResponses(client, metrics, recordTokens, request, logger) {
2853
2970
  }
2854
2971
  logUpstreamSuccess(logger, "/responses", upstream.status);
2855
2972
  const model = normalizeRequestedModel(asRecord(safeParseJson(body)).model);
2856
- return proxyResponse(observeResponseUsage(upstream, model, recordTokens, request.signal));
2973
+ return proxyResponse(
2974
+ await responseWithObservedUsage(
2975
+ upstream,
2976
+ model,
2977
+ recordTokens,
2978
+ request.signal,
2979
+ bufferProxyBodies
2980
+ )
2981
+ );
2982
+ }
2983
+ async function responseWithObservedUsage(response, fallbackModel, recordTokens, signal, bufferBody) {
2984
+ const isSse = isStreamingResponse(response);
2985
+ if (bufferBody && response.body) {
2986
+ const text = await response.text();
2987
+ recordResponseTextUsage(text, isSse, fallbackModel, recordTokens);
2988
+ return responseFromText(response, text);
2989
+ }
2990
+ return observeResponseUsage(response, fallbackModel, recordTokens, signal);
2991
+ }
2992
+ function responseFromText(source, text) {
2993
+ return new Response(text, {
2994
+ headers: source.headers,
2995
+ status: source.status,
2996
+ statusText: source.statusText
2997
+ });
2857
2998
  }
2858
2999
  async function proxyError(upstream, logger) {
2859
3000
  const text = await upstream.text();
@@ -3045,8 +3186,24 @@ function serverLogger(options) {
3045
3186
  }
3046
3187
  return noopLogger;
3047
3188
  }
3189
+ function resolveStreamingProxyMode(options) {
3190
+ const value = options.streamingProxyMode ?? envValue(options.env?.HOOPILOT_STREAM_MODE) ?? envValue(options.env?.HOOPILOT_STREAMING_PROXY_MODE) ?? "auto";
3191
+ if (value === "auto" || value === "buffer" || value === "live") {
3192
+ return value;
3193
+ }
3194
+ throw new Error(`Invalid stream mode: ${value}. Expected auto, live, or buffer.`);
3195
+ }
3196
+ function shouldBufferProxyBodies(mode) {
3197
+ if (mode === "buffer") {
3198
+ return true;
3199
+ }
3200
+ if (mode === "live") {
3201
+ return false;
3202
+ }
3203
+ return process.platform === "win32" && IS_STANDALONE_BINARY;
3204
+ }
3048
3205
  function finishResponse(response, options) {
3049
- const withRequestId = responseWithRequestId(response, options.requestId);
3206
+ const withRequestId = responseWithRequestId(response, options.requestId, options.closeConnection);
3050
3207
  const stream = isStreamingResponse(withRequestId);
3051
3208
  const status = withRequestId.status;
3052
3209
  const complete = () => {
@@ -3054,7 +3211,7 @@ function finishResponse(response, options) {
3054
3211
  options.metrics.observe({ durationMs, method: options.method, route: options.route, status });
3055
3212
  logRequestCompleted(options.logger, status, stream, durationMs);
3056
3213
  };
3057
- if (stream && withRequestId.body) {
3214
+ if (stream && withRequestId.body && options.trackStreamingBody) {
3058
3215
  return new Response(trackStreamCompletion(withRequestId.body, complete), {
3059
3216
  headers: withRequestId.headers,
3060
3217
  status,
@@ -3064,9 +3221,12 @@ function finishResponse(response, options) {
3064
3221
  complete();
3065
3222
  return withRequestId;
3066
3223
  }
3067
- function responseWithRequestId(response, requestId) {
3224
+ function responseWithRequestId(response, requestId, closeConnection) {
3068
3225
  const headers = new Headers(response.headers);
3069
3226
  headers.set("x-request-id", requestId);
3227
+ if (closeConnection) {
3228
+ headers.set("connection", "close");
3229
+ }
3070
3230
  return new Response(response.body, {
3071
3231
  headers,
3072
3232
  status: response.status,