jh-web-gateway 2.1.1 → 2.3.0
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/README.md +9 -34
- package/dist/{chunk-7H2RJZN3.js → chunk-E6JMUHPA.js} +328 -125
- package/dist/chunk-E6JMUHPA.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/{tui-DIQMK2CW.js → tui-QXRXB44O.js} +313 -107
- package/dist/tui-QXRXB44O.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-7H2RJZN3.js.map +0 -1
- package/dist/tui-DIQMK2CW.js.map +0 -1
|
@@ -59,7 +59,7 @@ async function findOrOpenJhPage(browser) {
|
|
|
59
59
|
const contexts = browser.contexts();
|
|
60
60
|
const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
|
|
61
61
|
const page = await context.newPage();
|
|
62
|
-
await page.goto(JH_URL);
|
|
62
|
+
await page.goto(JH_URL, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
63
63
|
return page;
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -638,102 +638,103 @@ async function sendChatRequestInner(page, credentials, request, options, isRetry
|
|
|
638
638
|
}
|
|
639
639
|
};
|
|
640
640
|
await page.route(streamPattern, routeHandler);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (ct.includes("text/event-stream")) {
|
|
669
|
-
const reader = res.body?.getReader();
|
|
670
|
-
if (!reader) return { error: true, status: 500, statusText: "No body", body: "No SSE body", contentType: ct };
|
|
671
|
-
const decoder = new TextDecoder();
|
|
672
|
-
let text = "";
|
|
673
|
-
while (true) {
|
|
674
|
-
const { done, value } = await reader.read();
|
|
675
|
-
if (done) break;
|
|
676
|
-
text += decoder.decode(value, { stream: true });
|
|
641
|
+
let result;
|
|
642
|
+
try {
|
|
643
|
+
const postResult = await page.evaluate(
|
|
644
|
+
async ({
|
|
645
|
+
apiBase,
|
|
646
|
+
bearerToken,
|
|
647
|
+
endpointPath,
|
|
648
|
+
requestBody
|
|
649
|
+
}) => {
|
|
650
|
+
try {
|
|
651
|
+
const res = await fetch(`${apiBase}/agents/chat/${endpointPath}`, {
|
|
652
|
+
method: "POST",
|
|
653
|
+
headers: {
|
|
654
|
+
"Content-Type": "application/json",
|
|
655
|
+
Accept: "text/event-stream",
|
|
656
|
+
Authorization: `Bearer ${bearerToken}`
|
|
657
|
+
},
|
|
658
|
+
body: JSON.stringify(requestBody)
|
|
659
|
+
});
|
|
660
|
+
if (!res.ok) {
|
|
661
|
+
return {
|
|
662
|
+
error: true,
|
|
663
|
+
status: res.status,
|
|
664
|
+
statusText: res.statusText,
|
|
665
|
+
body: (await res.text()).slice(0, 2e3),
|
|
666
|
+
contentType: ""
|
|
667
|
+
};
|
|
677
668
|
}
|
|
678
|
-
|
|
669
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
670
|
+
if (ct.includes("text/event-stream")) {
|
|
671
|
+
const reader = res.body?.getReader();
|
|
672
|
+
if (!reader) return { error: true, status: 500, statusText: "No body", body: "No SSE body", contentType: ct };
|
|
673
|
+
const decoder = new TextDecoder();
|
|
674
|
+
let text = "";
|
|
675
|
+
while (true) {
|
|
676
|
+
const { done, value } = await reader.read();
|
|
677
|
+
if (done) break;
|
|
678
|
+
text += decoder.decode(value, { stream: true });
|
|
679
|
+
}
|
|
680
|
+
return { error: false, status: 200, statusText: "OK", body: text, contentType: ct };
|
|
681
|
+
}
|
|
682
|
+
const bodyText = await res.text();
|
|
683
|
+
console.log(`[gateway] POST response content-type: ${ct}, body: ${bodyText.slice(0, 500)}`);
|
|
684
|
+
return { error: false, status: 200, statusText: "OK", body: bodyText, contentType: ct };
|
|
685
|
+
} catch (err) {
|
|
686
|
+
return { error: true, status: 500, statusText: "fetch error", body: String(err), contentType: "" };
|
|
679
687
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
apiBase: JH_API_BASE,
|
|
691
|
+
bearerToken: credentials.bearerToken,
|
|
692
|
+
endpointPath: endpoint,
|
|
693
|
+
requestBody: body
|
|
685
694
|
}
|
|
686
|
-
|
|
687
|
-
{
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
endpointPath: endpoint,
|
|
691
|
-
requestBody: body
|
|
692
|
-
}
|
|
693
|
-
);
|
|
694
|
-
let result;
|
|
695
|
-
if (postResult.error) {
|
|
696
|
-
result = { error: true, status: postResult.status, statusText: postResult.statusText ?? "", body: postResult.body };
|
|
697
|
-
await page.unroute(streamPattern, routeHandler);
|
|
698
|
-
} else if (postResult.contentType.includes("text/event-stream")) {
|
|
699
|
-
result = { error: false, status: 200, statusText: "OK", body: postResult.body };
|
|
700
|
-
await page.unroute(streamPattern, routeHandler);
|
|
701
|
-
} else {
|
|
702
|
-
let streamId;
|
|
703
|
-
try {
|
|
704
|
-
streamId = JSON.parse(postResult.body).streamId;
|
|
705
|
-
} catch {
|
|
706
|
-
}
|
|
707
|
-
if (!streamId) {
|
|
695
|
+
);
|
|
696
|
+
if (postResult.error) {
|
|
697
|
+
result = { error: true, status: postResult.status, statusText: postResult.statusText ?? "", body: postResult.body };
|
|
698
|
+
} else if (postResult.contentType.includes("text/event-stream")) {
|
|
708
699
|
result = { error: false, status: 200, statusText: "OK", body: postResult.body };
|
|
709
|
-
await page.unroute(streamPattern, routeHandler);
|
|
710
700
|
} else {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
701
|
+
let streamId;
|
|
702
|
+
try {
|
|
703
|
+
streamId = JSON.parse(postResult.body).streamId;
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
if (!streamId) {
|
|
707
|
+
result = { error: false, status: 200, statusText: "OK", body: postResult.body };
|
|
708
|
+
} else {
|
|
709
|
+
const streamUrl = `${JH_API_BASE}/agents/chat/stream/${streamId}`;
|
|
710
|
+
page.evaluate(
|
|
711
|
+
async ({ url, token }) => {
|
|
712
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
713
|
+
try {
|
|
714
|
+
await fetch(url, {
|
|
715
|
+
method: "GET",
|
|
716
|
+
headers: { Accept: "text/event-stream", Authorization: `Bearer ${token}` }
|
|
717
|
+
});
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
{ url: streamUrl, token: credentials.bearerToken }
|
|
722
|
+
).catch(() => {
|
|
723
|
+
});
|
|
724
|
+
const timeout = new Promise(
|
|
725
|
+
(res) => setTimeout(() => {
|
|
726
|
+
if (!sseResolved) {
|
|
727
|
+
sseResolved = true;
|
|
728
|
+
res({ error: true, status: 408, statusText: "timeout", body: "Stream capture timed out after 120s" });
|
|
729
|
+
}
|
|
730
|
+
}, 12e4)
|
|
731
|
+
);
|
|
732
|
+
result = await Promise.race([ssePromise, timeout]);
|
|
733
|
+
}
|
|
736
734
|
}
|
|
735
|
+
} finally {
|
|
736
|
+
await page.unroute(streamPattern, routeHandler).catch(() => {
|
|
737
|
+
});
|
|
737
738
|
}
|
|
738
739
|
if (result.error) {
|
|
739
740
|
const status = result.status;
|
|
@@ -741,7 +742,7 @@ async function sendChatRequestInner(page, credentials, request, options, isRetry
|
|
|
741
742
|
if (status === 401 && !isRetry) {
|
|
742
743
|
const cdpUrl = options?.cdpUrl ?? "http://127.0.0.1:9222";
|
|
743
744
|
try {
|
|
744
|
-
await page.reload({ waitUntil: "
|
|
745
|
+
await page.reload({ waitUntil: "commit" });
|
|
745
746
|
const fresh = await captureCredentials(cdpUrl, 3e4);
|
|
746
747
|
const newCreds = {
|
|
747
748
|
bearerToken: fresh.bearerToken,
|
|
@@ -849,11 +850,12 @@ function modelsRouter(_config) {
|
|
|
849
850
|
|
|
850
851
|
// src/routes/health.ts
|
|
851
852
|
import { Hono as Hono2 } from "hono";
|
|
852
|
-
function healthRouter(config, startTime) {
|
|
853
|
+
function healthRouter(config, startTime, deps) {
|
|
853
854
|
const app = new Hono2();
|
|
854
855
|
app.get("/", (c) => {
|
|
855
856
|
const uptime = (Date.now() - startTime) / 1e3;
|
|
856
|
-
const
|
|
857
|
+
const liveCreds = deps?.getCredentials?.() ?? config.credentials;
|
|
858
|
+
const tokenExpiry = liveCreds?.bearerToken ? getTokenExpiry(liveCreds.bearerToken) || null : null;
|
|
857
859
|
const tokenExpired = tokenExpiry !== null && Date.now() / 1e3 > tokenExpiry;
|
|
858
860
|
return c.json({
|
|
859
861
|
status: "ok",
|
|
@@ -956,8 +958,11 @@ function escapeXmlAttr(s) {
|
|
|
956
958
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
957
959
|
|
|
958
960
|
// src/core/tool-parser.ts
|
|
959
|
-
var TOOL_CALL_RE = /<tool_call\s+
|
|
961
|
+
var TOOL_CALL_RE = /<tool_call\s+(?=[^>]*\bid="([^"]*)")(?=[^>]*\bname="([^"]*)")(?:[^>]*)>([\s\S]*?)<\/tool_call>/g;
|
|
962
|
+
var TOOL_RESPONSE_RE = /<tool_response\b[^>]*>[\s\S]*?<\/tool_response>/g;
|
|
960
963
|
var THINK_RE = /<think>([\s\S]*?)<\/think>/g;
|
|
964
|
+
var PARTIAL_TOOL_CALL_RE = /<tool_call\b[^>]*>(?:(?!<\/tool_call>)[\s\S])*$/;
|
|
965
|
+
var PARTIAL_TOOL_RESPONSE_RE = /<tool_response\b[^>]*>(?:(?!<\/tool_response>)[\s\S])*$/;
|
|
961
966
|
function parseToolsAndThinking(text) {
|
|
962
967
|
const toolCalls = [];
|
|
963
968
|
let thinking = null;
|
|
@@ -966,6 +971,7 @@ function parseToolsAndThinking(text) {
|
|
|
966
971
|
thinking = thinkMatches.map((m) => m[1]).join("\n");
|
|
967
972
|
}
|
|
968
973
|
let remaining = text.replace(THINK_RE, "");
|
|
974
|
+
remaining = remaining.replace(TOOL_RESPONSE_RE, "");
|
|
969
975
|
const toolMatches = [...remaining.matchAll(TOOL_CALL_RE)];
|
|
970
976
|
for (const match of toolMatches) {
|
|
971
977
|
const id = match[1];
|
|
@@ -999,6 +1005,107 @@ function toOpenAIToolCalls(calls) {
|
|
|
999
1005
|
}
|
|
1000
1006
|
}));
|
|
1001
1007
|
}
|
|
1008
|
+
var WATCHED_TAGS = ["<tool_call", "<tool_response"];
|
|
1009
|
+
var MAX_TAG_PREFIX_LEN = Math.max(...WATCHED_TAGS.map((t) => t.length));
|
|
1010
|
+
function findWatchedTagPrefixAtEnd(text) {
|
|
1011
|
+
const maxLen = Math.min(text.length, MAX_TAG_PREFIX_LEN);
|
|
1012
|
+
for (let len = maxLen; len >= 1; len--) {
|
|
1013
|
+
const tail = text.slice(-len);
|
|
1014
|
+
for (const tag of WATCHED_TAGS) {
|
|
1015
|
+
if (tag.startsWith(tail)) {
|
|
1016
|
+
return text.length - len;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return -1;
|
|
1021
|
+
}
|
|
1022
|
+
function findPartialWatchedTag(text) {
|
|
1023
|
+
if (PARTIAL_TOOL_RESPONSE_RE.test(text)) {
|
|
1024
|
+
const idx = text.search(/<tool_response\b/);
|
|
1025
|
+
if (idx >= 0) return idx;
|
|
1026
|
+
}
|
|
1027
|
+
if (PARTIAL_TOOL_CALL_RE.test(text)) {
|
|
1028
|
+
const idx = text.search(/<tool_call\b/);
|
|
1029
|
+
if (idx >= 0) return idx;
|
|
1030
|
+
}
|
|
1031
|
+
return -1;
|
|
1032
|
+
}
|
|
1033
|
+
function findUnclosedWatchedTag(text) {
|
|
1034
|
+
for (const tag of WATCHED_TAGS) {
|
|
1035
|
+
const idx = text.lastIndexOf(tag);
|
|
1036
|
+
if (idx >= 0) {
|
|
1037
|
+
const afterTag = text.slice(idx);
|
|
1038
|
+
if (!afterTag.includes(">")) {
|
|
1039
|
+
return idx;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return -1;
|
|
1044
|
+
}
|
|
1045
|
+
var StreamingToolBuffer = class {
|
|
1046
|
+
buffer = "";
|
|
1047
|
+
/**
|
|
1048
|
+
* Push a text chunk. Returns an object with:
|
|
1049
|
+
* - `text`: safe-to-emit text (outside any partial/complete tag)
|
|
1050
|
+
* - `completedCalls`: fully parsed tool calls from this chunk
|
|
1051
|
+
*/
|
|
1052
|
+
push(chunk) {
|
|
1053
|
+
this.buffer += chunk;
|
|
1054
|
+
const completedCalls = [];
|
|
1055
|
+
let safeText = "";
|
|
1056
|
+
this.buffer = this.buffer.replace(TOOL_RESPONSE_RE, (match2, offset) => {
|
|
1057
|
+
return "";
|
|
1058
|
+
});
|
|
1059
|
+
let match;
|
|
1060
|
+
const re = new RegExp(TOOL_CALL_RE.source, "g");
|
|
1061
|
+
let lastIndex = 0;
|
|
1062
|
+
while ((match = re.exec(this.buffer)) !== null) {
|
|
1063
|
+
safeText += this.buffer.slice(lastIndex, match.index);
|
|
1064
|
+
const id = match[1];
|
|
1065
|
+
const name = match[2];
|
|
1066
|
+
const rawArgs = match[3].trim();
|
|
1067
|
+
let args;
|
|
1068
|
+
try {
|
|
1069
|
+
JSON.parse(rawArgs);
|
|
1070
|
+
args = rawArgs;
|
|
1071
|
+
} catch {
|
|
1072
|
+
args = JSON.stringify(rawArgs);
|
|
1073
|
+
}
|
|
1074
|
+
completedCalls.push({ id, name, arguments: args });
|
|
1075
|
+
lastIndex = match.index + match[0].length;
|
|
1076
|
+
}
|
|
1077
|
+
const remainder = this.buffer.slice(lastIndex);
|
|
1078
|
+
const partialIdx = findPartialWatchedTag(remainder);
|
|
1079
|
+
if (partialIdx >= 0) {
|
|
1080
|
+
if (partialIdx > 0) {
|
|
1081
|
+
safeText += remainder.slice(0, partialIdx);
|
|
1082
|
+
}
|
|
1083
|
+
this.buffer = remainder.slice(partialIdx);
|
|
1084
|
+
} else {
|
|
1085
|
+
const unclosedIdx = findUnclosedWatchedTag(remainder);
|
|
1086
|
+
if (unclosedIdx >= 0) {
|
|
1087
|
+
safeText += remainder.slice(0, unclosedIdx);
|
|
1088
|
+
this.buffer = remainder.slice(unclosedIdx);
|
|
1089
|
+
} else {
|
|
1090
|
+
const prefixStart = findWatchedTagPrefixAtEnd(remainder);
|
|
1091
|
+
if (prefixStart >= 0) {
|
|
1092
|
+
safeText += remainder.slice(0, prefixStart);
|
|
1093
|
+
this.buffer = remainder.slice(prefixStart);
|
|
1094
|
+
} else {
|
|
1095
|
+
safeText += remainder;
|
|
1096
|
+
this.buffer = "";
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return { text: safeText, completedCalls };
|
|
1101
|
+
}
|
|
1102
|
+
/** Flush any remaining buffer content as raw text (end of stream). */
|
|
1103
|
+
flush() {
|
|
1104
|
+
const remaining = this.buffer;
|
|
1105
|
+
this.buffer = "";
|
|
1106
|
+
return remaining;
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1002
1109
|
|
|
1003
1110
|
// src/core/stream-translator.ts
|
|
1004
1111
|
function parseSseEvents(rawSse) {
|
|
@@ -1107,6 +1214,9 @@ function translateToStream(rawSse, model, completionId) {
|
|
|
1107
1214
|
const events = parseSseEvents(rawSse);
|
|
1108
1215
|
const chunks = [];
|
|
1109
1216
|
let lastMessageText = "";
|
|
1217
|
+
const toolBuf = new StreamingToolBuffer();
|
|
1218
|
+
let toolCallIndex = 0;
|
|
1219
|
+
let hasToolCalls = false;
|
|
1110
1220
|
chunks.push({
|
|
1111
1221
|
id,
|
|
1112
1222
|
object: "chat.completion.chunk",
|
|
@@ -1115,6 +1225,37 @@ function translateToStream(rawSse, model, completionId) {
|
|
|
1115
1225
|
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
|
|
1116
1226
|
});
|
|
1117
1227
|
let gotDeltas = false;
|
|
1228
|
+
function processDelta(rawDelta) {
|
|
1229
|
+
const { text, completedCalls } = toolBuf.push(rawDelta);
|
|
1230
|
+
if (text) {
|
|
1231
|
+
chunks.push({
|
|
1232
|
+
id,
|
|
1233
|
+
object: "chat.completion.chunk",
|
|
1234
|
+
created,
|
|
1235
|
+
model,
|
|
1236
|
+
choices: [{ index: 0, delta: { content: text }, finish_reason: null }]
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
for (const call of completedCalls) {
|
|
1240
|
+
hasToolCalls = true;
|
|
1241
|
+
const toolDelta = {
|
|
1242
|
+
index: toolCallIndex++,
|
|
1243
|
+
id: call.id,
|
|
1244
|
+
type: "function",
|
|
1245
|
+
function: {
|
|
1246
|
+
name: call.name,
|
|
1247
|
+
arguments: call.arguments
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
chunks.push({
|
|
1251
|
+
id,
|
|
1252
|
+
object: "chat.completion.chunk",
|
|
1253
|
+
created,
|
|
1254
|
+
model,
|
|
1255
|
+
choices: [{ index: 0, delta: { tool_calls: [toolDelta] }, finish_reason: null }]
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1118
1259
|
for (const ev of events) {
|
|
1119
1260
|
const { type, parsed } = resolveEventType(ev);
|
|
1120
1261
|
if (isUserEcho(parsed)) continue;
|
|
@@ -1123,13 +1264,7 @@ function translateToStream(rawSse, model, completionId) {
|
|
|
1123
1264
|
const delta = extractDeltaText(parsed);
|
|
1124
1265
|
if (delta !== null) {
|
|
1125
1266
|
gotDeltas = true;
|
|
1126
|
-
|
|
1127
|
-
id,
|
|
1128
|
-
object: "chat.completion.chunk",
|
|
1129
|
-
created,
|
|
1130
|
-
model,
|
|
1131
|
-
choices: [{ index: 0, delta: { content: delta }, finish_reason: null }]
|
|
1132
|
-
});
|
|
1267
|
+
processDelta(delta);
|
|
1133
1268
|
}
|
|
1134
1269
|
}
|
|
1135
1270
|
if (type === "message" || !type) {
|
|
@@ -1138,23 +1273,27 @@ function translateToStream(rawSse, model, completionId) {
|
|
|
1138
1273
|
const delta = msgText.slice(lastMessageText.length);
|
|
1139
1274
|
lastMessageText = msgText;
|
|
1140
1275
|
if (!gotDeltas && delta) {
|
|
1141
|
-
|
|
1142
|
-
id,
|
|
1143
|
-
object: "chat.completion.chunk",
|
|
1144
|
-
created,
|
|
1145
|
-
model,
|
|
1146
|
-
choices: [{ index: 0, delta: { content: delta }, finish_reason: null }]
|
|
1147
|
-
});
|
|
1276
|
+
processDelta(delta);
|
|
1148
1277
|
}
|
|
1149
1278
|
}
|
|
1150
1279
|
}
|
|
1151
1280
|
}
|
|
1281
|
+
const flushed = toolBuf.flush();
|
|
1282
|
+
if (flushed) {
|
|
1283
|
+
chunks.push({
|
|
1284
|
+
id,
|
|
1285
|
+
object: "chat.completion.chunk",
|
|
1286
|
+
created,
|
|
1287
|
+
model,
|
|
1288
|
+
choices: [{ index: 0, delta: { content: flushed }, finish_reason: null }]
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1152
1291
|
chunks.push({
|
|
1153
1292
|
id,
|
|
1154
1293
|
object: "chat.completion.chunk",
|
|
1155
1294
|
created,
|
|
1156
1295
|
model,
|
|
1157
|
-
choices: [{ index: 0, delta: {}, finish_reason: "stop" }]
|
|
1296
|
+
choices: [{ index: 0, delta: {}, finish_reason: hasToolCalls ? "tool_calls" : "stop" }]
|
|
1158
1297
|
});
|
|
1159
1298
|
return chunks;
|
|
1160
1299
|
}
|
|
@@ -1177,7 +1316,7 @@ function translateToCompletion(rawSse, model, completionId) {
|
|
|
1177
1316
|
object: "chat.completion",
|
|
1178
1317
|
created,
|
|
1179
1318
|
model,
|
|
1180
|
-
choices: [{ index: 0, message, finish_reason: "stop" }],
|
|
1319
|
+
choices: [{ index: 0, message, finish_reason: parsed.toolCalls.length > 0 ? "tool_calls" : "stop" }],
|
|
1181
1320
|
usage: {
|
|
1182
1321
|
prompt_tokens: promptTokens,
|
|
1183
1322
|
completion_tokens: completionTokens,
|
|
@@ -1288,11 +1427,14 @@ function chatCompletionsRouter(_config, deps) {
|
|
|
1288
1427
|
const { page, queue, release } = acquired;
|
|
1289
1428
|
const stats = pool.stats;
|
|
1290
1429
|
console.log(`[chat] Acquired page (pool: ${stats.busy}/${stats.total} busy)`);
|
|
1430
|
+
const fullPrompt = built.systemPrompt ? `${built.systemPrompt}
|
|
1431
|
+
|
|
1432
|
+
${built.prompt}` : built.prompt;
|
|
1291
1433
|
try {
|
|
1292
1434
|
const response = await queue.enqueue(
|
|
1293
1435
|
() => sendChatRequest(page, credentials, {
|
|
1294
1436
|
model,
|
|
1295
|
-
prompt:
|
|
1437
|
+
prompt: fullPrompt
|
|
1296
1438
|
})
|
|
1297
1439
|
);
|
|
1298
1440
|
if (shouldStream) {
|
|
@@ -1349,7 +1491,7 @@ function chatCompletionsRouter(_config, deps) {
|
|
|
1349
1491
|
const retryResponse = await retry.queue.enqueue(
|
|
1350
1492
|
() => sendChatRequest(retry.page, freshCreds, {
|
|
1351
1493
|
model,
|
|
1352
|
-
prompt:
|
|
1494
|
+
prompt: fullPrompt
|
|
1353
1495
|
})
|
|
1354
1496
|
);
|
|
1355
1497
|
if (shouldStream) {
|
|
@@ -1523,10 +1665,27 @@ var Logger = class {
|
|
|
1523
1665
|
};
|
|
1524
1666
|
|
|
1525
1667
|
// src/server.ts
|
|
1668
|
+
function requestTrackerMiddleware(tracker) {
|
|
1669
|
+
return async (c, next) => {
|
|
1670
|
+
const id = crypto.randomUUID();
|
|
1671
|
+
c.set("requestId", id);
|
|
1672
|
+
tracker.start(id, c.req.method, c.req.path);
|
|
1673
|
+
try {
|
|
1674
|
+
await next();
|
|
1675
|
+
tracker.end(id, c.res.status);
|
|
1676
|
+
} catch (err) {
|
|
1677
|
+
tracker.end(id, 500);
|
|
1678
|
+
throw err;
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1526
1682
|
function createServer(config, deps) {
|
|
1527
1683
|
const app = new Hono4();
|
|
1528
1684
|
const startTime = Date.now();
|
|
1529
1685
|
const logger = new Logger();
|
|
1686
|
+
if (deps?.requestTracker) {
|
|
1687
|
+
app.use("*", requestTrackerMiddleware(deps.requestTracker));
|
|
1688
|
+
}
|
|
1530
1689
|
app.use("/v1/*", authMiddleware(config));
|
|
1531
1690
|
app.use("*", async (c, next) => {
|
|
1532
1691
|
const start = Date.now();
|
|
@@ -1559,7 +1718,7 @@ function createServer(config, deps) {
|
|
|
1559
1718
|
});
|
|
1560
1719
|
});
|
|
1561
1720
|
app.route("/v1/models", modelsRouter(config));
|
|
1562
|
-
app.route("/health", healthRouter(config, startTime));
|
|
1721
|
+
app.route("/health", healthRouter(config, startTime, deps ? { getCredentials: deps.getCredentials } : void 0));
|
|
1563
1722
|
if (deps) {
|
|
1564
1723
|
app.route(
|
|
1565
1724
|
"/v1/chat/completions",
|
|
@@ -1638,14 +1797,6 @@ async function startServer(config, deps) {
|
|
|
1638
1797
|
}
|
|
1639
1798
|
console.log("Shutdown complete.");
|
|
1640
1799
|
};
|
|
1641
|
-
process.on("SIGINT", async () => {
|
|
1642
|
-
await shutdown();
|
|
1643
|
-
process.exit(0);
|
|
1644
|
-
});
|
|
1645
|
-
process.on("SIGTERM", async () => {
|
|
1646
|
-
await shutdown();
|
|
1647
|
-
process.exit(0);
|
|
1648
|
-
});
|
|
1649
1800
|
return { close: shutdown };
|
|
1650
1801
|
}
|
|
1651
1802
|
|
|
@@ -1668,7 +1819,7 @@ var RequestQueue = class {
|
|
|
1668
1819
|
return await task();
|
|
1669
1820
|
} finally {
|
|
1670
1821
|
if (this.queue.length > 0) {
|
|
1671
|
-
await new Promise((r) => setTimeout(r,
|
|
1822
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1672
1823
|
}
|
|
1673
1824
|
this.release();
|
|
1674
1825
|
}
|
|
@@ -1718,6 +1869,7 @@ var PagePool = class {
|
|
|
1718
1869
|
initPromise = null;
|
|
1719
1870
|
pagesCreating = 0;
|
|
1720
1871
|
warmedUp = false;
|
|
1872
|
+
disconnected = false;
|
|
1721
1873
|
constructor(options = {}) {
|
|
1722
1874
|
this.targetUrl = options.targetUrl ?? "https://chat.ai.jh.edu";
|
|
1723
1875
|
this.maxPages = options.maxPages ?? 1;
|
|
@@ -1731,6 +1883,11 @@ var PagePool = class {
|
|
|
1731
1883
|
}
|
|
1732
1884
|
async _doInit(browser, seedPage) {
|
|
1733
1885
|
this.browser = browser;
|
|
1886
|
+
this.disconnected = false;
|
|
1887
|
+
browser.on("disconnected", () => {
|
|
1888
|
+
console.warn("[PagePool] Browser disconnected \u2014 all pages are now invalid");
|
|
1889
|
+
this.disconnected = true;
|
|
1890
|
+
});
|
|
1734
1891
|
this.pages.push({
|
|
1735
1892
|
page: seedPage,
|
|
1736
1893
|
queue: new RequestQueue(this.maxWaitMs),
|
|
@@ -1757,6 +1914,13 @@ var PagePool = class {
|
|
|
1757
1914
|
* Call `markWarmedUp()` after the first successful request to enable scaling.
|
|
1758
1915
|
*/
|
|
1759
1916
|
async acquire() {
|
|
1917
|
+
if (this.disconnected) {
|
|
1918
|
+
throw Object.assign(
|
|
1919
|
+
new Error("Browser has disconnected. Restart the gateway to reconnect."),
|
|
1920
|
+
{ statusCode: 503 }
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
this.evictDeadPages();
|
|
1760
1924
|
let pooled = this.pages.find((p2) => !p2.inUse);
|
|
1761
1925
|
if (!pooled && this.warmedUp && this.pages.length + this.pagesCreating < this.maxPages && this.browser) {
|
|
1762
1926
|
this.pagesCreating++;
|
|
@@ -1767,6 +1931,12 @@ var PagePool = class {
|
|
|
1767
1931
|
}
|
|
1768
1932
|
}
|
|
1769
1933
|
if (!pooled) {
|
|
1934
|
+
if (this.pages.length === 0) {
|
|
1935
|
+
throw Object.assign(
|
|
1936
|
+
new Error("No healthy browser pages available. Restart the gateway."),
|
|
1937
|
+
{ statusCode: 503 }
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1770
1940
|
pooled = this.pages.reduce(
|
|
1771
1941
|
(a, b) => a.queue.pending <= b.queue.pending ? a : b
|
|
1772
1942
|
);
|
|
@@ -1781,6 +1951,9 @@ var PagePool = class {
|
|
|
1781
1951
|
if (!this.warmedUp) {
|
|
1782
1952
|
this.warmedUp = true;
|
|
1783
1953
|
console.log(`[PagePool] Warm-up complete \u2014 page scaling enabled (max ${this.maxPages})`);
|
|
1954
|
+
if (this.maxPages > 1 && this.pages.length < this.maxPages && this.browser) {
|
|
1955
|
+
this.preWarmPage();
|
|
1956
|
+
}
|
|
1784
1957
|
}
|
|
1785
1958
|
}
|
|
1786
1959
|
};
|
|
@@ -1803,7 +1976,7 @@ var PagePool = class {
|
|
|
1803
1976
|
}
|
|
1804
1977
|
const page = await context.newPage();
|
|
1805
1978
|
try {
|
|
1806
|
-
await page.goto(this.targetUrl, { waitUntil: "
|
|
1979
|
+
await page.goto(this.targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
1807
1980
|
const finalUrl = page.url();
|
|
1808
1981
|
if (!finalUrl.includes("chat.ai.jh.edu")) {
|
|
1809
1982
|
throw new Error(
|
|
@@ -1824,6 +1997,36 @@ var PagePool = class {
|
|
|
1824
1997
|
throw err;
|
|
1825
1998
|
}
|
|
1826
1999
|
}
|
|
2000
|
+
/** Evict pages that have crashed or navigated away from JH */
|
|
2001
|
+
evictDeadPages() {
|
|
2002
|
+
const before = this.pages.length;
|
|
2003
|
+
this.pages = this.pages.filter((p) => {
|
|
2004
|
+
try {
|
|
2005
|
+
if (p.page.isClosed()) return false;
|
|
2006
|
+
const url = p.page.url();
|
|
2007
|
+
if (!url.includes("chat.ai.jh.edu")) {
|
|
2008
|
+
console.warn(`[PagePool] Evicting page \u2014 navigated away: ${url}`);
|
|
2009
|
+
p.page.close().catch(() => {
|
|
2010
|
+
});
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
return true;
|
|
2014
|
+
} catch {
|
|
2015
|
+
return false;
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
const evicted = before - this.pages.length;
|
|
2019
|
+
if (evicted > 0) {
|
|
2020
|
+
console.warn(`[PagePool] Evicted ${evicted} dead/stale page(s)`);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
/** Pre-warm a new page in the background (fire-and-forget) */
|
|
2024
|
+
preWarmPage() {
|
|
2025
|
+
this.pagesCreating++;
|
|
2026
|
+
this.createPage().then(() => console.log("[PagePool] Pre-warmed a new page")).catch((err) => console.warn(`[PagePool] Pre-warm failed: ${err.message}`)).finally(() => {
|
|
2027
|
+
this.pagesCreating--;
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
1827
2030
|
/** Close all pages except the seed page */
|
|
1828
2031
|
async drain() {
|
|
1829
2032
|
const toClose = this.pages.slice(1);
|
|
@@ -2192,4 +2395,4 @@ export {
|
|
|
2192
2395
|
TokenRefresher,
|
|
2193
2396
|
ChromeManager
|
|
2194
2397
|
};
|
|
2195
|
-
//# sourceMappingURL=chunk-
|
|
2398
|
+
//# sourceMappingURL=chunk-E6JMUHPA.js.map
|