@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/cli.js +234 -67
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +203 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +203 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
2466
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|