llm-simple-router 1.1.2-beta.2 → 1.1.3
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/admin/quick-setup.js +2 -2
- package/dist/db/providers.d.ts +5 -1
- package/dist/db/providers.js +5 -1
- package/dist/proxy/handler/iteration-setup.js +6 -3
- package/dist/proxy/orchestration/orchestrator.js +17 -9
- package/dist/proxy/proxy-core.d.ts +0 -11
- package/dist/proxy/proxy-core.js +2 -63
- package/dist/proxy/transport/http.d.ts +2 -16
- package/dist/proxy/transport/http.js +22 -34
- package/dist/proxy/transport/shared.d.ts +23 -0
- package/dist/proxy/transport/shared.js +58 -0
- package/dist/proxy/transport/stream.d.ts +1 -1
- package/dist/proxy/transport/stream.js +37 -11
- package/frontend-dist/assets/{AuthLayout-CNJ18LiO.js → AuthLayout-DgrURhUg.js} +1 -1
- package/frontend-dist/assets/{Card-DHdWC9Sr.js → Card-C_9nHSrd.js} +1 -1
- package/frontend-dist/assets/{CardContent-D3XiSLZZ.js → CardContent-i69eqvwn.js} +1 -1
- package/frontend-dist/assets/{CardTitle-ouEnIzFR.js → CardTitle-BG8tDKg7.js} +1 -1
- package/frontend-dist/assets/CascadingModelSelect-DSBog9EX.js +1 -0
- package/frontend-dist/assets/{Checkbox-D4B5Gy-L.js → Checkbox-DIo_zvR8.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-D5uuYzAy.js → CollapsibleContent-vZSsPw6t.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-7lOShWej.js → CollapsibleTrigger-BBdH_cCm.js} +1 -1
- package/frontend-dist/assets/{ConcurrencyControl-DSpn8cI_.js → ConcurrencyControl-hsDQfbVn.js} +1 -1
- package/frontend-dist/assets/{Dashboard-DaMz3m0K.js → Dashboard-BndYfuJw.js} +1 -1
- package/frontend-dist/assets/{Input-DRF4ONbF.js → Input-CCoAeW8B.js} +1 -1
- package/frontend-dist/assets/{Label-muHpCvRx.js → Label-C7Hesw43.js} +1 -1
- package/frontend-dist/assets/{Login-CuQA8bbc.js → Login-C8ZPAsdy.js} +1 -1
- package/frontend-dist/assets/{Logs-CKO3germ.js → Logs-BS0OalCQ.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-De98UugX.js → ModelMappings-Ck6Oddbz.js} +1 -1
- package/frontend-dist/assets/{Monitor-BzeTXX7u.js → Monitor-D-yoFX7E.js} +1 -1
- package/frontend-dist/assets/Providers-CXFMJpnM.js +1 -0
- package/frontend-dist/assets/{ProxyEnhancement-CaXVxlxk.js → ProxyEnhancement-BY66st8t.js} +1 -1
- package/frontend-dist/assets/QuickSetup-GgmYoihs.js +1 -0
- package/frontend-dist/assets/{RetryRules-CZtQYpr0.js → RetryRules-C_dY-UdI.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-WAbHBrzu.js → RouterKeys-l9RxPss_.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-BIoz-v9k.js → RovingFocusItem-8-Pjjqen.js} +1 -1
- package/frontend-dist/assets/{Schedules-DgxMv_wm.js → Schedules-B0TVDH59.js} +1 -1
- package/frontend-dist/assets/{Separator-_gcj_6Dd.js → Separator-D6RzArbH.js} +1 -1
- package/frontend-dist/assets/{Settings-w33AM_0F.js → Settings-BkSAw8ff.js} +1 -1
- package/frontend-dist/assets/{Setup-D6c5svHh.js → Setup-38qAFwc9.js} +1 -1
- package/frontend-dist/assets/{Skeleton-wP01Qhzi.js → Skeleton-BpwFXs9Z.js} +1 -1
- package/frontend-dist/assets/{Switch-BnSoCbqK.js → Switch-BVug67mH.js} +1 -1
- package/frontend-dist/assets/{TableHeader-oSu5tayf.js → TableHeader-CARoN-zc.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-CzLaOeN9.js → TabsTrigger-t8wD-SZ9.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-lCG2BxB4.js → UnifiedRequestDialog-h4e7P7Id.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-Dz0c537Q.js → VisuallyHiddenInput-CzjlCU82.js} +1 -1
- package/frontend-dist/assets/arrow-down-P_02O0zN.js +1 -0
- package/frontend-dist/assets/{badge-qilWWy3M.js → badge-CxTrJsDs.js} +1 -1
- package/frontend-dist/assets/{button-DhbTyZqO.js → button-CEjoKdKq.js} +2 -2
- package/frontend-dist/assets/chevron-right-Bvw4z2Yf.js +1 -0
- package/frontend-dist/assets/{dialog-J16vWuXK.js → dialog-DPvcfIzg.js} +1 -1
- package/frontend-dist/assets/{image-BAKOQH7r.js → image-DOH8COxk.js} +1 -1
- package/frontend-dist/assets/{index-BoaXEmuZ.js → index-DE_N-dT0.js} +2 -2
- package/frontend-dist/assets/index-DGJSS9jI.css +1 -0
- package/frontend-dist/assets/model-patches-BVZz8hmE.js +1 -0
- package/frontend-dist/assets/{pencil-CQiZ13On.js → pencil-BERHubJ1.js} +1 -1
- package/frontend-dist/assets/plus-B9mBymA8.js +1 -0
- package/frontend-dist/assets/search-uTHGe-LF.js +1 -0
- package/frontend-dist/assets/{sparkles-o5rM_OIC.js → sparkles-gPb_SItQ.js} +1 -1
- package/frontend-dist/assets/{transform-domain-DczpyJVT.js → transform-domain-C42QlTbk.js} +1 -1
- package/frontend-dist/assets/{trash-2-DfV00PQC.js → trash-2-m7vH2ViZ.js} +1 -1
- package/frontend-dist/assets/{useClipboard-BEGcEu2g.js → useClipboard-Bzk0ItE3.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-BT7mqSLb.js → useLogRetention-I_TUncV5.js} +1 -1
- package/frontend-dist/assets/{useProviderGroups-CpH380OR.js → useProviderGroups-bpCq-vsG.js} +1 -1
- package/frontend-dist/index.html +3 -3
- package/package.json +1 -1
- package/frontend-dist/assets/CascadingModelSelect-uCR_78CB.js +0 -1
- package/frontend-dist/assets/Providers-C7TMCyLZ.js +0 -1
- package/frontend-dist/assets/QuickSetup-9OsD4eib.js +0 -1
- package/frontend-dist/assets/arrow-down-DBlAmBEr.js +0 -1
- package/frontend-dist/assets/chevron-right-B1eJ5usR.js +0 -1
- package/frontend-dist/assets/index-Db9D0WPf.css +0 -1
- package/frontend-dist/assets/model-patches-B47btKty.js +0 -1
- package/frontend-dist/assets/plus-BVJPX-4z.js +0 -1
- package/frontend-dist/assets/search-DzYxIOWW.js +0 -1
|
@@ -53,8 +53,8 @@ const QuickSetupProviderSchema = Type.Object({
|
|
|
53
53
|
name: Type.String(),
|
|
54
54
|
context_window: Type.Optional(Type.Number()),
|
|
55
55
|
patches: Type.Optional(Type.Array(Type.String())),
|
|
56
|
-
stream_timeout_ms: Type.Optional(Type.Number()),
|
|
57
|
-
non_stream_timeout_ms: Type.Optional(Type.Number()),
|
|
56
|
+
stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })),
|
|
57
|
+
non_stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })),
|
|
58
58
|
capabilities: Type.Optional(Type.Array(Type.String())),
|
|
59
59
|
})),
|
|
60
60
|
endpoints: Type.Optional(Type.Array(QuickSetupEndpointSchema, { minItems: 1 })),
|
package/dist/db/providers.d.ts
CHANGED
|
@@ -24,7 +24,11 @@ export interface Provider {
|
|
|
24
24
|
created_at: string;
|
|
25
25
|
updated_at: string;
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* 默认流式超时 5 分钟。
|
|
29
|
+
* 行为变更:v1.1.x 起从 600s(10min) 降为 300s(5min),影响未显式配置 stream_timeout_ms 的 provider。
|
|
30
|
+
* 长跑流式生成(长推理/长输出)若超 5min 会被中断,需在 provider/model 配置中显式调大或设 0(禁用)。
|
|
31
|
+
*/
|
|
28
32
|
export declare const DEFAULT_STREAM_TIMEOUT_MS = 300000;
|
|
29
33
|
/** 默认非流式超时 10 分钟 */
|
|
30
34
|
export declare const DEFAULT_NON_STREAM_TIMEOUT_MS = 600000;
|
package/dist/db/providers.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import { buildUpdateQuery, deleteById } from "./helpers.js";
|
|
3
3
|
import { parseModels } from "../config/model-context.js";
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* 默认流式超时 5 分钟。
|
|
6
|
+
* 行为变更:v1.1.x 起从 600s(10min) 降为 300s(5min),影响未显式配置 stream_timeout_ms 的 provider。
|
|
7
|
+
* 长跑流式生成(长推理/长输出)若超 5min 会被中断,需在 provider/model 配置中显式调大或设 0(禁用)。
|
|
8
|
+
*/
|
|
5
9
|
export const DEFAULT_STREAM_TIMEOUT_MS = 300_000;
|
|
6
10
|
/** 默认非流式超时 10 分钟 */
|
|
7
11
|
export const DEFAULT_NON_STREAM_TIMEOUT_MS = 600_000;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveEndpoint } from "../routing/resolve-endpoint.js";
|
|
2
2
|
import { sanitizeHeadersForLog } from "../proxy-logging.js";
|
|
3
|
-
import { buildUpstreamHeaders
|
|
3
|
+
import { buildUpstreamHeaders } from "../proxy-core.js";
|
|
4
|
+
import { buildUpstreamUrl } from "../transport/shared.js";
|
|
4
5
|
import { getModelTimeouts } from "../../db/providers.js";
|
|
5
6
|
import { buildTransportFn } from "../transport/transport-fn.js";
|
|
6
7
|
import { parseModels } from "../../config/model-context.js";
|
|
@@ -87,11 +88,13 @@ export function buildIterationSetup(params) {
|
|
|
87
88
|
} : undefined;
|
|
88
89
|
// --- Build transport function ---
|
|
89
90
|
const streamLoopEnabled = enhancementConfig.stream_loop_enabled;
|
|
91
|
+
// 合并 stream/nonStream 超时查询,单次 parseModels(applyProviderPatches 内另有一次解析)
|
|
92
|
+
const modelTimeouts = getModelTimeouts(provider, resolved.backend_model);
|
|
90
93
|
const transportFn = buildTransportFn({
|
|
91
94
|
provider, apiKey, body: patchedBody, cliHdrs, reply, upstreamPath: effectiveUpstreamPath, apiType: effectiveApiType,
|
|
92
95
|
isStream, startTime, logId, effectiveModel: clientModel,
|
|
93
|
-
nonStreamTimeoutMs:
|
|
94
|
-
streamTimeoutMs:
|
|
96
|
+
nonStreamTimeoutMs: modelTimeouts.nonStream,
|
|
97
|
+
streamTimeoutMs: modelTimeouts.stream,
|
|
95
98
|
tracker, matcher, request,
|
|
96
99
|
streamLoopEnabled, formatTransform, responseTransform, injectedHeaders,
|
|
97
100
|
timeoutContext: { modelId: resolved.backend_model, providerId: provider.id },
|
|
@@ -6,6 +6,9 @@ import { SemaphoreTimeoutError, SemaphoreQueueFullError } from "../../core/error
|
|
|
6
6
|
import { extractThinkingLevel } from "../../db/logs.js";
|
|
7
7
|
const DEFAULT_BASE_DELAY_MS = 1000;
|
|
8
8
|
const DEFAULT_FAILOVER_THRESHOLD = 400;
|
|
9
|
+
// reply.raw close listener 上限:覆盖 MAX_FAILOVER_ITERATIONS(10) + Fastify/socket 自身 listener。
|
|
10
|
+
// failover 循环复用同一 reply 多次挂载 close listener,不提高上限会触发 MaxListenersExceededWarning。
|
|
11
|
+
const REPLY_CLOSE_MAX_LISTENERS = 16;
|
|
9
12
|
/**
|
|
10
13
|
* 从 clientRequest JSON 中提取 thinking level。
|
|
11
14
|
* 委托给 db/logs.ts 的 extractThinkingLevel,保持日志写入和 orchestrator 使用同一逻辑。
|
|
@@ -33,14 +36,17 @@ export class ProxyOrchestrator {
|
|
|
33
36
|
const providerId = config.provider.id;
|
|
34
37
|
const controller = new AbortController();
|
|
35
38
|
// 客户端断连检测:监听 reply.raw(响应端),用 writableEnded 判断响应未完成才 abort。
|
|
36
|
-
// 旧逻辑监听 request.raw + readableEnded
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
// 旧逻辑监听 request.raw + readableEnded,对 POST 请求 readableEnded 恒为 true(body 已读完),
|
|
40
|
+
// 导致 close 永不 abort。failover 循环复用同一 reply 多次调用 handle(),每次 new 一个独立
|
|
41
|
+
// AbortController 并挂载新 close listener,故需提高 listener 上限(覆盖 MAX_FAILOVER_ITERATIONS);
|
|
42
|
+
// close 只触发一次,listener 随 reply.raw GC 回收,无永久泄漏。
|
|
43
|
+
// 防御:reply.raw 在测试中可能是简化 mock(非 EventEmitter),typeof 守卫跳过。
|
|
44
|
+
const rawEmitter = reply.raw;
|
|
45
|
+
if (typeof rawEmitter.setMaxListeners === "function") {
|
|
46
|
+
const current = typeof rawEmitter.getMaxListeners === "function" ? rawEmitter.getMaxListeners() : 0;
|
|
47
|
+
if (current < REPLY_CLOSE_MAX_LISTENERS)
|
|
48
|
+
rawEmitter.setMaxListeners(REPLY_CLOSE_MAX_LISTENERS);
|
|
49
|
+
}
|
|
44
50
|
reply.raw.on("close", () => {
|
|
45
51
|
if (!reply.raw.writableEnded)
|
|
46
52
|
controller.abort();
|
|
@@ -55,7 +61,9 @@ export class ProxyOrchestrator {
|
|
|
55
61
|
try {
|
|
56
62
|
reply.raw.destroy();
|
|
57
63
|
}
|
|
58
|
-
catch {
|
|
64
|
+
catch (e) {
|
|
65
|
+
request.log.debug({ err: e }, "reply.raw.destroy failed (already destroyed)");
|
|
66
|
+
}
|
|
59
67
|
});
|
|
60
68
|
return this.deps.semaphoreScope.withSlot(providerId, controller.signal, () => {
|
|
61
69
|
trackerReq.queued = true;
|
|
@@ -24,17 +24,6 @@ export type { ErrorKind } from "./format/types.js";
|
|
|
24
24
|
* 由 formatBody 回调根据 kind 参数映射各自的 type/code 并组装 body。
|
|
25
25
|
*/
|
|
26
26
|
export declare function createErrorFormatter(formatBody: (kind: ErrorKind, message: string) => Record<string, unknown>): ProxyErrorFormatter;
|
|
27
|
-
/**
|
|
28
|
-
* 拼接上游 URL,自动处理 base_url 已包含部分或完整 API 路径的情况。
|
|
29
|
-
*
|
|
30
|
-
* 兼容场景:
|
|
31
|
-
* - base_url = `https://host/v1`, upstreamPath = `/v1/chat/completions` → `https://host/v1/chat/completions`
|
|
32
|
-
* - base_url = `https://host/v1/chat/completions`, upstreamPath = `/v1/chat/completions` → `https://host/v1/chat/completions`
|
|
33
|
-
* - base_url = `https://host/chat/completions`, upstreamPath = `/v1/chat/completions` → `https://host/chat/completions`
|
|
34
|
-
* - base_url = `https://host/v1/`, upstreamPath = `/v1/chat/completions` → `https://host/v1/chat/completions`
|
|
35
|
-
* - base_url = `https://host/api/paas/v4`, upstreamPath = `/api/paas/v4/chat/completions` → `https://host/api/paas/v4/chat/completions`
|
|
36
|
-
*/
|
|
37
|
-
export declare function buildUpstreamUrl(baseUrl: string, upstreamPath: string): string;
|
|
38
27
|
export declare const SKIP_UPSTREAM: Set<string>;
|
|
39
28
|
export declare function selectHeaders(raw: RawHeaders, skip: Set<string>): Record<string, string>;
|
|
40
29
|
export declare function buildUpstreamHeaders(clientHeaders: RawHeaders, apiKey: string, payloadBytes?: number, apiType?: "openai" | "openai-responses" | "anthropic"): Record<string, string>;
|
package/dist/proxy/proxy-core.js
CHANGED
|
@@ -44,69 +44,8 @@ export function createErrorFormatter(formatBody) {
|
|
|
44
44
|
}),
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
* 已知上游 API 路径后缀(不含 /v1 等版本前缀)。
|
|
50
|
-
* 用于检测 base_url 中是否已包含完整路径。
|
|
51
|
-
*/
|
|
52
|
-
const KNOWN_API_SUFFIXES = [
|
|
53
|
-
"/chat/completions",
|
|
54
|
-
"/messages",
|
|
55
|
-
"/responses",
|
|
56
|
-
];
|
|
57
|
-
/**
|
|
58
|
-
* 拼接上游 URL,自动处理 base_url 已包含部分或完整 API 路径的情况。
|
|
59
|
-
*
|
|
60
|
-
* 兼容场景:
|
|
61
|
-
* - base_url = `https://host/v1`, upstreamPath = `/v1/chat/completions` → `https://host/v1/chat/completions`
|
|
62
|
-
* - base_url = `https://host/v1/chat/completions`, upstreamPath = `/v1/chat/completions` → `https://host/v1/chat/completions`
|
|
63
|
-
* - base_url = `https://host/chat/completions`, upstreamPath = `/v1/chat/completions` → `https://host/chat/completions`
|
|
64
|
-
* - base_url = `https://host/v1/`, upstreamPath = `/v1/chat/completions` → `https://host/v1/chat/completions`
|
|
65
|
-
* - base_url = `https://host/api/paas/v4`, upstreamPath = `/api/paas/v4/chat/completions` → `https://host/api/paas/v4/chat/completions`
|
|
66
|
-
*/
|
|
67
|
-
export function buildUpstreamUrl(baseUrl, upstreamPath) {
|
|
68
|
-
const normalized = baseUrl.replace(/\/+$/, "");
|
|
69
|
-
// 1) 完全匹配:base_url 已包含完整 upstreamPath
|
|
70
|
-
if (normalized.endsWith(upstreamPath))
|
|
71
|
-
return normalized;
|
|
72
|
-
// 2) 检测 base_url 是否已包含已知 API 路径后缀
|
|
73
|
-
// 例如 `https://host/v1/chat/completions` → 已包含,直接返回
|
|
74
|
-
for (const suffix of KNOWN_API_SUFFIXES) {
|
|
75
|
-
if (normalized.endsWith(suffix))
|
|
76
|
-
return normalized;
|
|
77
|
-
}
|
|
78
|
-
// 3) 从 upstreamPath 中找到 base_url 的重叠部分,只追加非重叠尾部
|
|
79
|
-
// 例如 base_url = `https://host/api/paas/v4`, upstreamPath = `/api/paas/v4/chat/completions`
|
|
80
|
-
// → 重叠 `/api/paas/v4`,追加 `/chat/completions`
|
|
81
|
-
// 例如 base_url = `https://host/v1`, upstreamPath = `/v1/chat/completions`
|
|
82
|
-
// → 重叠 `/v1`,追加 `/chat/completions`
|
|
83
|
-
const overlap = findPathOverlap(normalized, upstreamPath);
|
|
84
|
-
if (overlap.length > 0) {
|
|
85
|
-
const rest = upstreamPath.slice(overlap.length);
|
|
86
|
-
return `${normalized}${rest}`;
|
|
87
|
-
}
|
|
88
|
-
// 4) 确保拼接处有且仅有一个 /
|
|
89
|
-
if (!upstreamPath.startsWith("/"))
|
|
90
|
-
return `${normalized}/${upstreamPath}`;
|
|
91
|
-
return `${normalized}${upstreamPath}`;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* 找出 base_url 末尾与 upstreamPath 开头的最长重叠路径段。
|
|
95
|
-
* 例如 base_url = `https://host/api/v4`, upstreamPath = `/api/v4/chat/completions` → 返回 `/api/v4`
|
|
96
|
-
*/
|
|
97
|
-
function findPathOverlap(baseUrl, upstreamPath) {
|
|
98
|
-
// 将 upstreamPath 按 / 拆分,逐段检查是否与 baseUrl 末尾匹配
|
|
99
|
-
const segments = upstreamPath.split("/");
|
|
100
|
-
// segments[0] 是空字符串(因为 upstreamPath 以 / 开头),至少需要 2 段才有意义
|
|
101
|
-
const MIN_OVERLAP_SEGMENTS = 2;
|
|
102
|
-
for (let len = segments.length - 1; len >= MIN_OVERLAP_SEGMENTS; len--) {
|
|
103
|
-
const candidate = segments.slice(0, len).join("/");
|
|
104
|
-
if (candidate.length > 0 && baseUrl.endsWith(candidate)) {
|
|
105
|
-
return candidate;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return "";
|
|
109
|
-
}
|
|
47
|
+
// buildUpstreamUrl / findPathOverlap / KNOWN_API_SUFFIXES 已下沉至 ./transport/shared.ts
|
|
48
|
+
// (打破 proxy-core ↔ transport/http 循环依赖)
|
|
110
49
|
// ---------- Header utilities ----------
|
|
111
50
|
export const SKIP_UPSTREAM = new Set([
|
|
112
51
|
"host",
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { Agent } from "http";
|
|
2
2
|
import type { RawHeaders, TransportResult } from "../types.js";
|
|
3
|
+
import { type BuildHeadersFn, type TransportCallOpts } from "./shared.js";
|
|
3
4
|
export { callStream } from "./stream.js";
|
|
4
|
-
|
|
5
|
-
export interface TransportCallOpts {
|
|
6
|
-
signal?: AbortSignal;
|
|
7
|
-
}
|
|
5
|
+
export { _transportInternals } from "./shared.js";
|
|
8
6
|
/** callNonStream 选项:timeoutMs=0/Infinity 表示禁用超时。 */
|
|
9
7
|
export interface NonStreamCallOpts extends TransportCallOpts {
|
|
10
8
|
timeoutMs?: number;
|
|
@@ -13,18 +11,6 @@ export interface NonStreamCallOpts extends TransportCallOpts {
|
|
|
13
11
|
export interface GetCallOpts {
|
|
14
12
|
timeoutMs?: number;
|
|
15
13
|
}
|
|
16
|
-
export interface UpstreamRequestOptions {
|
|
17
|
-
hostname: string;
|
|
18
|
-
port: number;
|
|
19
|
-
path: string;
|
|
20
|
-
method: string;
|
|
21
|
-
headers: Record<string, string>;
|
|
22
|
-
}
|
|
23
|
-
export declare const _transportInternals: {
|
|
24
|
-
createUpstreamRequest(url: URL, options: UpstreamRequestOptions, agent?: Agent): import("http").ClientRequest;
|
|
25
|
-
};
|
|
26
|
-
export declare function buildRequestOptions(url: URL, headers: Record<string, string>, method?: string): UpstreamRequestOptions;
|
|
27
|
-
export type BuildHeadersFn = (cliHdrs: RawHeaders, key: string, bytes?: number) => Record<string, string>;
|
|
28
14
|
export declare function callNonStream(backend: {
|
|
29
15
|
base_url: string;
|
|
30
16
|
}, apiKey: string, body: Record<string, unknown>, clientHeaders: RawHeaders, upstreamPath: string, buildHeaders: BuildHeadersFn, agent?: Agent, opts?: NonStreamCallOpts): Promise<TransportResult>;
|
|
@@ -1,33 +1,13 @@
|
|
|
1
|
-
import { request as httpRequestFn } from "http";
|
|
2
|
-
import { request as httpsRequestFn } from "https";
|
|
3
1
|
import { UPSTREAM_SUCCESS, filterHeaders } from "../types.js";
|
|
4
|
-
import { buildUpstreamUrl } from "../proxy-core.js";
|
|
5
2
|
import { DEFAULT_GET_TIMEOUT_MS } from "../../core/constants.js";
|
|
6
|
-
|
|
3
|
+
import { buildUpstreamUrl, _transportInternals, buildRequestOptions, } from "./shared.js";
|
|
4
|
+
// Re-export callStream from stream.ts for external consumers
|
|
7
5
|
export { callStream } from "./stream.js";
|
|
6
|
+
// 兼容测试 mock:transport.test.ts 经 http 模块命名空间修改 _transportInternals 属性
|
|
7
|
+
export { _transportInternals } from "./shared.js";
|
|
8
8
|
// ---------- Constants ----------
|
|
9
9
|
const UPSTREAM_BAD_GATEWAY = 502;
|
|
10
10
|
const UPSTREAM_SUCCESS_RANGE = 100;
|
|
11
|
-
const HTTPS_DEFAULT_PORT = 443;
|
|
12
|
-
const HTTP_DEFAULT_PORT = 80;
|
|
13
|
-
export const _transportInternals = {
|
|
14
|
-
createUpstreamRequest(url, options, agent) {
|
|
15
|
-
const opts = agent ? { ...options, agent } : options;
|
|
16
|
-
return url.protocol === "https:"
|
|
17
|
-
? httpsRequestFn(opts)
|
|
18
|
-
: httpRequestFn(opts);
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
export function buildRequestOptions(url, headers, method = "POST") {
|
|
22
|
-
return {
|
|
23
|
-
hostname: url.hostname,
|
|
24
|
-
port: Number(url.port) ||
|
|
25
|
-
(url.protocol === "https:" ? HTTPS_DEFAULT_PORT : HTTP_DEFAULT_PORT),
|
|
26
|
-
path: url.pathname,
|
|
27
|
-
method,
|
|
28
|
-
headers,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
11
|
// ---------- callNonStream ----------
|
|
32
12
|
export function callNonStream(backend, apiKey, body, clientHeaders, upstreamPath, buildHeaders, agent, opts) {
|
|
33
13
|
return new Promise((resolve) => {
|
|
@@ -44,14 +24,22 @@ export function callNonStream(backend, apiKey, body, clientHeaders, upstreamPath
|
|
|
44
24
|
req.on("timeout", () => req.destroy(new Error("upstream inactivity timeout")));
|
|
45
25
|
}
|
|
46
26
|
// 客户端断连:abort 信号穿透到上游 socket,立即切断连接。
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
// resolveOnce 在 Promise settle 时移除 abort listener,避免重试累积残留
|
|
28
|
+
// (与 callStream 的 resolveOnce 模式对称)。
|
|
29
|
+
const clientSignal = opts?.signal;
|
|
30
|
+
const onClientAbort = clientSignal ? () => req.destroy(new Error("client aborted")) : undefined;
|
|
31
|
+
const resolveOnce = (r) => {
|
|
32
|
+
if (onClientAbort && clientSignal && !clientSignal.aborted) {
|
|
33
|
+
clientSignal.removeEventListener("abort", onClientAbort);
|
|
34
|
+
}
|
|
35
|
+
resolve(r);
|
|
36
|
+
};
|
|
37
|
+
if (onClientAbort && clientSignal) {
|
|
38
|
+
if (clientSignal.aborted) {
|
|
39
|
+
onClientAbort();
|
|
52
40
|
}
|
|
53
41
|
else {
|
|
54
|
-
|
|
42
|
+
clientSignal.addEventListener("abort", onClientAbort, { once: true });
|
|
55
43
|
}
|
|
56
44
|
}
|
|
57
45
|
req.on("response", (res) => {
|
|
@@ -62,7 +50,7 @@ export function callNonStream(backend, apiKey, body, clientHeaders, upstreamPath
|
|
|
62
50
|
const responseBody = Buffer.concat(chunks).toString("utf-8");
|
|
63
51
|
const headers = filterHeaders(res.headers);
|
|
64
52
|
if (statusCode >= UPSTREAM_SUCCESS && statusCode < UPSTREAM_SUCCESS + UPSTREAM_SUCCESS_RANGE) {
|
|
65
|
-
|
|
53
|
+
resolveOnce({
|
|
66
54
|
kind: "success",
|
|
67
55
|
statusCode,
|
|
68
56
|
body: responseBody,
|
|
@@ -72,7 +60,7 @@ export function callNonStream(backend, apiKey, body, clientHeaders, upstreamPath
|
|
|
72
60
|
});
|
|
73
61
|
}
|
|
74
62
|
else {
|
|
75
|
-
|
|
63
|
+
resolveOnce({
|
|
76
64
|
kind: "error",
|
|
77
65
|
statusCode,
|
|
78
66
|
body: responseBody,
|
|
@@ -84,9 +72,9 @@ export function callNonStream(backend, apiKey, body, clientHeaders, upstreamPath
|
|
|
84
72
|
});
|
|
85
73
|
// 上游响应过程中连接中断时,IncomingMessage 发射 'error' 事件。
|
|
86
74
|
// 无 listener 会导致 uncaught exception 使进程退出。
|
|
87
|
-
res.on("error", (error) =>
|
|
75
|
+
res.on("error", (error) => resolveOnce({ kind: "throw", error }));
|
|
88
76
|
});
|
|
89
|
-
req.on("error", (error) =>
|
|
77
|
+
req.on("error", (error) => resolveOnce({ kind: "throw", error }));
|
|
90
78
|
req.write(payload);
|
|
91
79
|
req.end();
|
|
92
80
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Agent } from "http";
|
|
2
|
+
import type { RawHeaders } from "../types.js";
|
|
3
|
+
/** 非流式/流式调用通用可选项:客户端断连信号。 */
|
|
4
|
+
export interface TransportCallOpts {
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 拼接上游 URL,自动处理 base_url 已包含部分或完整 API 路径的情况。
|
|
9
|
+
* 兼容:base_url 可带或不带 /v1 前缀、可含完整 endpoint,本函数只追加非重叠尾部。
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildUpstreamUrl(baseUrl: string, upstreamPath: string): string;
|
|
12
|
+
export interface UpstreamRequestOptions {
|
|
13
|
+
hostname: string;
|
|
14
|
+
port: number;
|
|
15
|
+
path: string;
|
|
16
|
+
method: string;
|
|
17
|
+
headers: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export declare const _transportInternals: {
|
|
20
|
+
createUpstreamRequest(url: URL, options: UpstreamRequestOptions, agent?: Agent): import("http").ClientRequest;
|
|
21
|
+
};
|
|
22
|
+
export declare function buildRequestOptions(url: URL, headers: Record<string, string>, method?: string): UpstreamRequestOptions;
|
|
23
|
+
export type BuildHeadersFn = (cliHdrs: RawHeaders, key: string, bytes?: number) => Record<string, string>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { request as httpRequestFn } from "http";
|
|
2
|
+
import { request as httpsRequestFn } from "https";
|
|
3
|
+
// ===== URL building =====
|
|
4
|
+
const KNOWN_API_SUFFIXES = [
|
|
5
|
+
"/chat/completions",
|
|
6
|
+
"/messages",
|
|
7
|
+
"/responses",
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* 拼接上游 URL,自动处理 base_url 已包含部分或完整 API 路径的情况。
|
|
11
|
+
* 兼容:base_url 可带或不带 /v1 前缀、可含完整 endpoint,本函数只追加非重叠尾部。
|
|
12
|
+
*/
|
|
13
|
+
export function buildUpstreamUrl(baseUrl, upstreamPath) {
|
|
14
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
15
|
+
if (normalized.endsWith(upstreamPath))
|
|
16
|
+
return normalized;
|
|
17
|
+
for (const suffix of KNOWN_API_SUFFIXES) {
|
|
18
|
+
if (normalized.endsWith(suffix))
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
const overlap = findPathOverlap(normalized, upstreamPath);
|
|
22
|
+
if (overlap.length > 0) {
|
|
23
|
+
return `${normalized}${upstreamPath.slice(overlap.length)}`;
|
|
24
|
+
}
|
|
25
|
+
if (!upstreamPath.startsWith("/"))
|
|
26
|
+
return `${normalized}/${upstreamPath}`;
|
|
27
|
+
return `${normalized}${upstreamPath}`;
|
|
28
|
+
}
|
|
29
|
+
function findPathOverlap(baseUrl, upstreamPath) {
|
|
30
|
+
const segments = upstreamPath.split("/");
|
|
31
|
+
const MIN_OVERLAP_SEGMENTS = 2;
|
|
32
|
+
for (let len = segments.length - 1; len >= MIN_OVERLAP_SEGMENTS; len--) {
|
|
33
|
+
const candidate = segments.slice(0, len).join("/");
|
|
34
|
+
if (candidate.length > 0 && baseUrl.endsWith(candidate)) {
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
// ===== Request utilities =====
|
|
41
|
+
const HTTPS_DEFAULT_PORT = 443;
|
|
42
|
+
const HTTP_DEFAULT_PORT = 80;
|
|
43
|
+
export const _transportInternals = {
|
|
44
|
+
createUpstreamRequest(url, options, agent) {
|
|
45
|
+
const opts = agent ? { ...options, agent } : options;
|
|
46
|
+
return url.protocol === "https:" ? httpsRequestFn(opts) : httpRequestFn(opts);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export function buildRequestOptions(url, headers, method = "POST") {
|
|
50
|
+
return {
|
|
51
|
+
hostname: url.hostname,
|
|
52
|
+
port: Number(url.port) ||
|
|
53
|
+
(url.protocol === "https:" ? HTTPS_DEFAULT_PORT : HTTP_DEFAULT_PORT),
|
|
54
|
+
path: url.pathname,
|
|
55
|
+
method,
|
|
56
|
+
headers,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { FastifyReply } from "fastify";
|
|
|
4
4
|
import type { RawHeaders, TransportResult } from "../types.js";
|
|
5
5
|
import type { SSEMetricsTransform } from "../../metrics/sse-metrics-transform.js";
|
|
6
6
|
import type { StreamLoopGuard } from "../../core/loop-prevention/index.js";
|
|
7
|
-
import { type BuildHeadersFn, type TransportCallOpts } from "./
|
|
7
|
+
import { type BuildHeadersFn, type TransportCallOpts } from "./shared.js";
|
|
8
8
|
/** callStream 选项:connectTimeoutMs 为响应头前的无活动超时(复用 stream timeout 语义)。 */
|
|
9
9
|
export interface StreamCallOpts extends TransportCallOpts {
|
|
10
10
|
connectTimeoutMs?: number;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { PassThrough } from "stream";
|
|
2
2
|
import { UPSTREAM_SUCCESS, filterHeaders } from "../types.js";
|
|
3
|
-
import { buildUpstreamUrl } from "
|
|
4
|
-
import { _transportInternals, buildRequestOptions, } from "./http.js";
|
|
3
|
+
import { buildUpstreamUrl, _transportInternals, buildRequestOptions, } from "./shared.js";
|
|
5
4
|
const UPSTREAM_BAD_GATEWAY = 502;
|
|
6
5
|
const BUFFER_SIZE_LIMIT = 4096;
|
|
7
6
|
const END_REPLY_TIMEOUT_MS = 1000;
|
|
@@ -426,14 +425,24 @@ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutM
|
|
|
426
425
|
upstreamReq.on("timeout", () => upstreamReq.destroy(new Error("upstream inactivity timeout (pre-response)")));
|
|
427
426
|
}
|
|
428
427
|
// 客户端断连:abort 信号穿透到上游 socket,立即切断连接。
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
428
|
+
// resolveOnce 在 Promise settle 时移除 abort listener,避免重试累积残留
|
|
429
|
+
// (listener 受 ITERATION_CAP 上界约束,非进程级泄漏,此处显式清理保持干净)。
|
|
430
|
+
const clientSignal = opts?.signal;
|
|
431
|
+
const onClientAbort = clientSignal
|
|
432
|
+
? () => upstreamReq.destroy(new Error("client aborted"))
|
|
433
|
+
: undefined;
|
|
434
|
+
const resolveOnce = (r) => {
|
|
435
|
+
if (onClientAbort && clientSignal && !clientSignal.aborted) {
|
|
436
|
+
clientSignal.removeEventListener("abort", onClientAbort);
|
|
437
|
+
}
|
|
438
|
+
effectiveResolve(r);
|
|
439
|
+
};
|
|
440
|
+
if (onClientAbort && clientSignal) {
|
|
441
|
+
if (clientSignal.aborted) {
|
|
442
|
+
onClientAbort();
|
|
434
443
|
}
|
|
435
444
|
else {
|
|
436
|
-
|
|
445
|
+
clientSignal.addEventListener("abort", onClientAbort, { once: true });
|
|
437
446
|
}
|
|
438
447
|
}
|
|
439
448
|
upstreamReq.on("response", (upstreamRes) => {
|
|
@@ -443,9 +452,23 @@ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutM
|
|
|
443
452
|
const statusCode = upstreamRes.statusCode || UPSTREAM_BAD_GATEWAY;
|
|
444
453
|
if (statusCode !== UPSTREAM_SUCCESS) {
|
|
445
454
|
const chunks = [];
|
|
455
|
+
// 非 2xx body 兜底超时:connect timeout 已在响应头到达时清零(L474),
|
|
456
|
+
// 若上游发半截 body 后静默挂住(不 FIN 不 RST,CDN/反代常见故障),
|
|
457
|
+
// end/error 均不触发 → resolveOnce 永不调用 → slot 泄漏。
|
|
458
|
+
// 复用 connectTimeoutMs 作为 body 读取上限,与 200 路径 idleTimer 同源。
|
|
459
|
+
const bodyGuard = hasConnectTimeout
|
|
460
|
+
? setTimeout(() => upstreamRes.destroy(new Error("upstream error-body timeout")), connectTimeoutMs)
|
|
461
|
+
: null;
|
|
462
|
+
if (bodyGuard)
|
|
463
|
+
bodyGuard.unref();
|
|
464
|
+
const finishBody = (r) => {
|
|
465
|
+
if (bodyGuard)
|
|
466
|
+
clearTimeout(bodyGuard);
|
|
467
|
+
resolveOnce(r);
|
|
468
|
+
};
|
|
446
469
|
upstreamRes.on("data", (chunk) => chunks.push(chunk));
|
|
447
470
|
upstreamRes.on("end", () => {
|
|
448
|
-
|
|
471
|
+
finishBody({
|
|
449
472
|
kind: "stream_error",
|
|
450
473
|
statusCode,
|
|
451
474
|
body: Buffer.concat(chunks).toString("utf-8"),
|
|
@@ -453,10 +476,13 @@ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutM
|
|
|
453
476
|
sentHeaders: upstreamHeaders,
|
|
454
477
|
});
|
|
455
478
|
});
|
|
479
|
+
// 非 2xx body 传输中连接中断:无 listener 会 uncaughtException + Promise 永挂(slot 泄漏)。
|
|
480
|
+
// 与 200 分支 proxy.onUpstreamError 对称,与 callNonStream(http.ts) 同路径一致。
|
|
481
|
+
upstreamRes.on("error", (err) => finishBody({ kind: "throw", error: err }));
|
|
456
482
|
return;
|
|
457
483
|
}
|
|
458
484
|
const proxy = new StreamProxy(statusCode, upstreamRes.headers, upstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform, timeoutContext, onTimeoutAbort, upstreamRes, upstreamReq);
|
|
459
|
-
proxy.bindResolve(
|
|
485
|
+
proxy.bindResolve(resolveOnce);
|
|
460
486
|
proxy.registerCloseHandler();
|
|
461
487
|
// 无 early error checker 时直接开始流式传输
|
|
462
488
|
if (!checkEarlyError)
|
|
@@ -466,7 +492,7 @@ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutM
|
|
|
466
492
|
upstreamRes.on("end", () => proxy.onEnd());
|
|
467
493
|
upstreamRes.on("error", (err) => proxy.onUpstreamError(err));
|
|
468
494
|
});
|
|
469
|
-
upstreamReq.on("error", (error) =>
|
|
495
|
+
upstreamReq.on("error", (error) => resolveOnce({ kind: "throw", error }));
|
|
470
496
|
upstreamReq.write(payload);
|
|
471
497
|
upstreamReq.end();
|
|
472
498
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$t as e,Jt as t,Mt as n,St as r,et as i,n as a,ot as o,rt as s,st as c,tt as l,yt as u}from"./button-
|
|
1
|
+
import{$t as e,Jt as t,Mt as n,St as r,et as i,n as a,ot as o,rt as s,st as c,tt as l,yt as u}from"./button-CEjoKdKq.js";import{dt as d,ft as f,r as p}from"./index-DE_N-dT0.js";import{t as m}from"./Card-C_9nHSrd.js";import{t as h}from"./CardContent-i69eqvwn.js";var g={class:`min-h-screen flex items-center justify-center bg-background relative`},_={class:`text-center mb-6`},v={class:`text-sm text-muted-foreground mt-1`},y=c({__name:`AuthLayout`,props:{subtitle:{}},setup(c){let{isDark:y,toggleTheme:b}=p();return(p,x)=>(u(),s(`div`,g,[o(t(a),{variant:`ghost`,size:`icon`,class:`absolute top-4 right-4 text-muted-foreground hover:text-foreground`,onClick:t(b)},{default:n(()=>[t(y)?(u(),l(t(d),{key:1,class:`w-4 h-4`})):(u(),l(t(f),{key:0,class:`w-4 h-4`}))]),_:1},8,[`onClick`]),o(t(m),{class:`w-full max-w-sm shadow-lg`},{default:n(()=>[o(t(h),{class:`pt-6`},{default:n(()=>[i(`div`,_,[x[0]||=i(`div`,{class:`w-12 h-12 bg-primary rounded-lg mx-auto mb-3 flex items-center justify-center`},[i(`svg`,{class:`w-7 h-7 text-white`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[i(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z`})])],-1),x[1]||=i(`h1`,{class:`text-xl font-semibold text-foreground`},` LLM Simple Router `,-1),i(`p`,v,e(c.subtitle),1)]),r(p.$slots,`default`)]),_:3})]),_:3})]))}});export{y as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Jt as e,St as t,Xt as n,r,rt as i,st as a,yt as o}from"./button-
|
|
1
|
+
import{Jt as e,St as t,Xt as n,r,rt as i,st as a,yt as o}from"./button-CEjoKdKq.js";var s=[`data-size`,`data-flush`],c=a({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`},flush:{type:Boolean,default:!1}},setup(a){let c=a;return(l,u)=>(o(),i(`div`,{"data-slot":`card`,"data-size":a.size,"data-flush":a.flush||void 0,class:n(e(r)(`border-border bg-card text-card-foreground rounded-lg border text-sm group/card flex flex-col`,a.flush?`overflow-hidden`:`overflow-hidden gap-4 py-4 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg`,`data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0`,c.class))},[t(l.$slots,`default`)],10,s))}});export{c as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Jt as e,St as t,Xt as n,r,rt as i,st as a,yt as o}from"./button-
|
|
1
|
+
import{Jt as e,St as t,Xt as n,r,rt as i,st as a,yt as o}from"./button-CEjoKdKq.js";var s=a({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(o(),i(`div`,{"data-slot":`card-content`,class:n(e(r)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[t(a.$slots,`default`)],2))}});export{s as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Jt as e,St as t,Xt as n,r,rt as i,st as a,yt as o}from"./button-
|
|
1
|
+
import{Jt as e,St as t,Xt as n,r,rt as i,st as a,yt as o}from"./button-CEjoKdKq.js";var s=a({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(o(),i(`div`,{"data-slot":`card-header`,class:n(e(r)(`gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]`,s.class))},[t(a.$slots,`default`)],2))}}),c=a({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(o(),i(`div`,{"data-slot":`card-title`,class:n(e(r)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[t(a.$slots,`default`)],2))}});export{s as n,c as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{$ as e,$t as t,Bt as n,Jt as r,Mt as i,Q as a,Qt as o,W as s,Xt as c,Z as l,et as u,ht as d,nt as f,ot as p,rt as m,st as h,tt as g,xt as _,yt as v}from"./button-CEjoKdKq.js";import{t as y}from"./chevron-right-Bvw4z2Yf.js";import{b,vt as x,x as S,xt as C,y as w}from"./index-DE_N-dT0.js";var T=[`onMouseenter`],E={class:`truncate max-w-40`},D={key:0,class:`ml-1 text-[10px] px-1 py-px rounded bg-emerald-500/15 text-emerald-400 shrink-0`},O=[`onClick`],k={class:`truncate`},A={key:0,class:`shrink-0 text-xs text-muted-foreground`},j={key:0,class:`px-2 py-1.5 text-sm text-muted-foreground`},M=150,N=2,P=h({__name:`CascadingSelect`,props:{groups:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean,default:!1},dashed:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(h,{emit:C}){let{t:P}=s(),F=h,I=e(()=>F.placeholder||P(`common.selectPlaceholder`)),L=C,R=n(!1),z=n(null),B=n(new Map),V=n({top:0,left:0}),H=null;function U(e,t){t?B.value.set(e,t):B.value.delete(e)}function W(e){H&&=(clearTimeout(H),null),z.value=e,d(()=>J(e))}function G(){H=setTimeout(()=>{z.value=null},M)}function K(){H&&=(clearTimeout(H),null)}function q(){z.value=null}function J(e){let t=B.value.get(e);if(!t)return;let n=t.getBoundingClientRect();V.value={top:n.top,left:n.right+N}}let Y=e(()=>{if(!F.modelValue)return``;let e=F.groups.find(e=>e.key===F.modelValue.groupKey);if(!e)return``;let t=e.options.find(e=>e.value===F.modelValue.value);return t?`${e.label} / ${t.label}`:``});function X(e,t){L(`update:modelValue`,{groupKey:e,value:t}),R.value=!1}function Z(e){R.value=e,e||(z.value=null)}return(e,n)=>(v(),g(r(S),{open:R.value,"onUpdate:open":Z},{default:i(()=>[p(r(w),{"as-child":``},{default:i(()=>[u(`div`,{class:c([`flex w-full items-center justify-between rounded-md bg-background ring-offset-background cursor-pointer hover:bg-accent hover:text-accent-foreground`,[h.compact?`h-7 text-xs px-2 py-1 rounded-md`:`h-10 text-sm px-3 py-2 border border-border rounded-md`,!h.compact&&h.dashed?`border-dashed border-primary/20`:``,{"ring-2 ring-ring ring-offset-2":R.value}]])},[u(`span`,{class:c([`truncate`,h.modelValue?`text-foreground`:`text-muted-foreground`])},t(Y.value||I.value),3),p(r(x),{class:`h-4 w-4 shrink-0 opacity-50`})],2)]),_:1}),p(r(b),{align:`start`,"side-offset":4,class:`z-[200] w-auto min-w-56 max-h-[80vh] overflow-y-auto p-1`},{default:i(()=>[(v(!0),m(l,null,_(h.groups,e=>(v(),m(`div`,{key:e.key,ref_for:!0,ref:t=>U(e.key,t),class:c([`relative flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground`,{"bg-accent text-accent-foreground z-10":z.value===e.key}]),onMouseenter:t=>W(e.key),onMouseleave:G},[u(`span`,E,t(e.label),1),e.badge?(v(),m(`span`,D,t(e.badge),1)):f(``,!0),p(r(y),{class:`ml-1 h-4 w-4 shrink-0 opacity-50`})],42,T))),128)),(v(),g(a,{to:`body`},[z.value&&h.groups.find(e=>e.key===z.value)?.options.length?(v(),m(`div`,{key:0,class:`fixed z-[201] min-w-48 max-h-[80vh] overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md`,style:o({top:`${V.value.top}px`,left:`${V.value.left}px`}),onMouseenter:K,onMouseleave:q},[(v(!0),m(l,null,_(h.groups.find(e=>e.key===z.value)?.options??[],e=>(v(),m(`div`,{key:e.value,class:c([`flex cursor-pointer items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground`,{"bg-accent text-accent-foreground":h.modelValue?.groupKey===z.value&&h.modelValue?.value===e.value}]),onClick:t=>X(z.value,e.value)},[u(`span`,k,t(e.label),1),e.tag?(v(),m(`span`,A,t(e.tag),1)):f(``,!0)],10,O))),128))],36)):f(``,!0)])),h.groups.length===0?(v(),m(`div`,j,t(r(P)(`common.noOptions`)),1)):f(``,!0)]),_:1})]),_:1},8,[`open`]))}}),F=h({__name:`CascadingModelSelect`,props:{providers:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean,default:!1},dashed:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(t,{emit:n}){let{t:r}=s(),i=t,a=e(()=>i.placeholder||r(`mappings.selectProviderModel`)),o=n,c=e(()=>i.providers.map(e=>({key:e.provider.id,label:e.provider.name,badge:e.isNew?r(`common.new`):void 0,options:e.models.map(e=>({value:e.name,label:e.name,tag:C(e.contextWindow)}))}))),l=e(()=>i.modelValue?{groupKey:i.modelValue.provider_id,value:i.modelValue.model}:void 0);function u(e){o(`update:modelValue`,{provider_id:e.groupKey,model:e.value})}return(e,n)=>(v(),g(P,{groups:c.value,"model-value":l.value,placeholder:a.value,compact:t.compact,dashed:t.dashed,"onUpdate:modelValue":u},null,8,[`groups`,`model-value`,`placeholder`,`compact`,`dashed`]))}});export{F as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$ as e,Jt as t,Mt as n,S as r,St as i,X as a,Y as o,Zt as s,h as c,i as l,lt as u,mt as d,nt as f,o as p,ot as m,r as h,st as g,tt as _,wt as v,yt as y}from"./button-
|
|
1
|
+
import{$ as e,Jt as t,Mt as n,S as r,St as i,X as a,Y as o,Zt as s,h as c,i as l,lt as u,mt as d,nt as f,o as p,ot as m,r as h,st as g,tt as _,wt as v,yt as y}from"./button-CEjoKdKq.js";import{t as b}from"./VisuallyHiddenInput-CzjlCU82.js";import{t as x}from"./RovingFocusItem-8-Pjjqen.js";import{H as S,J as C,Q as w,W as T,X as E,yt as D,z as O}from"./index-DE_N-dT0.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>w(e,t)):w(e,t)}var[A,j]=E(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=E(`CheckboxRoot`),I=g({inheritAttrs:!1,__name:`CheckboxRoot`,props:{defaultValue:{type:null,required:!1},modelValue:{type:null,required:!1,default:void 0},disabled:{type:Boolean,required:!1},value:{type:null,required:!1,default:`on`},id:{type:String,required:!1},trueValue:{type:null,required:!1,default:()=>!0},falseValue:{type:null,required:!1,default:()=>!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`},name:{type:String,required:!1},required:{type:Boolean,required:!1}},emits:[`update:modelValue`],setup(r,{emit:s}){let u=r,m=s,{forwardRef:h,currentElement:g}=p(),S=A(null),E=c(u,`modelValue`,m,{defaultValue:u.defaultValue??u.falseValue,passive:u.modelValue===void 0}),D=e(()=>S?.disabled.value||u.disabled),O=e(()=>w(E.value,u.trueValue)),j=e(()=>C(S?.modelValue.value)?E.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,u.value));function P(){if(C(S?.modelValue.value))E.value===`indeterminate`?E.value=u.trueValue:E.value=O.value?u.falseValue:u.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,u.value)){let t=e.findIndex(e=>w(e,u.value));e.splice(t,1)}else e.push(u.value);S.modelValue.value=e}}let I=T(g),L=e(()=>u.id&&g.value?document.querySelector(`[for="${u.id}"]`)?.innerText:void 0);return F({disabled:D,state:j}),(e,r)=>(y(),_(v(t(S)?.rovingFocus.value?t(x):t(l)),d(e.$attrs,{id:e.id,ref:t(h),role:`checkbox`,"as-child":e.asChild,as:e.as,type:e.as===`button`?`button`:void 0,"aria-checked":t(M)(j.value)?`mixed`:j.value,"aria-required":e.required,"aria-label":e.$attrs[`aria-label`]||L.value,"data-state":t(N)(j.value),"data-disabled":D.value?``:void 0,disabled:D.value,focusable:t(S)?.rovingFocus.value?!D.value:void 0,onKeydown:o(a(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:n(()=>[i(e.$slots,`default`,{modelValue:t(E),state:j.value}),t(I)&&e.name&&!t(S)?(y(),_(t(b),{key:0,type:`checkbox`,checked:!!j.value,name:e.name,value:e.value,disabled:D.value,required:e.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):f(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=g({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(e){let{forwardRef:r}=p(),a=P();return(e,o)=>(y(),_(t(O),{present:e.forceMount||t(M)(t(a).state.value)||t(a).state.value===!0},{default:n(()=>[m(t(l),d({ref:t(r),"data-state":t(N)(t(a).state.value),"data-disabled":t(a).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:n(()=>[i(e.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=g({__name:`Checkbox`,props:{defaultValue:{},modelValue:{},disabled:{type:Boolean},value:{},id:{},trueValue:{},falseValue:{},asChild:{type:Boolean},as:{},name:{},required:{type:Boolean},class:{type:[Boolean,null,String,Object,Array]}},emits:[`update:modelValue`],setup(e,{emit:a}){let o=e,c=a,l=S(r(o,`class`),c);return(e,r)=>(y(),_(t(I),d({"data-slot":`checkbox`},t(l),{class:t(h)(`border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-md border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50`,o.class)}),{default:n(r=>[m(t(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:n(()=>[i(e.$slots,`default`,s(u(r)),()=>[m(t(D))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
|
package/frontend-dist/assets/{CollapsibleContent-D5uuYzAy.js → CollapsibleContent-vZSsPw6t.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$ as e,Bt as t,Gt as n,Jt as r,Mt as i,St as a,Zt as o,_t as s,f as c,h as l,ht as u,i as d,kt as f,lt as p,mt as m,nt as h,o as g,ot as _,st as v,tt as y,yt as b}from"./button-
|
|
1
|
+
import{$ as e,Bt as t,Gt as n,Jt as r,Mt as i,St as a,Zt as o,_t as s,f as c,h as l,ht as u,i as d,kt as f,lt as p,mt as m,nt as h,o as g,ot as _,st as v,tt as y,yt as b}from"./button-CEjoKdKq.js";import{H as x,V as S,X as C,z as w}from"./index-DE_N-dT0.js";var[T,E]=C(`CollapsibleRoot`),D=v({__name:`CollapsibleRoot`,props:{defaultOpen:{type:Boolean,required:!1,default:!1},open:{type:Boolean,required:!1,default:void 0},disabled:{type:Boolean,required:!1},unmountOnHide:{type:Boolean,required:!1,default:!0},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`update:open`],setup(e,{expose:t,emit:o}){let s=e,c=l(s,`open`,o,{defaultValue:s.defaultOpen,passive:s.open===void 0}),{disabled:u,unmountOnHide:f}=n(s);return E({contentId:``,disabled:u,open:c,unmountOnHide:f,onOpenToggle:()=>{u.value||(c.value=!c.value)}}),t({open:c}),g(),(e,t)=>(b(),y(r(d),{as:e.as,"as-child":s.asChild,"data-state":r(c)?`open`:`closed`,"data-disabled":r(u)?``:void 0},{default:i(()=>[a(e.$slots,`default`,{open:r(c)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=v({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(n,{emit:o}){let l=n,p=o,v=T();v.contentId||=S(void 0,`reka-collapsible-content`);let x=t(),{forwardRef:C,currentElement:E}=g(),D=t(0),O=t(0),k=e(()=>v.open.value),A=t(k.value),j=t();f(()=>[k.value,x.value?.present],async()=>{await u();let e=E.value;if(!e)return;j.value=j.value||{transitionDuration:e.style.transitionDuration,animationName:e.style.animationName},e.style.transitionDuration=`0s`,e.style.animationName=`none`;let t=e.getBoundingClientRect();O.value=t.height,D.value=t.width,A.value||(e.style.transitionDuration=j.value.transitionDuration,e.style.animationName=j.value.animationName)},{immediate:!0});let M=e(()=>A.value&&v.open.value);return s(()=>{requestAnimationFrame(()=>{A.value=!1})}),c(E,`beforematch`,e=>{requestAnimationFrame(()=>{v.onOpenToggle(),p(`contentFound`)})}),(e,t)=>(b(),y(r(w),{ref_key:`presentRef`,ref:x,present:e.forceMount||r(v).open.value,"force-mount":!0},{default:i(({present:t})=>[_(r(d),m(e.$attrs,{id:r(v).contentId,ref:r(C),"as-child":l.asChild,as:e.as,hidden:t?void 0:r(v).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:r(v).open.value?`open`:`closed`,"data-disabled":r(v).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:i(()=>[!r(v).unmountOnHide.value||t?a(e.$slots,`default`,{key:0}):h(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=v({__name:`Collapsible`,props:{defaultOpen:{type:Boolean},open:{type:Boolean},disabled:{type:Boolean},unmountOnHide:{type:Boolean},asChild:{type:Boolean},as:{}},emits:[`update:open`],setup(e,{emit:t}){let n=x(e,t);return(e,t)=>(b(),y(r(D),m({"data-slot":`collapsible`},r(n)),{default:i(t=>[a(e.$slots,`default`,o(p(t)))]),_:3},16))}}),A=v({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,n)=>(b(),y(r(O),m({"data-slot":`collapsible-content`},t),{default:i(()=>[a(e.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};
|
package/frontend-dist/assets/{CollapsibleTrigger-7lOShWej.js → CollapsibleTrigger-BBdH_cCm.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Jt as e,Mt as t,St as n,i as r,mt as i,o as a,st as o,tt as s,yt as c}from"./button-
|
|
1
|
+
import{Jt as e,Mt as t,St as n,i as r,mt as i,o as a,st as o,tt as s,yt as c}from"./button-CEjoKdKq.js";import{r as l}from"./CollapsibleContent-vZSsPw6t.js";var u=o({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(i){let o=i;a();let u=l();return(i,a)=>(c(),s(e(r),{type:i.as===`button`?`button`:void 0,as:i.as,"as-child":o.asChild,"aria-controls":e(u).contentId,"aria-expanded":e(u).open.value,"data-state":e(u).open.value?`open`:`closed`,"data-disabled":e(u).disabled?.value?``:void 0,disabled:e(u).disabled?.value,onClick:e(u).onOpenToggle},{default:t(()=>[n(i.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),d=o({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(r){let a=r;return(r,o)=>(c(),s(e(u),i({"data-slot":`collapsible-trigger`},a),{default:t(()=>[n(r.$slots,`default`)]),_:3},16))}});export{d as t};
|