jh-web-gateway 2.2.0 → 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-Y2NMKJOG.js → chunk-E6JMUHPA.js} +82 -10
- package/dist/chunk-E6JMUHPA.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/{tui-GRDJWXQL.js → tui-QXRXB44O.js} +313 -107
- package/dist/tui-QXRXB44O.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-Y2NMKJOG.js.map +0 -1
- package/dist/tui-GRDJWXQL.js.map +0 -1
package/README.md
CHANGED
|
@@ -90,7 +90,6 @@ The header shows the gateway status at all times. Use arrow keys to navigate and
|
|
|
90
90
|
│ Main Menu │
|
|
91
91
|
│ │
|
|
92
92
|
│ > Start Gateway │
|
|
93
|
-
│ Model │
|
|
94
93
|
│ Chat │
|
|
95
94
|
│ Server Info │
|
|
96
95
|
│ Settings │
|
|
@@ -170,38 +169,9 @@ Once all phases complete the gateway is live:
|
|
|
170
169
|
|
|
171
170
|
---
|
|
172
171
|
|
|
173
|
-
### Model Selector
|
|
174
|
-
|
|
175
|
-
Choose the AI model for all requests. The currently active model is marked with a filled circle (`●`).
|
|
176
|
-
|
|
177
|
-
```text
|
|
178
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
179
|
-
│ jh-gateway ● Gateway: running │
|
|
180
|
-
├─────────────────────────────────────────────────────────────┤
|
|
181
|
-
│ │
|
|
182
|
-
│ Select Model │
|
|
183
|
-
│ │
|
|
184
|
-
│ > ● claude-opus-4.5 ← active model │
|
|
185
|
-
│ ○ claude-sonnet-4.5 │
|
|
186
|
-
│ ○ claude-haiku-4.5 │
|
|
187
|
-
│ ○ gpt-4.1 │
|
|
188
|
-
│ ○ gpt-5 │
|
|
189
|
-
│ ○ gpt-5.1 │
|
|
190
|
-
│ ○ o3 │
|
|
191
|
-
│ ○ o3-mini │
|
|
192
|
-
│ ○ llama3-3-70b-instruct │
|
|
193
|
-
│ │
|
|
194
|
-
│ [↑↓] Navigate [Enter] Select [b/Esc] Back │
|
|
195
|
-
└─────────────────────────────────────────────────────────────┘
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
The selection is saved to `~/.jh-gateway/config.json` as `defaultModel` immediately on press.
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
172
|
### Chat Panel
|
|
203
173
|
|
|
204
|
-
Send a quick test message from inside the TUI — no external tool required. The panel shows the last exchange and streams a response from the running gateway.
|
|
174
|
+
Send a quick test message from inside the TUI — no external tool required. The panel shows the last exchange and streams a response from the running gateway. Use `↑`/`↓` to cycle through models without leaving the chat.
|
|
205
175
|
|
|
206
176
|
```text
|
|
207
177
|
┌─────────────────────────────────────────────────────────────┐
|
|
@@ -218,7 +188,7 @@ Send a quick test message from inside the TUI — no external tool required. The
|
|
|
218
188
|
│ │ Type a message and press Enter… █ │ │
|
|
219
189
|
│ ╰─────────────────────────────────────────────────────╯ │
|
|
220
190
|
│ │
|
|
221
|
-
│ [Enter] Send [b/Esc] Back
|
|
191
|
+
│ [Enter] Send [↑↓] Model [b/Esc] Back │
|
|
222
192
|
└─────────────────────────────────────────────────────────────┘
|
|
223
193
|
```
|
|
224
194
|
|
|
@@ -228,6 +198,7 @@ Send a quick test message from inside the TUI — no external tool required. The
|
|
|
228
198
|
|-----|--------|
|
|
229
199
|
| Type | Compose your message |
|
|
230
200
|
| `Enter` | Send the message |
|
|
201
|
+
| `↑` / `↓` | Cycle through models |
|
|
231
202
|
| `Backspace` | Delete last character |
|
|
232
203
|
| `b` / `Esc` | Back to menu |
|
|
233
204
|
|
|
@@ -235,7 +206,7 @@ Send a quick test message from inside the TUI — no external tool required. The
|
|
|
235
206
|
|
|
236
207
|
### Server Info Panel
|
|
237
208
|
|
|
238
|
-
Displays the live base URL and
|
|
209
|
+
Displays the live base URL, API key, and real-time server activity. Use one-key shortcuts to copy connection details directly to the clipboard.
|
|
239
210
|
|
|
240
211
|
```text
|
|
241
212
|
┌─────────────────────────────────────────────────────────────┐
|
|
@@ -249,6 +220,10 @@ Displays the live base URL and API key. Use one-key shortcuts to copy them direc
|
|
|
249
220
|
│ │ API Key: jh-local-xxxxxxxxxxxxxxxxxxxxxxxx │ │
|
|
250
221
|
│ ╰───────────────────────────────────────────────────╯ │
|
|
251
222
|
│ │
|
|
223
|
+
│ Activity │
|
|
224
|
+
│ ● POST /v1/chat/completions 200 1.2s claude-opus-4.5 │
|
|
225
|
+
│ ● GET /v1/models 200 0.1s │
|
|
226
|
+
│ │
|
|
252
227
|
│ Copied URL! │
|
|
253
228
|
│ │
|
|
254
229
|
│ [c] Copy URL [k] Copy Key [b/Esc] Back │
|
|
@@ -304,7 +279,7 @@ Changes are validated and written to `~/.jh-gateway/config.json` on confirm.
|
|
|
304
279
|
|
|
305
280
|
| Key | Context | Action |
|
|
306
281
|
|-----|---------|--------|
|
|
307
|
-
| `↑` / `↓` | Menu,
|
|
282
|
+
| `↑` / `↓` | Menu, Chat, Settings | Navigate items / cycle models |
|
|
308
283
|
| `Enter` | Main Menu | Open selected panel |
|
|
309
284
|
| `Enter` | Gateway Panel | Start / Stop gateway |
|
|
310
285
|
| `Enter` | Chat | Send message |
|
|
@@ -958,9 +958,11 @@ function escapeXmlAttr(s) {
|
|
|
958
958
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
959
959
|
|
|
960
960
|
// src/core/tool-parser.ts
|
|
961
|
-
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;
|
|
962
963
|
var THINK_RE = /<think>([\s\S]*?)<\/think>/g;
|
|
963
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])*$/;
|
|
964
966
|
function parseToolsAndThinking(text) {
|
|
965
967
|
const toolCalls = [];
|
|
966
968
|
let thinking = null;
|
|
@@ -969,6 +971,7 @@ function parseToolsAndThinking(text) {
|
|
|
969
971
|
thinking = thinkMatches.map((m) => m[1]).join("\n");
|
|
970
972
|
}
|
|
971
973
|
let remaining = text.replace(THINK_RE, "");
|
|
974
|
+
remaining = remaining.replace(TOOL_RESPONSE_RE, "");
|
|
972
975
|
const toolMatches = [...remaining.matchAll(TOOL_CALL_RE)];
|
|
973
976
|
for (const match of toolMatches) {
|
|
974
977
|
const id = match[1];
|
|
@@ -1002,17 +1005,57 @@ function toOpenAIToolCalls(calls) {
|
|
|
1002
1005
|
}
|
|
1003
1006
|
}));
|
|
1004
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
|
+
}
|
|
1005
1045
|
var StreamingToolBuffer = class {
|
|
1006
1046
|
buffer = "";
|
|
1007
1047
|
/**
|
|
1008
1048
|
* Push a text chunk. Returns an object with:
|
|
1009
|
-
* - `text`: safe-to-emit text (outside any partial tag)
|
|
1049
|
+
* - `text`: safe-to-emit text (outside any partial/complete tag)
|
|
1010
1050
|
* - `completedCalls`: fully parsed tool calls from this chunk
|
|
1011
1051
|
*/
|
|
1012
1052
|
push(chunk) {
|
|
1013
1053
|
this.buffer += chunk;
|
|
1014
1054
|
const completedCalls = [];
|
|
1015
1055
|
let safeText = "";
|
|
1056
|
+
this.buffer = this.buffer.replace(TOOL_RESPONSE_RE, (match2, offset) => {
|
|
1057
|
+
return "";
|
|
1058
|
+
});
|
|
1016
1059
|
let match;
|
|
1017
1060
|
const re = new RegExp(TOOL_CALL_RE.source, "g");
|
|
1018
1061
|
let lastIndex = 0;
|
|
@@ -1032,15 +1075,27 @@ var StreamingToolBuffer = class {
|
|
|
1032
1075
|
lastIndex = match.index + match[0].length;
|
|
1033
1076
|
}
|
|
1034
1077
|
const remainder = this.buffer.slice(lastIndex);
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
if (
|
|
1038
|
-
safeText += remainder.slice(0,
|
|
1078
|
+
const partialIdx = findPartialWatchedTag(remainder);
|
|
1079
|
+
if (partialIdx >= 0) {
|
|
1080
|
+
if (partialIdx > 0) {
|
|
1081
|
+
safeText += remainder.slice(0, partialIdx);
|
|
1039
1082
|
}
|
|
1040
|
-
this.buffer =
|
|
1083
|
+
this.buffer = remainder.slice(partialIdx);
|
|
1041
1084
|
} else {
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
+
}
|
|
1044
1099
|
}
|
|
1045
1100
|
return { text: safeText, completedCalls };
|
|
1046
1101
|
}
|
|
@@ -1610,10 +1665,27 @@ var Logger = class {
|
|
|
1610
1665
|
};
|
|
1611
1666
|
|
|
1612
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
|
+
}
|
|
1613
1682
|
function createServer(config, deps) {
|
|
1614
1683
|
const app = new Hono4();
|
|
1615
1684
|
const startTime = Date.now();
|
|
1616
1685
|
const logger = new Logger();
|
|
1686
|
+
if (deps?.requestTracker) {
|
|
1687
|
+
app.use("*", requestTrackerMiddleware(deps.requestTracker));
|
|
1688
|
+
}
|
|
1617
1689
|
app.use("/v1/*", authMiddleware(config));
|
|
1618
1690
|
app.use("*", async (c, next) => {
|
|
1619
1691
|
const start = Date.now();
|
|
@@ -2323,4 +2395,4 @@ export {
|
|
|
2323
2395
|
TokenRefresher,
|
|
2324
2396
|
ChromeManager
|
|
2325
2397
|
};
|
|
2326
|
-
//# sourceMappingURL=chunk-
|
|
2398
|
+
//# sourceMappingURL=chunk-E6JMUHPA.js.map
|