@openhoo/hoopilot 0.8.3 → 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.d.cts CHANGED
@@ -48,6 +48,7 @@ interface Logger {
48
48
  type LogFields = Record<string, unknown>;
49
49
  type LogFormat = "json" | "pretty";
50
50
  type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal" | "silent";
51
+ type StreamingProxyMode = "auto" | "buffer" | "live";
51
52
  interface LogMethod {
52
53
  (message: string): void;
53
54
  (fields: LogFields, message: string): void;
@@ -93,6 +94,7 @@ interface HoopilotServerOptions extends CopilotAuthOptions {
93
94
  logLevel?: LogLevel | string;
94
95
  metrics?: MetricsRegistry;
95
96
  port?: number;
97
+ streamingProxyMode?: StreamingProxyMode | string;
96
98
  }
97
99
  interface StartedHoopilotServer {
98
100
  server: Bun.Server<undefined>;
package/dist/index.d.ts CHANGED
@@ -48,6 +48,7 @@ interface Logger {
48
48
  type LogFields = Record<string, unknown>;
49
49
  type LogFormat = "json" | "pretty";
50
50
  type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal" | "silent";
51
+ type StreamingProxyMode = "auto" | "buffer" | "live";
51
52
  interface LogMethod {
52
53
  (message: string): void;
53
54
  (fields: LogFields, message: string): void;
@@ -93,6 +94,7 @@ interface HoopilotServerOptions extends CopilotAuthOptions {
93
94
  logLevel?: LogLevel | string;
94
95
  metrics?: MetricsRegistry;
95
96
  port?: number;
97
+ streamingProxyMode?: StreamingProxyMode | string;
96
98
  }
97
99
  interface StartedHoopilotServer {
98
100
  server: Bun.Server<undefined>;
package/dist/index.js CHANGED
@@ -201,6 +201,25 @@ function completionStreamFromChatStream(chatStream) {
201
201
  }
202
202
  });
203
203
  }
204
+ function completionSseTextFromChatSseText(text) {
205
+ const chunks = [];
206
+ let sawTerminalEvent = false;
207
+ const enqueue = (data) => {
208
+ chunks.push(encodeDataSse(data));
209
+ };
210
+ const markTerminal = () => {
211
+ sawTerminalEvent = true;
212
+ };
213
+ for (const block of text.split(/\r?\n\r?\n/)) {
214
+ if (block.trim()) {
215
+ processCompletionSseBlock(block, enqueue, markTerminal);
216
+ }
217
+ }
218
+ if (!sawTerminalEvent) {
219
+ enqueue("[DONE]");
220
+ }
221
+ return chunks.join("");
222
+ }
204
223
  function normalizeModelsResponse(upstream) {
205
224
  const record = asRecord(upstream);
206
225
  const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
@@ -919,16 +938,7 @@ function responsesStreamToAnthropicStream(stream, options) {
919
938
  const decoder = new TextDecoder();
920
939
  const encoder = new TextEncoder();
921
940
  let buffer = "";
922
- const state = {
923
- blocks: /* @__PURE__ */ new Map(),
924
- completed: false,
925
- messageId: options.messageId ?? `msg_${randomId2()}`,
926
- model: options.model,
927
- nextBlockIndex: 0,
928
- sawToolUse: false,
929
- started: false,
930
- usage: anthropicUsage(void 0)
931
- };
941
+ const state = createAnthropicStreamState(options);
932
942
  return new ReadableStream({
933
943
  async start(controller) {
934
944
  const enqueue = (event, data) => {
@@ -964,6 +974,20 @@ function responsesStreamToAnthropicStream(stream, options) {
964
974
  }
965
975
  });
966
976
  }
977
+ function responsesSseTextToAnthropicSseText(text, options) {
978
+ const chunks = [];
979
+ const state = createAnthropicStreamState(options);
980
+ const enqueue = (event, data) => {
981
+ chunks.push(encodeSse2(event, data));
982
+ };
983
+ for (const block of text.split(/\r?\n\r?\n/)) {
984
+ if (block.trim()) {
985
+ processResponsesSseBlock(block, state, enqueue);
986
+ }
987
+ }
988
+ finishAnthropicStream(state, enqueue);
989
+ return chunks.join("");
990
+ }
967
991
  function estimateAnthropicMessageTokens(request) {
968
992
  const chars = estimatedTextSize(request.system) + estimatedTextSize(request.messages) + estimatedTextSize(request.tools) + estimatedTextSize(request.tool_choice) + estimatedTextSize(request.thinking);
969
993
  const messageCount = Array.isArray(request.messages) ? request.messages.length : 1;
@@ -974,6 +998,18 @@ function estimateAnthropicMessageTokens(request) {
974
998
  total_tokens: inputTokens
975
999
  };
976
1000
  }
1001
+ function createAnthropicStreamState(options) {
1002
+ return {
1003
+ blocks: /* @__PURE__ */ new Map(),
1004
+ completed: false,
1005
+ messageId: options.messageId ?? `msg_${randomId2()}`,
1006
+ model: options.model,
1007
+ nextBlockIndex: 0,
1008
+ sawToolUse: false,
1009
+ started: false,
1010
+ usage: anthropicUsage(void 0)
1011
+ };
1012
+ }
977
1013
  function anthropicMessagesToResponsesInput(messages) {
978
1014
  if (!Array.isArray(messages)) {
979
1015
  throw new AnthropicCompatibilityError("Anthropic Messages requests require messages[].");
@@ -2391,6 +2427,20 @@ function observeResponseUsage(response, fallbackModel, onUsage, signal) {
2391
2427
  statusText: response.statusText
2392
2428
  });
2393
2429
  }
2430
+ function recordResponseTextUsage(text, isSse, fallbackModel, onUsage) {
2431
+ const accumulator = createUsageAccumulator(fallbackModel, onUsage);
2432
+ if (isSse) {
2433
+ for (const line of text.split(/\r?\n/)) {
2434
+ considerSseLine(line, accumulator.consider);
2435
+ }
2436
+ } else {
2437
+ const parsed = safeParse(text);
2438
+ if (parsed !== void 0) {
2439
+ accumulator.consider(parsed);
2440
+ }
2441
+ }
2442
+ accumulator.finish();
2443
+ }
2394
2444
  async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal) {
2395
2445
  const reader = stream.getReader();
2396
2446
  const onAbort = () => {
@@ -2404,22 +2454,10 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal) {
2404
2454
  signal?.addEventListener("abort", onAbort, { once: true });
2405
2455
  }
2406
2456
  const decoder = new TextDecoder();
2407
- let model = fallbackModel;
2408
- let usage;
2457
+ const accumulator = createUsageAccumulator(fallbackModel, onUsage);
2409
2458
  let buffer = "";
2410
2459
  let bufferedBytes = 0;
2411
2460
  let overflowed = false;
2412
- const consider = (payload) => {
2413
- const record = asRecord(payload);
2414
- const found = extractTokenUsage(record.usage) ?? extractTokenUsage(asRecord(record.response).usage);
2415
- if (found) {
2416
- usage = found;
2417
- }
2418
- const candidateModel = modelText(record.model) || modelText(asRecord(record.response).model);
2419
- if (candidateModel) {
2420
- model = candidateModel;
2421
- }
2422
- };
2423
2461
  try {
2424
2462
  while (true) {
2425
2463
  const result = await reader.read();
@@ -2432,7 +2470,7 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal) {
2432
2470
  const lines = buffer.split(/\r?\n/);
2433
2471
  buffer = lines.pop() ?? "";
2434
2472
  for (const line of lines) {
2435
- considerSseLine(line, consider);
2473
+ considerSseLine(line, accumulator.consider);
2436
2474
  }
2437
2475
  if (buffer.length > USAGE_BUFFER_LIMIT_BYTES) {
2438
2476
  buffer = "";
@@ -2450,21 +2488,41 @@ async function consumeUsage(stream, isSse, fallbackModel, onUsage, signal) {
2450
2488
  const finalBuffer = buffer + decoder.decode();
2451
2489
  if (isSse) {
2452
2490
  if (finalBuffer) {
2453
- considerSseLine(finalBuffer, consider);
2491
+ considerSseLine(finalBuffer, accumulator.consider);
2454
2492
  }
2455
2493
  } else if (!overflowed && finalBuffer) {
2456
2494
  const parsed = safeParse(finalBuffer);
2457
2495
  if (parsed !== void 0) {
2458
- consider(parsed);
2496
+ accumulator.consider(parsed);
2459
2497
  }
2460
2498
  }
2461
2499
  } finally {
2462
2500
  signal?.removeEventListener("abort", onAbort);
2463
2501
  reader.releaseLock();
2464
2502
  }
2465
- if (usage) {
2466
- onUsage(model, usage);
2467
- }
2503
+ accumulator.finish();
2504
+ }
2505
+ function createUsageAccumulator(fallbackModel, onUsage) {
2506
+ let model = fallbackModel;
2507
+ let usage;
2508
+ return {
2509
+ consider(payload) {
2510
+ const record = asRecord(payload);
2511
+ const found = extractTokenUsage(record.usage) ?? extractTokenUsage(asRecord(record.response).usage);
2512
+ if (found) {
2513
+ usage = found;
2514
+ }
2515
+ const candidateModel = modelText(record.model) || modelText(asRecord(record.response).model);
2516
+ if (candidateModel) {
2517
+ model = candidateModel;
2518
+ }
2519
+ },
2520
+ finish() {
2521
+ if (usage) {
2522
+ onUsage(model, usage);
2523
+ }
2524
+ }
2525
+ };
2468
2526
  }
2469
2527
  function considerSseLine(line, consider) {
2470
2528
  const trimmed = line.trim();
@@ -2511,6 +2569,10 @@ function formatNumber(value) {
2511
2569
  return Number.isInteger(value) ? value.toString() : String(value);
2512
2570
  }
2513
2571
 
2572
+ // src/version.ts
2573
+ var BAKED_VERSION = typeof HOOPILOT_VERSION !== "undefined" ? HOOPILOT_VERSION : void 0;
2574
+ var IS_STANDALONE_BINARY = BAKED_VERSION !== void 0;
2575
+
2514
2576
  // src/server.ts
2515
2577
  var DEFAULT_HOST = "127.0.0.1";
2516
2578
  var DEFAULT_PORT = 4141;
@@ -2534,6 +2596,8 @@ function createHoopilotHandler(options = {}) {
2534
2596
  const metrics = options.metrics ?? new MetricsRegistry();
2535
2597
  const readUsage = createUsageReader(client, metrics);
2536
2598
  const recordTokens = (model, usage) => metrics.recordTokens(model, usage);
2599
+ const streamingProxyMode = resolveStreamingProxyMode(options);
2600
+ const bufferProxyBodies = shouldBufferProxyBodies(streamingProxyMode);
2537
2601
  return async (request) => {
2538
2602
  const startedAt = performance.now();
2539
2603
  const url = new URL(request.url);
@@ -2553,7 +2617,9 @@ function createHoopilotHandler(options = {}) {
2553
2617
  metrics,
2554
2618
  requestId,
2555
2619
  route,
2556
- startedAt
2620
+ startedAt,
2621
+ closeConnection: bufferProxyBodies,
2622
+ trackStreamingBody: !bufferProxyBodies
2557
2623
  });
2558
2624
  const browserOrigin = forbiddenBrowserOrigin(request, apiKey);
2559
2625
  if (browserOrigin) {
@@ -2588,7 +2654,14 @@ function createHoopilotHandler(options = {}) {
2588
2654
  }
2589
2655
  if (request.method === "POST" && apiPath === "/v1/messages") {
2590
2656
  return finish(
2591
- await handleAnthropicMessages(client, metrics, recordTokens, request, requestLogger)
2657
+ await handleAnthropicMessages(
2658
+ client,
2659
+ metrics,
2660
+ recordTokens,
2661
+ request,
2662
+ requestLogger,
2663
+ bufferProxyBodies
2664
+ )
2592
2665
  );
2593
2666
  }
2594
2667
  if (request.method === "POST" && apiPath === "/v1/messages/count_tokens") {
@@ -2596,16 +2669,39 @@ function createHoopilotHandler(options = {}) {
2596
2669
  }
2597
2670
  if (request.method === "POST" && apiPath === "/v1/chat/completions") {
2598
2671
  return finish(
2599
- await handleChatCompletions(client, metrics, recordTokens, request, requestLogger)
2672
+ await handleChatCompletions(
2673
+ client,
2674
+ metrics,
2675
+ recordTokens,
2676
+ request,
2677
+ requestLogger,
2678
+ bufferProxyBodies
2679
+ )
2600
2680
  );
2601
2681
  }
2602
2682
  if (request.method === "POST" && apiPath === "/v1/completions") {
2603
2683
  return finish(
2604
- await handleCompletions(client, metrics, recordTokens, request, requestLogger)
2684
+ await handleCompletions(
2685
+ client,
2686
+ metrics,
2687
+ recordTokens,
2688
+ request,
2689
+ requestLogger,
2690
+ bufferProxyBodies
2691
+ )
2605
2692
  );
2606
2693
  }
2607
2694
  if (request.method === "POST" && apiPath === "/v1/responses") {
2608
- return finish(await handleResponses(client, metrics, recordTokens, request, requestLogger));
2695
+ return finish(
2696
+ await handleResponses(
2697
+ client,
2698
+ metrics,
2699
+ recordTokens,
2700
+ request,
2701
+ requestLogger,
2702
+ bufferProxyBodies
2703
+ )
2704
+ );
2609
2705
  }
2610
2706
  return finish(jsonError(404, "not_found", `No route for ${request.method} ${url.pathname}.`));
2611
2707
  } catch (error) {
@@ -2670,7 +2766,7 @@ function startHoopilotServer(options = {}) {
2670
2766
  url: `http://${urlHost(host)}:${server.port}`
2671
2767
  };
2672
2768
  }
2673
- async function handleAnthropicMessages(client, metrics, recordTokens, request, logger) {
2769
+ async function handleAnthropicMessages(client, metrics, recordTokens, request, logger, bufferProxyBodies) {
2674
2770
  const anthropicRequest = await readJson(request);
2675
2771
  const responsesRequest = anthropicMessagesToResponsesRequest(anthropicRequest);
2676
2772
  const upstream = await client.responses(JSON.stringify(responsesRequest), request.signal);
@@ -2681,6 +2777,13 @@ async function handleAnthropicMessages(client, metrics, recordTokens, request, l
2681
2777
  logUpstreamSuccess(logger, "/responses", upstream.status);
2682
2778
  const model = normalizeRequestedModel(responsesRequest.model);
2683
2779
  if (isStreamingResponse(upstream) && upstream.body) {
2780
+ if (bufferProxyBodies) {
2781
+ const text = await upstream.text();
2782
+ recordResponseTextUsage(text, true, model, recordTokens);
2783
+ return proxyResponse(
2784
+ responseFromText(upstream, responsesSseTextToAnthropicSseText(text, { model }))
2785
+ );
2786
+ }
2684
2787
  const observed = observeResponseUsage(upstream, model, recordTokens, request.signal);
2685
2788
  if (!observed.body) {
2686
2789
  return proxyResponse(observed);
@@ -2724,7 +2827,7 @@ async function handleModels(client, metrics, signal, logger) {
2724
2827
  logUpstreamSuccess(logger, "/models", upstream.status);
2725
2828
  return jsonResponse(normalizeModelsResponse(await upstream.json()));
2726
2829
  }
2727
- async function handleChatCompletions(client, metrics, recordTokens, request, logger) {
2830
+ async function handleChatCompletions(client, metrics, recordTokens, request, logger, bufferProxyBodies) {
2728
2831
  const chatRequest = normalizeChatCompletionRequest(await readJson(request));
2729
2832
  const upstream = await client.chatCompletions(chatRequest, request.signal);
2730
2833
  metrics.recordUpstream("/chat/completions", upstream.ok);
@@ -2733,9 +2836,17 @@ async function handleChatCompletions(client, metrics, recordTokens, request, log
2733
2836
  }
2734
2837
  logUpstreamSuccess(logger, "/chat/completions", upstream.status);
2735
2838
  const model = normalizeRequestedModel(chatRequest.model);
2736
- return proxyResponse(observeResponseUsage(upstream, model, recordTokens, request.signal));
2839
+ return proxyResponse(
2840
+ await responseWithObservedUsage(
2841
+ upstream,
2842
+ model,
2843
+ recordTokens,
2844
+ request.signal,
2845
+ bufferProxyBodies
2846
+ )
2847
+ );
2737
2848
  }
2738
- async function handleCompletions(client, metrics, recordTokens, request, logger) {
2849
+ async function handleCompletions(client, metrics, recordTokens, request, logger, bufferProxyBodies) {
2739
2850
  const body = await readJson(request);
2740
2851
  const upstream = await client.chatCompletions(
2741
2852
  completionsRequestToChatCompletion(body),
@@ -2748,6 +2859,12 @@ async function handleCompletions(client, metrics, recordTokens, request, logger)
2748
2859
  logUpstreamSuccess(logger, "/chat/completions", upstream.status);
2749
2860
  const model = normalizeRequestedModel(body.model);
2750
2861
  if (isStreamingResponse(upstream) && upstream.body) {
2862
+ if (bufferProxyBodies) {
2863
+ const upstreamText = await upstream.text();
2864
+ recordResponseTextUsage(upstreamText, true, model, recordTokens);
2865
+ const text = completionSseTextFromChatSseText(upstreamText);
2866
+ return proxyResponse(responseFromText(upstream, text));
2867
+ }
2751
2868
  return proxyResponse(
2752
2869
  observeResponseUsage(
2753
2870
  new Response(completionStreamFromChatStream(upstream.body), {
@@ -2769,7 +2886,7 @@ async function handleCompletions(client, metrics, recordTokens, request, logger)
2769
2886
  }
2770
2887
  return jsonResponse(chatCompletionToCompletion(completion));
2771
2888
  }
2772
- async function handleResponses(client, metrics, recordTokens, request, logger) {
2889
+ async function handleResponses(client, metrics, recordTokens, request, logger, bufferProxyBodies) {
2773
2890
  const body = await readJsonText(request);
2774
2891
  const upstream = await client.responses(body, request.signal);
2775
2892
  metrics.recordUpstream("/responses", upstream.ok);
@@ -2778,7 +2895,31 @@ async function handleResponses(client, metrics, recordTokens, request, logger) {
2778
2895
  }
2779
2896
  logUpstreamSuccess(logger, "/responses", upstream.status);
2780
2897
  const model = normalizeRequestedModel(asRecord(safeParseJson(body)).model);
2781
- return proxyResponse(observeResponseUsage(upstream, model, recordTokens, request.signal));
2898
+ return proxyResponse(
2899
+ await responseWithObservedUsage(
2900
+ upstream,
2901
+ model,
2902
+ recordTokens,
2903
+ request.signal,
2904
+ bufferProxyBodies
2905
+ )
2906
+ );
2907
+ }
2908
+ async function responseWithObservedUsage(response, fallbackModel, recordTokens, signal, bufferBody) {
2909
+ const isSse = isStreamingResponse(response);
2910
+ if (bufferBody && response.body) {
2911
+ const text = await response.text();
2912
+ recordResponseTextUsage(text, isSse, fallbackModel, recordTokens);
2913
+ return responseFromText(response, text);
2914
+ }
2915
+ return observeResponseUsage(response, fallbackModel, recordTokens, signal);
2916
+ }
2917
+ function responseFromText(source, text) {
2918
+ return new Response(text, {
2919
+ headers: source.headers,
2920
+ status: source.status,
2921
+ statusText: source.statusText
2922
+ });
2782
2923
  }
2783
2924
  async function proxyError(upstream, logger) {
2784
2925
  const text = await upstream.text();
@@ -2970,8 +3111,24 @@ function serverLogger(options) {
2970
3111
  }
2971
3112
  return noopLogger;
2972
3113
  }
3114
+ function resolveStreamingProxyMode(options) {
3115
+ const value = options.streamingProxyMode ?? envValue(options.env?.HOOPILOT_STREAM_MODE) ?? envValue(options.env?.HOOPILOT_STREAMING_PROXY_MODE) ?? "auto";
3116
+ if (value === "auto" || value === "buffer" || value === "live") {
3117
+ return value;
3118
+ }
3119
+ throw new Error(`Invalid stream mode: ${value}. Expected auto, live, or buffer.`);
3120
+ }
3121
+ function shouldBufferProxyBodies(mode) {
3122
+ if (mode === "buffer") {
3123
+ return true;
3124
+ }
3125
+ if (mode === "live") {
3126
+ return false;
3127
+ }
3128
+ return process.platform === "win32" && IS_STANDALONE_BINARY;
3129
+ }
2973
3130
  function finishResponse(response, options) {
2974
- const withRequestId = responseWithRequestId(response, options.requestId);
3131
+ const withRequestId = responseWithRequestId(response, options.requestId, options.closeConnection);
2975
3132
  const stream = isStreamingResponse(withRequestId);
2976
3133
  const status = withRequestId.status;
2977
3134
  const complete = () => {
@@ -2979,7 +3136,7 @@ function finishResponse(response, options) {
2979
3136
  options.metrics.observe({ durationMs, method: options.method, route: options.route, status });
2980
3137
  logRequestCompleted(options.logger, status, stream, durationMs);
2981
3138
  };
2982
- if (stream && withRequestId.body) {
3139
+ if (stream && withRequestId.body && options.trackStreamingBody) {
2983
3140
  return new Response(trackStreamCompletion(withRequestId.body, complete), {
2984
3141
  headers: withRequestId.headers,
2985
3142
  status,
@@ -2989,9 +3146,12 @@ function finishResponse(response, options) {
2989
3146
  complete();
2990
3147
  return withRequestId;
2991
3148
  }
2992
- function responseWithRequestId(response, requestId) {
3149
+ function responseWithRequestId(response, requestId, closeConnection) {
2993
3150
  const headers = new Headers(response.headers);
2994
3151
  headers.set("x-request-id", requestId);
3152
+ if (closeConnection) {
3153
+ headers.set("connection", "close");
3154
+ }
2995
3155
  return new Response(response.body, {
2996
3156
  headers,
2997
3157
  status: response.status,