@khanglvm/llm-router 2.0.0-beta.1 → 2.0.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/CHANGELOG.md +27 -0
- package/README.md +163 -426
- package/package.json +3 -3
- package/src/cli/router-module.js +2773 -2587
- package/src/cli-entry.js +32 -103
- package/src/node/activity-log.js +119 -0
- package/src/node/coding-tool-config.js +85 -11
- package/src/node/config-workflows.js +51 -12
- package/src/node/instance-state.js +1 -1
- package/src/node/litellm-context-catalog.js +184 -0
- package/src/node/local-server.js +23 -3
- package/src/node/port-reclaim.js +2 -2
- package/src/node/start-command.js +22 -22
- package/src/node/startup-manager.js +3 -3
- package/src/node/web-command.js +1 -1
- package/src/node/web-console-assets.js +1 -1
- package/src/node/web-console-client.js +34 -29
- package/src/node/web-console-server.js +420 -38
- package/src/node/web-console-styles.generated.js +1 -1
- package/src/node/web-console-ui/buffered-text-input.js +133 -0
- package/src/node/web-console-ui/config-editor-utils.js +57 -4
- package/src/node/web-console-ui/dropdown-placement.js +153 -0
- package/src/node/web-console-ui/select-search-utils.js +6 -0
- package/src/node/web-console-ui/transient-integer-input-utils.js +12 -0
- package/src/runtime/balancer.js +78 -1
- package/src/runtime/codex-request-transformer.js +16 -7
- package/src/runtime/config.js +448 -12
- package/src/runtime/handler/amp-response.js +5 -3
- package/src/runtime/handler/amp-web-search.js +2232 -0
- package/src/runtime/handler/fallback.js +30 -2
- package/src/runtime/handler/provider-call.js +353 -36
- package/src/runtime/handler/provider-translation.js +14 -0
- package/src/runtime/handler/request.js +128 -2
- package/src/runtime/handler/route-debug.js +36 -0
- package/src/runtime/handler.js +210 -20
- package/src/runtime/subscription-provider.js +1 -1
- package/src/shared/coding-tool-bindings.js +49 -0
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/request/claude-to-openai.js +43 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_VIEWPORT_PADDING = 12;
|
|
4
|
+
|
|
5
|
+
function isScrollableOverflow(value) {
|
|
6
|
+
return /auto|scroll|hidden|clip|overlay/.test(String(value || "").toLowerCase());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function intersectRects(baseRect, nextRect) {
|
|
10
|
+
return {
|
|
11
|
+
top: Math.max(baseRect.top, nextRect.top),
|
|
12
|
+
right: Math.min(baseRect.right, nextRect.right),
|
|
13
|
+
bottom: Math.min(baseRect.bottom, nextRect.bottom),
|
|
14
|
+
left: Math.max(baseRect.left, nextRect.left)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getClippingAncestors(node) {
|
|
19
|
+
if (typeof window === "undefined") return [];
|
|
20
|
+
|
|
21
|
+
const ancestors = [];
|
|
22
|
+
let current = node?.parentElement || null;
|
|
23
|
+
|
|
24
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
25
|
+
const style = window.getComputedStyle(current);
|
|
26
|
+
if (
|
|
27
|
+
isScrollableOverflow(style.overflow)
|
|
28
|
+
|| isScrollableOverflow(style.overflowY)
|
|
29
|
+
|| isScrollableOverflow(style.overflowX)
|
|
30
|
+
) {
|
|
31
|
+
ancestors.push(current);
|
|
32
|
+
}
|
|
33
|
+
current = current.parentElement;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return ancestors;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getDropdownBoundaryRect(node, { viewportPadding = DEFAULT_VIEWPORT_PADDING } = {}) {
|
|
40
|
+
if (typeof window === "undefined") return null;
|
|
41
|
+
|
|
42
|
+
let boundaryRect = {
|
|
43
|
+
top: viewportPadding,
|
|
44
|
+
right: window.innerWidth - viewportPadding,
|
|
45
|
+
bottom: window.innerHeight - viewportPadding,
|
|
46
|
+
left: viewportPadding
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const ancestor of getClippingAncestors(node)) {
|
|
50
|
+
boundaryRect = intersectRects(boundaryRect, ancestor.getBoundingClientRect());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return boundaryRect;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function calculateDropdownPlacement({
|
|
57
|
+
anchorRect,
|
|
58
|
+
boundaryRect,
|
|
59
|
+
preferredSide = "bottom",
|
|
60
|
+
offset = 4,
|
|
61
|
+
desiredHeight = 288
|
|
62
|
+
} = {}) {
|
|
63
|
+
if (!anchorRect || !boundaryRect) {
|
|
64
|
+
return {
|
|
65
|
+
side: preferredSide === "top" ? "top" : "bottom",
|
|
66
|
+
maxHeight: desiredHeight
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const spaceAbove = Math.max(0, anchorRect.top - boundaryRect.top - offset);
|
|
71
|
+
const spaceBelow = Math.max(0, boundaryRect.bottom - anchorRect.bottom - offset);
|
|
72
|
+
const resolvedPreferredSide = preferredSide === "top" ? "top" : "bottom";
|
|
73
|
+
const side = spaceAbove === spaceBelow
|
|
74
|
+
? resolvedPreferredSide
|
|
75
|
+
: spaceAbove > spaceBelow
|
|
76
|
+
? "top"
|
|
77
|
+
: "bottom";
|
|
78
|
+
const maxHeight = Math.max(
|
|
79
|
+
0,
|
|
80
|
+
Math.min(
|
|
81
|
+
desiredHeight,
|
|
82
|
+
side === "top" ? spaceAbove : spaceBelow
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
side,
|
|
88
|
+
maxHeight
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function useDropdownPlacement({
|
|
93
|
+
open = false,
|
|
94
|
+
anchorRef,
|
|
95
|
+
preferredSide = "bottom",
|
|
96
|
+
offset = 4,
|
|
97
|
+
desiredHeight = 288
|
|
98
|
+
} = {}) {
|
|
99
|
+
const [placement, setPlacement] = useState(() => ({
|
|
100
|
+
side: preferredSide === "top" ? "top" : "bottom",
|
|
101
|
+
maxHeight: desiredHeight
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
useLayoutEffect(() => {
|
|
105
|
+
if (!open || typeof window === "undefined") return undefined;
|
|
106
|
+
|
|
107
|
+
const anchorNode = anchorRef?.current || null;
|
|
108
|
+
if (!anchorNode) {
|
|
109
|
+
setPlacement({
|
|
110
|
+
side: preferredSide === "top" ? "top" : "bottom",
|
|
111
|
+
maxHeight: desiredHeight
|
|
112
|
+
});
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const updatePlacement = () => {
|
|
117
|
+
setPlacement(calculateDropdownPlacement({
|
|
118
|
+
anchorRect: anchorNode.getBoundingClientRect(),
|
|
119
|
+
boundaryRect: getDropdownBoundaryRect(anchorNode),
|
|
120
|
+
preferredSide,
|
|
121
|
+
offset,
|
|
122
|
+
desiredHeight
|
|
123
|
+
}));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
updatePlacement();
|
|
127
|
+
|
|
128
|
+
const ancestors = getClippingAncestors(anchorNode);
|
|
129
|
+
window.addEventListener("resize", updatePlacement);
|
|
130
|
+
for (const ancestor of ancestors) {
|
|
131
|
+
ancestor.addEventListener("scroll", updatePlacement, { passive: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let resizeObserver = null;
|
|
135
|
+
if (typeof ResizeObserver === "function") {
|
|
136
|
+
resizeObserver = new ResizeObserver(updatePlacement);
|
|
137
|
+
resizeObserver.observe(anchorNode);
|
|
138
|
+
for (const ancestor of ancestors) {
|
|
139
|
+
resizeObserver.observe(ancestor);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return () => {
|
|
144
|
+
window.removeEventListener("resize", updatePlacement);
|
|
145
|
+
for (const ancestor of ancestors) {
|
|
146
|
+
ancestor.removeEventListener("scroll", updatePlacement);
|
|
147
|
+
}
|
|
148
|
+
resizeObserver?.disconnect?.();
|
|
149
|
+
};
|
|
150
|
+
}, [open, anchorRef, preferredSide, offset, desiredHeight]);
|
|
151
|
+
|
|
152
|
+
return placement;
|
|
153
|
+
}
|
|
@@ -6,6 +6,12 @@ export function hasSelectSearchQuery(value) {
|
|
|
6
6
|
return normalizeSelectSearchText(value).length > 0;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export function shouldShowSelectSearchInput({
|
|
10
|
+
searchEnabled = true,
|
|
11
|
+
} = {}) {
|
|
12
|
+
return Boolean(searchEnabled);
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export function getSelectSearchKey(event) {
|
|
10
16
|
const key = String(event?.key || "");
|
|
11
17
|
if (!key || key.length !== 1) return "";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const TRANSIENT_INTEGER_INPUT_PATTERN = /^\d*$/;
|
|
2
|
+
|
|
3
|
+
export function classifyTransientIntegerInput(rawValue) {
|
|
4
|
+
const nextValue = String(rawValue ?? "");
|
|
5
|
+
const accepted = TRANSIENT_INTEGER_INPUT_PATTERN.test(nextValue);
|
|
6
|
+
return {
|
|
7
|
+
accepted,
|
|
8
|
+
draftValue: accepted ? nextValue : "",
|
|
9
|
+
shouldCommit: accepted && nextValue !== "",
|
|
10
|
+
commitValue: accepted && nextValue !== "" ? nextValue : ""
|
|
11
|
+
};
|
|
12
|
+
}
|
package/src/runtime/balancer.js
CHANGED
|
@@ -55,6 +55,66 @@ function resolveCandidateWeight(candidate) {
|
|
|
55
55
|
return Math.floor(parsed);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function resolveCandidateContextWindow(candidate) {
|
|
59
|
+
const raw = candidate?.contextWindow ?? candidate?.model?.contextWindow;
|
|
60
|
+
const parsed = Number(raw);
|
|
61
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
|
62
|
+
return Math.floor(parsed);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function shouldApplyContextAwareOrdering(route, estimatedRequiredTokens) {
|
|
66
|
+
if (!Number.isFinite(estimatedRequiredTokens) || estimatedRequiredTokens <= 0) return false;
|
|
67
|
+
return Boolean(route?.routeStrategy || route?.routeType === "alias");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function partitionEligibleEntriesByContextWindow(eligibleEntries, estimatedRequiredTokens) {
|
|
71
|
+
if (!Array.isArray(eligibleEntries) || eligibleEntries.length <= 1) {
|
|
72
|
+
return {
|
|
73
|
+
prioritizedEntries: [...(eligibleEntries || [])],
|
|
74
|
+
deferredEntries: []
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fittingKnown = [];
|
|
79
|
+
const unknownWindow = [];
|
|
80
|
+
const tooSmallKnown = [];
|
|
81
|
+
|
|
82
|
+
for (const entry of eligibleEntries) {
|
|
83
|
+
const contextWindow = resolveCandidateContextWindow(entry?.candidate);
|
|
84
|
+
if (!contextWindow) {
|
|
85
|
+
unknownWindow.push(entry);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (contextWindow >= estimatedRequiredTokens) {
|
|
90
|
+
fittingKnown.push(entry);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
tooSmallKnown.push(entry);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const prioritizedEntries = [...fittingKnown, ...unknownWindow];
|
|
98
|
+
if (prioritizedEntries.length === 0) {
|
|
99
|
+
return {
|
|
100
|
+
prioritizedEntries: [...tooSmallKnown].sort((left, right) => {
|
|
101
|
+
const leftWindow = resolveCandidateContextWindow(left?.candidate) || 0;
|
|
102
|
+
const rightWindow = resolveCandidateContextWindow(right?.candidate) || 0;
|
|
103
|
+
if (rightWindow !== leftWindow) {
|
|
104
|
+
return rightWindow - leftWindow;
|
|
105
|
+
}
|
|
106
|
+
return sortEntriesByOriginalOrder(left, right);
|
|
107
|
+
}),
|
|
108
|
+
deferredEntries: []
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
prioritizedEntries,
|
|
114
|
+
deferredEntries: tooSmallKnown
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
58
118
|
function resolveHealthState(candidateState, now) {
|
|
59
119
|
const openUntil = Math.max(
|
|
60
120
|
normalizeNonNegativeInteger(candidateState?.openUntil),
|
|
@@ -229,6 +289,7 @@ export async function rankRouteCandidates({
|
|
|
229
289
|
stateStore,
|
|
230
290
|
config,
|
|
231
291
|
rateLimitEvaluations,
|
|
292
|
+
requestContext,
|
|
232
293
|
now = Date.now()
|
|
233
294
|
}) {
|
|
234
295
|
const normalizedStrategy = normalizeStrategyName(strategy || route?.routeStrategy || route?.strategy);
|
|
@@ -254,14 +315,30 @@ export async function rankRouteCandidates({
|
|
|
254
315
|
const ineligibleEntries = entries
|
|
255
316
|
.filter((entry) => !entry.eligible)
|
|
256
317
|
.sort(sortEntriesByOriginalOrder);
|
|
318
|
+
const estimatedRequiredTokens = normalizeNonNegativeInteger(
|
|
319
|
+
requestContext?.estimatedRequiredTokens ??
|
|
320
|
+
requestContext?.requiredTokens ??
|
|
321
|
+
requestContext?.totalTokensEstimate
|
|
322
|
+
);
|
|
257
323
|
|
|
258
324
|
const routeCursor = stateStore
|
|
259
325
|
? await stateStore.getRouteCursor(resolvedRouteKey)
|
|
260
326
|
: 0;
|
|
261
|
-
const
|
|
327
|
+
const contextAwareGroups = shouldApplyContextAwareOrdering(route, estimatedRequiredTokens)
|
|
328
|
+
? partitionEligibleEntriesByContextWindow(eligibleEntries, estimatedRequiredTokens)
|
|
329
|
+
: {
|
|
330
|
+
prioritizedEntries: eligibleEntries,
|
|
331
|
+
deferredEntries: []
|
|
332
|
+
};
|
|
333
|
+
const ranking = rankEligibleEntries(
|
|
334
|
+
normalizedStrategy,
|
|
335
|
+
contextAwareGroups.prioritizedEntries,
|
|
336
|
+
routeCursor
|
|
337
|
+
);
|
|
262
338
|
|
|
263
339
|
const rankedEntries = [
|
|
264
340
|
...ranking.orderedEligible,
|
|
341
|
+
...contextAwareGroups.deferredEntries,
|
|
265
342
|
...ineligibleEntries
|
|
266
343
|
];
|
|
267
344
|
|
|
@@ -193,10 +193,15 @@ function normalizeMessageRole(role) {
|
|
|
193
193
|
return 'user';
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
function
|
|
196
|
+
function getResponsesTextPartTypeForRole(role) {
|
|
197
|
+
return normalizeMessageRole(role) === 'assistant' ? 'output_text' : 'input_text';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeInputMessageContent(content, role) {
|
|
201
|
+
const textPartType = getResponsesTextPartTypeForRole(role);
|
|
197
202
|
if (typeof content === 'string') {
|
|
198
203
|
return content
|
|
199
|
-
? [{ type:
|
|
204
|
+
? [{ type: textPartType, text: content }]
|
|
200
205
|
: [];
|
|
201
206
|
}
|
|
202
207
|
|
|
@@ -208,7 +213,7 @@ function normalizeInputMessageContent(content) {
|
|
|
208
213
|
|
|
209
214
|
if ((part.type === 'text' || part.type === 'input_text' || part.type === 'output_text') && typeof part.text === 'string') {
|
|
210
215
|
parts.push({
|
|
211
|
-
type:
|
|
216
|
+
type: textPartType,
|
|
212
217
|
text: part.text
|
|
213
218
|
});
|
|
214
219
|
continue;
|
|
@@ -284,11 +289,12 @@ function convertMessagesToResponseInput(messages) {
|
|
|
284
289
|
continue;
|
|
285
290
|
}
|
|
286
291
|
|
|
287
|
-
const
|
|
292
|
+
const normalizedRole = normalizeMessageRole(normalizedMessage.role);
|
|
293
|
+
const contentParts = normalizeInputMessageContent(normalizedMessage.content, normalizedRole);
|
|
288
294
|
if (contentParts.length > 0) {
|
|
289
295
|
items.push({
|
|
290
296
|
type: 'message',
|
|
291
|
-
role:
|
|
297
|
+
role: normalizedRole,
|
|
292
298
|
content: contentParts
|
|
293
299
|
});
|
|
294
300
|
} else {
|
|
@@ -296,8 +302,11 @@ function convertMessagesToResponseInput(messages) {
|
|
|
296
302
|
if (fallbackText) {
|
|
297
303
|
items.push({
|
|
298
304
|
type: 'message',
|
|
299
|
-
role:
|
|
300
|
-
content: [{
|
|
305
|
+
role: normalizedRole,
|
|
306
|
+
content: [{
|
|
307
|
+
type: getResponsesTextPartTypeForRole(normalizedRole),
|
|
308
|
+
text: fallbackText
|
|
309
|
+
}]
|
|
301
310
|
});
|
|
302
311
|
}
|
|
303
312
|
}
|