llm-simple-router 0.9.19 → 0.9.21
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/config/recommended-retry-rules.json +1 -0
- package/dist/admin/providers.js +22 -6
- package/dist/admin/setup.js +2 -1
- package/dist/config/model-context.d.ts +2 -0
- package/dist/config/model-context.js +14 -6
- package/dist/core/types.d.ts +5 -0
- package/dist/db/index.js +45 -0
- package/dist/db/migrations/040_models_object_format.sql +4 -0
- package/dist/db/providers.d.ts +4 -0
- package/dist/db/providers.js +31 -0
- package/dist/middleware/admin-auth.js +2 -1
- package/dist/proxy/handler/proxy-handler.js +19 -1
- package/dist/proxy/transport/stream.d.ts +4 -1
- package/dist/proxy/transport/stream.js +21 -6
- package/dist/proxy/transport/transport-fn.d.ts +4 -0
- package/dist/proxy/transport/transport-fn.js +1 -1
- package/dist/utils/cookie-secure.d.ts +3 -0
- package/dist/utils/cookie-secure.js +8 -0
- package/frontend-dist/assets/CardContent-DmBQARma.js +1 -0
- package/frontend-dist/assets/CardTitle-DgwtSVqy.js +1 -0
- package/frontend-dist/assets/Checkbox-DgWzyASL.js +1 -0
- package/frontend-dist/assets/CollapsibleContent-BjJrE5cf.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-C73cozLg.js +1 -0
- package/frontend-dist/assets/{Dashboard-yhOvjTlI.js → Dashboard-D47Lzd5i.js} +2 -2
- package/frontend-dist/assets/{Input-1iFstr5N.js → Input-glS226Q6.js} +1 -1
- package/frontend-dist/assets/Label-B2lstY9k.js +1 -0
- package/frontend-dist/assets/Login-B5v_CIaw.js +1 -0
- package/frontend-dist/assets/Logs-CC6e8ty3.js +1 -0
- package/frontend-dist/assets/MappingEntryEditor-auxWvl8S.js +1 -0
- package/frontend-dist/assets/{ModelCard-PiwXmas-.js → ModelCard-DixuS062.js} +1 -1
- package/frontend-dist/assets/ModelMappings-DtmMS2o6.js +1 -0
- package/frontend-dist/assets/Monitor-F6DD03LF.js +1 -0
- package/frontend-dist/assets/Providers-DZkl5ryZ.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-C1uXuPZm.js +1 -0
- package/frontend-dist/assets/QuickSetup-BEeZJQ3g.js +1 -0
- package/frontend-dist/assets/RetryRules-plJdXlmH.js +1 -0
- package/frontend-dist/assets/RouterKeys-CLh6XISQ.js +1 -0
- package/frontend-dist/assets/{RovingFocusItem-WFfF7xTx.js → RovingFocusItem-BnqO1JTY.js} +1 -1
- package/frontend-dist/assets/Schedules-btnuRbYc.js +1 -0
- package/frontend-dist/assets/Settings-CbQaUkQ5.js +6 -0
- package/frontend-dist/assets/Setup-8yOUQ97d.js +1 -0
- package/frontend-dist/assets/Switch-BwPbL6O4.js +1 -0
- package/frontend-dist/assets/TooltipTrigger-D4gYyjGF.js +1 -0
- package/frontend-dist/assets/TransformRulesForm-C6PBk8zI.js +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-rmWxp09D.js +3 -0
- package/frontend-dist/assets/VisuallyHiddenInput-BPy71v35.js +1 -0
- package/frontend-dist/assets/{button-DMmB7tDy.js → button-DRFSckEU.js} +2 -2
- package/frontend-dist/assets/{copy-3gKHS1GQ.js → copy-BsWNnxok.js} +1 -1
- package/frontend-dist/assets/dialog-DxoN_Irc.js +1 -0
- package/frontend-dist/assets/index-Dr-BkaVu.js +3 -0
- package/frontend-dist/assets/index-iEMoIOdZ.css +1 -0
- package/frontend-dist/assets/{trash-2-C8JRtoE6.js → trash-2-DZ6KuutY.js} +1 -1
- package/frontend-dist/assets/{useClipboard-Df2zt0gp.js → useClipboard-B0s-K67s.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-D79yrt_t.js → useLogRetention-BS-AFvlE.js} +1 -1
- package/frontend-dist/index.html +3 -3
- package/package.json +1 -1
- package/frontend-dist/assets/CardContent-DpdNrtkG.js +0 -1
- package/frontend-dist/assets/CardTitle-mrTD9vua.js +0 -1
- package/frontend-dist/assets/Checkbox-1rtzAXdv.js +0 -1
- package/frontend-dist/assets/CollapsibleContent-Gd8G1WPI.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-DEs_2Gam.js +0 -1
- package/frontend-dist/assets/Label-CRw4oQf2.js +0 -1
- package/frontend-dist/assets/Login-BJLsCn5J.js +0 -1
- package/frontend-dist/assets/Logs-DhCuj7rp.js +0 -1
- package/frontend-dist/assets/MappingEntryEditor-B7cc69AQ.js +0 -1
- package/frontend-dist/assets/ModelMappings-8u8jBFoA.js +0 -1
- package/frontend-dist/assets/Monitor-UFRtv7Oi.js +0 -1
- package/frontend-dist/assets/Providers-CyUi2DVb.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-BxZCYxBL.js +0 -1
- package/frontend-dist/assets/QuickSetup-CYVEtfPZ.js +0 -1
- package/frontend-dist/assets/RetryRules-DXU-NS1-.js +0 -1
- package/frontend-dist/assets/RouterKeys-CUa64rMC.js +0 -1
- package/frontend-dist/assets/Schedules-DJboHZQe.js +0 -1
- package/frontend-dist/assets/Settings-C00E7rLh.js +0 -6
- package/frontend-dist/assets/Setup-Bdg3g-7K.js +0 -1
- package/frontend-dist/assets/Switch-DXVDTQ8F.js +0 -1
- package/frontend-dist/assets/TooltipTrigger-BcM0p009.js +0 -1
- package/frontend-dist/assets/TransformRulesForm-BhiHKW55.js +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-DniIXKEa.js +0 -3
- package/frontend-dist/assets/VisuallyHiddenInput-Bae_wVf3.js +0 -1
- package/frontend-dist/assets/dialog-B68sJflZ.js +0 -1
- package/frontend-dist/assets/index-BdA9t4qo.css +0 -1
- package/frontend-dist/assets/index-QPBBCV_W.js +0 -3
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
{ "name": "ZAI 速率限制 (HTTP 200, code 1302)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1302\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
|
|
8
8
|
{ "name": "ZAI SSE 错误 (HTTP 200, code 500)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"500\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
|
|
9
9
|
{ "name": "ZAI SSE 错误 (HTTP 200, code 1234)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1234\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
|
|
10
|
+
{ "name": "ZAI 模型过载 (HTTP 200, code 1305)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1305\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
|
|
10
11
|
{ "name": "KIMI 401 认证错误", "status_code": 401, "body_pattern": ".*authentication_error.*", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 3, "max_delay_ms": 60000, "providers": ["月之暗面"] }
|
|
11
12
|
]
|
package/dist/admin/providers.js
CHANGED
|
@@ -58,10 +58,24 @@ function cascadeProviderDisable(db, providerId) {
|
|
|
58
58
|
return result;
|
|
59
59
|
}
|
|
60
60
|
function extractModelOverrides(models) {
|
|
61
|
-
const entries =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const entries = [];
|
|
62
|
+
const overrides = [];
|
|
63
|
+
for (const m of models) {
|
|
64
|
+
if (typeof m === "string") {
|
|
65
|
+
entries.push({ name: m, patches: [] });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const name = m.name ?? m.id;
|
|
69
|
+
if (!name)
|
|
70
|
+
continue;
|
|
71
|
+
const entry = { name, patches: m.patches ?? [] };
|
|
72
|
+
if (m.stream_timeout_ms != null)
|
|
73
|
+
entry.stream_timeout_ms = m.stream_timeout_ms;
|
|
74
|
+
entries.push(entry);
|
|
75
|
+
if (m.name != null && m.context_window != null) {
|
|
76
|
+
overrides.push({ name: m.name, context_window: m.context_window });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
65
79
|
return { entries, overrides };
|
|
66
80
|
}
|
|
67
81
|
const API_KEY_PREVIEW_PREFIX_LEN = 4;
|
|
@@ -74,7 +88,8 @@ const CreateProviderSchema = Type.Object({
|
|
|
74
88
|
api_key: Type.String({ minLength: 1 }),
|
|
75
89
|
models: Type.Optional(Type.Array(Type.Union([
|
|
76
90
|
Type.String(),
|
|
77
|
-
Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())) })
|
|
91
|
+
Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) }),
|
|
92
|
+
Type.Object({ id: Type.String(), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) })
|
|
78
93
|
]))),
|
|
79
94
|
is_active: Type.Optional(Type.Number()),
|
|
80
95
|
max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
@@ -90,7 +105,8 @@ const UpdateProviderSchema = Type.Object({
|
|
|
90
105
|
api_key: Type.Optional(Type.String({ minLength: 1 })),
|
|
91
106
|
models: Type.Optional(Type.Array(Type.Union([
|
|
92
107
|
Type.String(),
|
|
93
|
-
Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())) })
|
|
108
|
+
Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) }),
|
|
109
|
+
Type.Object({ id: Type.String(), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) })
|
|
94
110
|
]))),
|
|
95
111
|
is_active: Type.Optional(Type.Number()),
|
|
96
112
|
max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
|
package/dist/admin/setup.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import jwt from "jsonwebtoken";
|
|
3
3
|
import { getSetting, setSetting, isInitialized } from "../db/settings.js";
|
|
4
4
|
import { hashPassword } from "../utils/password.js";
|
|
5
|
+
import { isForwardedProtoHttps } from "../utils/cookie-secure.js";
|
|
5
6
|
import { HTTP_BAD_REQUEST, HTTP_CONFLICT } from "./constants.js";
|
|
6
7
|
import { API_CODE, apiError } from "./api-response.js";
|
|
7
8
|
const CRYPTO_BYTES_LENGTH = 32;
|
|
@@ -38,7 +39,7 @@ export const adminSetupRoutes = (app, options, done) => {
|
|
|
38
39
|
reply.setCookie("admin_token", token, {
|
|
39
40
|
path: "/admin",
|
|
40
41
|
httpOnly: true,
|
|
41
|
-
secure:
|
|
42
|
+
secure: request.protocol === "https" || isForwardedProtoHttps(request),
|
|
42
43
|
sameSite: "lax",
|
|
43
44
|
maxAge: TOKEN_EXPIRY_SECONDS,
|
|
44
45
|
});
|
|
@@ -2,11 +2,13 @@ export interface ModelInfo {
|
|
|
2
2
|
name: string;
|
|
3
3
|
context_window: number | null;
|
|
4
4
|
patches: string[];
|
|
5
|
+
stream_timeout_ms?: number;
|
|
5
6
|
}
|
|
6
7
|
export interface ModelEntry {
|
|
7
8
|
name: string;
|
|
8
9
|
context_window?: number;
|
|
9
10
|
patches?: string[];
|
|
11
|
+
stream_timeout_ms?: number;
|
|
10
12
|
}
|
|
11
13
|
export declare const MODEL_CONTEXT_WINDOWS: Record<string, number>;
|
|
12
14
|
export declare const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
@@ -102,10 +102,13 @@ export function parseModels(raw) {
|
|
|
102
102
|
const obj = item;
|
|
103
103
|
if (!obj || !obj.name)
|
|
104
104
|
return null;
|
|
105
|
-
|
|
105
|
+
const result = {
|
|
106
106
|
name: obj.name,
|
|
107
107
|
patches: (obj.patches ?? []).map(normalizePatchName),
|
|
108
108
|
};
|
|
109
|
+
if (obj.stream_timeout_ms != null)
|
|
110
|
+
result.stream_timeout_ms = obj.stream_timeout_ms;
|
|
111
|
+
return result;
|
|
109
112
|
}).filter((e) => e !== null);
|
|
110
113
|
}
|
|
111
114
|
catch {
|
|
@@ -113,9 +116,14 @@ export function parseModels(raw) {
|
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
export function buildModelInfoList(modelEntries, overrides) {
|
|
116
|
-
return modelEntries.map(entry =>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
return modelEntries.map(entry => {
|
|
120
|
+
const info = {
|
|
121
|
+
name: entry.name,
|
|
122
|
+
context_window: overrides.get(entry.name) ?? lookupContextWindow(entry.name),
|
|
123
|
+
patches: entry.patches ?? [],
|
|
124
|
+
};
|
|
125
|
+
if (entry.stream_timeout_ms != null)
|
|
126
|
+
info.stream_timeout_ms = entry.stream_timeout_ms;
|
|
127
|
+
return info;
|
|
128
|
+
});
|
|
121
129
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -71,6 +71,11 @@ export type TransportResult = {
|
|
|
71
71
|
metrics?: MetricsResult;
|
|
72
72
|
upstreamResponseHeaders?: Record<string, string>;
|
|
73
73
|
sentHeaders: Record<string, string>;
|
|
74
|
+
timeoutContext?: {
|
|
75
|
+
modelId: string;
|
|
76
|
+
providerId: string;
|
|
77
|
+
};
|
|
78
|
+
timeoutMs?: number;
|
|
74
79
|
} | {
|
|
75
80
|
kind: "error";
|
|
76
81
|
statusCode: number;
|
package/dist/db/index.js
CHANGED
|
@@ -84,8 +84,53 @@ export function initDatabase(dbPath) {
|
|
|
84
84
|
throw err;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
// 应用层迁移:SQL 无法安全处理的转换
|
|
88
|
+
runApplicationMigrations(db);
|
|
87
89
|
return db;
|
|
88
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* 应用层迁移:需要 Node.js 逻辑处理的 DB 转换。
|
|
93
|
+
* 在 SQL migration 执行完毕后运行。
|
|
94
|
+
*/
|
|
95
|
+
function runApplicationMigrations(db) {
|
|
96
|
+
// 040: providers.models 从字符串数组转为对象数组
|
|
97
|
+
// ["glm-5.1"] → [{"id":"glm-5.1"}]
|
|
98
|
+
// 已有对象数组({name, patches})→ 补充 id 字段
|
|
99
|
+
const markerKey = "app_migration_040_models_object_format";
|
|
100
|
+
const done = db.prepare("SELECT value FROM settings WHERE key = ?").get(markerKey);
|
|
101
|
+
if (done)
|
|
102
|
+
return;
|
|
103
|
+
const providers = db.prepare("SELECT id, models FROM providers").all();
|
|
104
|
+
const update = db.prepare("UPDATE providers SET models = ? WHERE id = ?");
|
|
105
|
+
db.transaction(() => {
|
|
106
|
+
for (const p of providers) {
|
|
107
|
+
try {
|
|
108
|
+
const raw = JSON.parse(p.models);
|
|
109
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
110
|
+
continue;
|
|
111
|
+
// 已是对象数组且每个元素都有 id → 无需转换
|
|
112
|
+
if (raw.every((m) => typeof m === "object" && m !== null && "id" in m))
|
|
113
|
+
continue;
|
|
114
|
+
const converted = raw.map((m) => {
|
|
115
|
+
if (typeof m === "string")
|
|
116
|
+
return { id: m };
|
|
117
|
+
const obj = m;
|
|
118
|
+
if (typeof obj !== "object" || obj === null)
|
|
119
|
+
return null;
|
|
120
|
+
// 已有 id → 保留;有 name 无 id → 用 name 作 id
|
|
121
|
+
if ("id" in obj)
|
|
122
|
+
return obj;
|
|
123
|
+
if ("name" in obj)
|
|
124
|
+
return { id: obj.name, ...obj };
|
|
125
|
+
return obj;
|
|
126
|
+
}).filter((m) => m !== null);
|
|
127
|
+
update.run(JSON.stringify(converted), p.id);
|
|
128
|
+
}
|
|
129
|
+
catch { /* JSON parse failed — skip this provider's models conversion */ } // eslint-disable-line taste/no-silent-catch
|
|
130
|
+
}
|
|
131
|
+
db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run(markerKey, "done");
|
|
132
|
+
})();
|
|
133
|
+
}
|
|
89
134
|
// --- Re-export from per-table modules ---
|
|
90
135
|
export { getActiveProviders, getAllProviders, getProviderById, getActiveProviderByName, getActiveProvidersWithModels, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
|
|
91
136
|
export { getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
|
package/dist/db/providers.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface Provider {
|
|
|
16
16
|
created_at: string;
|
|
17
17
|
updated_at: string;
|
|
18
18
|
}
|
|
19
|
+
/** 默认流式超时 10 分钟 */
|
|
20
|
+
export declare const DEFAULT_STREAM_TIMEOUT_MS = 600000;
|
|
21
|
+
/** 从 provider 的 models JSON 中查找指定模型的超时值 */
|
|
22
|
+
export declare function getModelStreamTimeout(provider: Provider, backendModel: string): number;
|
|
19
23
|
export declare const PROVIDER_CONCURRENCY_DEFAULTS: {
|
|
20
24
|
readonly max_concurrency: 0;
|
|
21
25
|
readonly queue_timeout_ms: 0;
|
package/dist/db/providers.js
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import { buildUpdateQuery, deleteById } from "./helpers.js";
|
|
3
|
+
/** 默认流式超时 10 分钟 */
|
|
4
|
+
export const DEFAULT_STREAM_TIMEOUT_MS = 600_000;
|
|
5
|
+
/** 从 provider 的 models JSON 中查找指定模型的超时值 */
|
|
6
|
+
export function getModelStreamTimeout(provider, backendModel) {
|
|
7
|
+
try {
|
|
8
|
+
const raw = JSON.parse(provider.models);
|
|
9
|
+
if (!Array.isArray(raw))
|
|
10
|
+
return DEFAULT_STREAM_TIMEOUT_MS;
|
|
11
|
+
for (const m of raw) {
|
|
12
|
+
if (typeof m === "string") {
|
|
13
|
+
if (m === backendModel)
|
|
14
|
+
return DEFAULT_STREAM_TIMEOUT_MS;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const obj = m;
|
|
18
|
+
if (!obj || typeof obj !== "object")
|
|
19
|
+
continue;
|
|
20
|
+
const modelId = (obj.name ?? obj.id);
|
|
21
|
+
if (modelId === backendModel) {
|
|
22
|
+
const timeout = obj.stream_timeout_ms;
|
|
23
|
+
// stream_timeout_ms: 0 表示禁用超时,返回 Infinity;
|
|
24
|
+
// undefined/null/未设置 表示使用默认值
|
|
25
|
+
if (timeout === 0)
|
|
26
|
+
return Number.POSITIVE_INFINITY;
|
|
27
|
+
return timeout ?? DEFAULT_STREAM_TIMEOUT_MS;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore parse errors — models field may be empty or invalid */ } // eslint-disable-line taste/no-silent-catch
|
|
32
|
+
return DEFAULT_STREAM_TIMEOUT_MS;
|
|
33
|
+
}
|
|
3
34
|
export const PROVIDER_CONCURRENCY_DEFAULTS = {
|
|
4
35
|
max_concurrency: 0,
|
|
5
36
|
queue_timeout_ms: 0,
|
|
@@ -3,6 +3,7 @@ import cookie from "@fastify/cookie";
|
|
|
3
3
|
import jwt from "jsonwebtoken";
|
|
4
4
|
import { isInitialized, getSetting } from "../db/settings.js";
|
|
5
5
|
import { verifyPassword } from "../utils/password.js";
|
|
6
|
+
import { isForwardedProtoHttps } from "../utils/cookie-secure.js";
|
|
6
7
|
import { API_CODE, apiError } from "../admin/api-response.js";
|
|
7
8
|
const HTTP_UNAUTHORIZED = 401;
|
|
8
9
|
const adminAuthRaw = (app, options, done) => {
|
|
@@ -61,7 +62,7 @@ export const adminLoginRoutes = (app, options, done) => {
|
|
|
61
62
|
reply.setCookie("admin_token", token, {
|
|
62
63
|
path: "/admin",
|
|
63
64
|
httpOnly: true,
|
|
64
|
-
secure:
|
|
65
|
+
secure: request.protocol === "https" || isForwardedProtoHttps(request),
|
|
65
66
|
sameSite: "lax",
|
|
66
67
|
maxAge: TOKEN_EXPIRY_SECONDS,
|
|
67
68
|
});
|
|
@@ -3,6 +3,7 @@ import { HTTP_UNPROCESSABLE_ENTITY } from "../../core/constants.js";
|
|
|
3
3
|
import { getProviderById, insertRequestLog, updateLogStreamContent, updateLogClientStatus } from "../../db/index.js";
|
|
4
4
|
import { decrypt } from "../../utils/crypto.js";
|
|
5
5
|
import { getSetting } from "../../db/settings.js";
|
|
6
|
+
import { getModelStreamTimeout } from "../../db/providers.js";
|
|
6
7
|
import { resolveMapping } from "../routing/mapping-resolver.js";
|
|
7
8
|
import { SemaphoreQueueFullError, SemaphoreTimeoutError } from "@llm-router/core";
|
|
8
9
|
import { logResilienceResult, collectTransportMetrics, sanitizeHeadersForLog, } from "../proxy-logging.js";
|
|
@@ -279,8 +280,9 @@ async function executeFailoverLoop(ctx) {
|
|
|
279
280
|
const transportFn = buildTransportFn({
|
|
280
281
|
provider, apiKey, body: patchedBody, cliHdrs, reply, upstreamPath: effectiveUpstreamPath, apiType: effectiveApiType,
|
|
281
282
|
isStream, startTime, logId, effectiveModel,
|
|
282
|
-
streamTimeoutMs:
|
|
283
|
+
streamTimeoutMs: getModelStreamTimeout(provider, resolved.backend_model), tracker, matcher, request,
|
|
283
284
|
streamLoopEnabled, formatTransform, responseTransform, injectedHeaders,
|
|
285
|
+
timeoutContext: { modelId: resolved.backend_model, providerId: provider.id },
|
|
284
286
|
});
|
|
285
287
|
const pipelineSnapshot = iterationSnapshot.toJSON();
|
|
286
288
|
try {
|
|
@@ -293,6 +295,22 @@ async function executeFailoverLoop(ctx) {
|
|
|
293
295
|
matcher, logFileWriter,
|
|
294
296
|
}, resilienceResult.attempts, resilienceResult.result, startTime);
|
|
295
297
|
collectTransportMetrics(deps.db, apiType, resilienceResult.result, isStream, lastLogId, provider.id, resolved.backend_model, request, routerKeyId, getTransportStatusCode(resilienceResult.result));
|
|
298
|
+
// Stream timeout: send 408 error with API-specific body to client
|
|
299
|
+
if (resilienceResult.result.kind === "stream_abort" && resilienceResult.result.timeoutContext) {
|
|
300
|
+
const { modelId, providerId } = resilienceResult.result.timeoutContext;
|
|
301
|
+
const msg = `Stream timeout: no data received for ${resilienceResult.result.timeoutMs}ms (model: ${modelId}, provider: ${providerId})`;
|
|
302
|
+
const errBody = apiType === "anthropic"
|
|
303
|
+
? { type: "error", error: { type: "api_error", message: msg } }
|
|
304
|
+
: { error: { message: msg, type: "server_error", code: "stream_timeout" } };
|
|
305
|
+
try {
|
|
306
|
+
reply.raw.write(`data: ${JSON.stringify(errBody)}\n\n`);
|
|
307
|
+
}
|
|
308
|
+
catch { /* client disconnected */ } // eslint-disable-line taste/no-silent-catch
|
|
309
|
+
try {
|
|
310
|
+
reply.raw.end();
|
|
311
|
+
}
|
|
312
|
+
catch { /* client disconnected */ } // eslint-disable-line taste/no-silent-catch
|
|
313
|
+
}
|
|
296
314
|
const tr = resilienceResult.result;
|
|
297
315
|
const succeeded = tr.kind === "success" || tr.kind === "stream_success" || tr.kind === "stream_abort";
|
|
298
316
|
if (succeeded)
|
|
@@ -5,4 +5,7 @@ import type { StreamLoopGuard } from "@llm-router/core/loop-prevention";
|
|
|
5
5
|
import { type BuildHeadersFn } from "./http.js";
|
|
6
6
|
export declare function callStream(backend: {
|
|
7
7
|
base_url: string;
|
|
8
|
-
}, apiKey: string, body: Record<string, unknown>, clientHeaders: RawHeaders, reply: FastifyReply, timeoutMs: number, upstreamPath: string, buildHeaders: BuildHeadersFn, metricsTransform?: SSEMetricsTransform, checkEarlyError?: (bufferedData: string) => boolean, compatResolve?: (result: TransportResult) => void, loopGuard?: StreamLoopGuard, formatTransform?: import("stream").Transform
|
|
8
|
+
}, apiKey: string, body: Record<string, unknown>, clientHeaders: RawHeaders, reply: FastifyReply, timeoutMs: number, upstreamPath: string, buildHeaders: BuildHeadersFn, metricsTransform?: SSEMetricsTransform, checkEarlyError?: (bufferedData: string) => boolean, compatResolve?: (result: TransportResult) => void, loopGuard?: StreamLoopGuard, formatTransform?: import("stream").Transform, timeoutContext?: {
|
|
9
|
+
modelId: string;
|
|
10
|
+
providerId: string;
|
|
11
|
+
}, onTimeoutAbort?: () => void): Promise<TransportResult>;
|
|
@@ -12,6 +12,8 @@ class StreamProxy {
|
|
|
12
12
|
checkEarlyError;
|
|
13
13
|
timeoutMs;
|
|
14
14
|
loopGuard;
|
|
15
|
+
timeoutContext;
|
|
16
|
+
onTimeoutAbort;
|
|
15
17
|
state = "BUFFERING";
|
|
16
18
|
resolved = false;
|
|
17
19
|
resolveFn = null;
|
|
@@ -28,7 +30,7 @@ class StreamProxy {
|
|
|
28
30
|
// 流式阶段 SSE error 扫描缓冲(跨 chunk 边界匹配)
|
|
29
31
|
sseScanBuffer = "";
|
|
30
32
|
static SSE_SCAN_MAX = 8 * 1024; // eslint-disable-line no-magic-numbers -- 8KB scan buffer
|
|
31
|
-
constructor(statusCode, rawUpstreamHeaders, sentUpstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform) {
|
|
33
|
+
constructor(statusCode, rawUpstreamHeaders, sentUpstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform, timeoutContext, onTimeoutAbort) {
|
|
32
34
|
this.statusCode = statusCode;
|
|
33
35
|
this.sentUpstreamHeaders = sentUpstreamHeaders;
|
|
34
36
|
this.reply = reply;
|
|
@@ -36,6 +38,8 @@ class StreamProxy {
|
|
|
36
38
|
this.checkEarlyError = checkEarlyError;
|
|
37
39
|
this.timeoutMs = timeoutMs;
|
|
38
40
|
this.loopGuard = loopGuard;
|
|
41
|
+
this.timeoutContext = timeoutContext;
|
|
42
|
+
this.onTimeoutAbort = onTimeoutAbort;
|
|
39
43
|
this.formatTransform = formatTransform;
|
|
40
44
|
this.sseHeaders = filterHeaders(rawUpstreamHeaders);
|
|
41
45
|
this.sseHeaders["Content-Type"] = "text/event-stream";
|
|
@@ -79,7 +83,7 @@ class StreamProxy {
|
|
|
79
83
|
result = { kind: "stream_error", ...base, body: extra.body, headers: this.sseHeaders, headersSent: this.headersSent || undefined };
|
|
80
84
|
break;
|
|
81
85
|
case "stream_abort":
|
|
82
|
-
result = { kind: "stream_abort", ...base, metrics: extra.metrics };
|
|
86
|
+
result = { kind: "stream_abort", ...base, metrics: extra.metrics, timeoutContext: extra.timeoutContext, timeoutMs: extra.timeoutMs };
|
|
83
87
|
break;
|
|
84
88
|
}
|
|
85
89
|
// deferred 模式:先 resolve 让 handler 链路(日志写入等)在 microtask 中执行,
|
|
@@ -94,7 +98,8 @@ class StreamProxy {
|
|
|
94
98
|
}
|
|
95
99
|
else {
|
|
96
100
|
// stream_abort 且 headers 已发送时,必须 end reply 避免客户端挂起
|
|
97
|
-
|
|
101
|
+
// 但如果有 timeoutContext,让 handler 层负责写入错误 SSE 后再 end
|
|
102
|
+
if (kind === "stream_abort" && this.headersSent && !extra.timeoutContext) {
|
|
98
103
|
// eslint-disable-next-line taste/no-silent-catch -- reply may already be destroyed, warn is sufficient
|
|
99
104
|
try {
|
|
100
105
|
this.reply.raw.end();
|
|
@@ -132,10 +137,20 @@ class StreamProxy {
|
|
|
132
137
|
resetIdleTimer() {
|
|
133
138
|
if (this.idleTimer)
|
|
134
139
|
clearTimeout(this.idleTimer);
|
|
140
|
+
if (!isFinite(this.timeoutMs) || this.timeoutMs <= 0)
|
|
141
|
+
return; // 0 或 Infinity 表示禁用超时
|
|
135
142
|
this.idleTimer = setTimeout(() => {
|
|
136
143
|
if (this.resolved)
|
|
137
144
|
return;
|
|
138
|
-
|
|
145
|
+
// 在 terminal() 调用 reply.raw.end() 之前,同步写入超时错误 SSE
|
|
146
|
+
// 必须同步执行,确保 inject() 能正确收集响应体
|
|
147
|
+
if (this.onTimeoutAbort) {
|
|
148
|
+
try {
|
|
149
|
+
this.onTimeoutAbort();
|
|
150
|
+
}
|
|
151
|
+
catch { /* reply may be destroyed */ } // eslint-disable-line taste/no-silent-catch
|
|
152
|
+
}
|
|
153
|
+
this.terminal("stream_abort", { metrics: this.collectMetrics(false), timeoutContext: this.timeoutContext, timeoutMs: this.timeoutMs });
|
|
139
154
|
}, this.timeoutMs);
|
|
140
155
|
}
|
|
141
156
|
startStreaming() {
|
|
@@ -292,7 +307,7 @@ class StreamProxy {
|
|
|
292
307
|
}
|
|
293
308
|
}
|
|
294
309
|
// ---------- callStream ----------
|
|
295
|
-
export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError, compatResolve, loopGuard, formatTransform) {
|
|
310
|
+
export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError, compatResolve, loopGuard, formatTransform, timeoutContext, onTimeoutAbort) {
|
|
296
311
|
return new Promise((resolve) => {
|
|
297
312
|
const effectiveResolve = compatResolve ?? resolve;
|
|
298
313
|
const url = new URL(buildUpstreamUrl(backend.base_url, upstreamPath));
|
|
@@ -316,7 +331,7 @@ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutM
|
|
|
316
331
|
});
|
|
317
332
|
return;
|
|
318
333
|
}
|
|
319
|
-
const proxy = new StreamProxy(statusCode, upstreamRes.headers, upstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform);
|
|
334
|
+
const proxy = new StreamProxy(statusCode, upstreamRes.headers, upstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform, timeoutContext, onTimeoutAbort);
|
|
320
335
|
proxy.bindResolve(effectiveResolve);
|
|
321
336
|
proxy.registerCloseHandler();
|
|
322
337
|
// 无 early error checker 时直接开始流式传输
|
|
@@ -24,5 +24,9 @@ export interface TransportFnParams {
|
|
|
24
24
|
formatTransform?: import("stream").Transform;
|
|
25
25
|
responseTransform?: (body: string) => string;
|
|
26
26
|
injectedHeaders?: Record<string, string>;
|
|
27
|
+
timeoutContext?: {
|
|
28
|
+
modelId: string;
|
|
29
|
+
providerId: string;
|
|
30
|
+
};
|
|
27
31
|
}
|
|
28
32
|
export declare function buildTransportFn(p: TransportFnParams): (target: Target) => Promise<TransportResult>;
|
|
@@ -52,7 +52,7 @@ export function buildTransportFn(p) {
|
|
|
52
52
|
onContentDelta: streamLoopGuard ? (text) => streamLoopGuard.feed(text) : undefined,
|
|
53
53
|
});
|
|
54
54
|
const checkEarlyError = p.matcher ? (data) => p.matcher.test(UPSTREAM_SUCCESS, data) : undefined;
|
|
55
|
-
const streamResult = await callStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildHeaders, metricsTransform, checkEarlyError, undefined, streamLoopGuard, p.formatTransform);
|
|
55
|
+
const streamResult = await callStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildHeaders, metricsTransform, checkEarlyError, undefined, streamLoopGuard, p.formatTransform, p.timeoutContext);
|
|
56
56
|
const m = (streamResult.kind === "stream_success" || streamResult.kind === "stream_abort")
|
|
57
57
|
? streamResult.metrics : undefined;
|
|
58
58
|
if (m)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 判断请求是否经过 HTTPS(直连或反向代理) */
|
|
2
|
+
export function isForwardedProtoHttps(request) {
|
|
3
|
+
if (request.protocol === "https")
|
|
4
|
+
return true;
|
|
5
|
+
const forwarded = request.headers["x-forwarded-proto"];
|
|
6
|
+
const value = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
7
|
+
return value === "https";
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{$ as e,Vt as t,X as n,dt as r,mt as i,r as a,zt as o}from"./button-DRFSckEU.js";var s=[`data-size`],c=e({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(e){let c=e;return(l,u)=>(r(),n(`div`,{"data-slot":`card`,"data-size":e.size,class:t(o(a)(`ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-lg py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col`,c.class))},[i(l.$slots,`default`)],10,s))}}),l=e({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let s=e;return(e,c)=>(r(),n(`div`,{"data-slot":`card-content`,class:t(o(a)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[i(e.$slots,`default`)],2))}});export{c as n,l as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{$ as e,Vt as t,X as n,dt as r,mt as i,r as a,zt as o}from"./button-DRFSckEU.js";var s=e({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let s=e;return(e,c)=>(r(),n(`div`,{"data-slot":`card-header`,class:t(o(a)(`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))},[i(e.$slots,`default`)],2))}}),c=e({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(e){let s=e;return(e,c)=>(r(),n(`div`,{"data-slot":`card-title`,class:t(o(a)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[i(e.$slots,`default`)],2))}});export{s as n,c as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{$ as e,H as t,Ht as n,J as r,K as i,Q as a,U as o,Y as s,dt as c,gt as l,i as u,m as d,mt as f,o as p,ot as m,r as h,tt as g,wt as _,x as v,zt as y}from"./button-DRFSckEU.js";import{t as b}from"./VisuallyHiddenInput-BPy71v35.js";import{t as x}from"./RovingFocusItem-BnqO1JTY.js";import{B as S,G as C,H as w,L as T,Y as E,nt as D,q as O}from"./index-Dr-BkaVu.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>E(e,t)):E(e,t)}var[A,j]=O(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=O(`CheckboxRoot`),I=e({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(e,{emit:n}){let a=e,h=n,{forwardRef:g,currentElement:v}=p(),S=A(null),T=d(a,`modelValue`,h,{defaultValue:a.defaultValue??a.falseValue,passive:a.modelValue===void 0}),D=i(()=>S?.disabled.value||a.disabled),O=i(()=>E(T.value,a.trueValue)),j=i(()=>C(S?.modelValue.value)?T.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,a.value));function P(){if(C(S?.modelValue.value))T.value===`indeterminate`?T.value=a.trueValue:T.value=O.value?a.falseValue:a.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,a.value)){let t=e.findIndex(e=>E(e,a.value));e.splice(t,1)}else e.push(a.value);S.modelValue.value=e}}let I=w(v),L=i(()=>a.id&&v.value?document.querySelector(`[for="${a.id}"]`)?.innerText:void 0);return F({disabled:D,state:j}),(e,n)=>(c(),r(l(y(S)?.rovingFocus.value?y(x):y(u)),m(e.$attrs,{id:e.id,ref:y(g),role:`checkbox`,"as-child":e.asChild,as:e.as,type:e.as===`button`?`button`:void 0,"aria-checked":y(M)(j.value)?`mixed`:j.value,"aria-required":e.required,"aria-label":e.$attrs[`aria-label`]||L.value,"data-state":y(N)(j.value),"data-disabled":D.value?``:void 0,disabled:D.value,focusable:y(S)?.rovingFocus.value?!D.value:void 0,onKeydown:t(o(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:_(()=>[f(e.$slots,`default`,{modelValue:y(T),state:j.value}),y(I)&&e.name&&!y(S)?(c(),r(y(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`])):s(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=e({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(e){let{forwardRef:t}=p(),n=P();return(e,i)=>(c(),r(y(T),{present:e.forceMount||y(M)(y(n).state.value)||y(n).state.value===!0},{default:_(()=>[a(y(u),m({ref:y(t),"data-state":y(N)(y(n).state.value),"data-disabled":y(n).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:_(()=>[f(e.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=e({__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:t}){let i=e,o=t,s=S(v(i,`class`),o);return(e,t)=>(c(),r(y(I),m({"data-slot":`checkbox`},y(s),{class:y(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`,i.class)}),{default:_(t=>[a(y(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:_(()=>[f(e.$slots,`default`,n(g(t)),()=>[a(y(D))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{$ as e,Ht as t,J as n,K as r,Lt as i,Mt as a,Q as o,Y as s,d as c,dt as l,i as u,lt as d,m as f,mt as p,o as m,ot as h,st as g,tt as _,wt as v,xt as y,zt as b}from"./button-DRFSckEU.js";import{B as x,L as S,q as C,z as w}from"./index-Dr-BkaVu.js";var[T,E]=C(`CollapsibleRoot`),D=e({__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:r}){let a=e,o=f(a,`open`,r,{defaultValue:a.defaultOpen,passive:a.open===void 0}),{disabled:s,unmountOnHide:c}=i(a);return E({contentId:``,disabled:s,open:o,unmountOnHide:c,onOpenToggle:()=>{s.value||(o.value=!o.value)}}),t({open:o}),m(),(e,t)=>(l(),n(b(u),{as:e.as,"as-child":a.asChild,"data-state":b(o)?`open`:`closed`,"data-disabled":b(s)?``:void 0},{default:v(()=>[p(e.$slots,`default`,{open:b(o)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=e({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(e,{emit:t}){let i=e,f=t,_=T();_.contentId||=w(void 0,`reka-collapsible-content`);let x=a(),{forwardRef:C,currentElement:E}=m(),D=a(0),O=a(0),k=r(()=>_.open.value),A=a(k.value),j=a();y(()=>[k.value,x.value?.present],async()=>{await g();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=r(()=>A.value&&_.open.value);return d(()=>{requestAnimationFrame(()=>{A.value=!1})}),c(E,`beforematch`,e=>{requestAnimationFrame(()=>{_.onOpenToggle(),f(`contentFound`)})}),(e,t)=>(l(),n(b(S),{ref_key:`presentRef`,ref:x,present:e.forceMount||b(_).open.value,"force-mount":!0},{default:v(({present:t})=>[o(b(u),h(e.$attrs,{id:b(_).contentId,ref:b(C),"as-child":i.asChild,as:e.as,hidden:t?void 0:b(_).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:b(_).open.value?`open`:`closed`,"data-disabled":b(_).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:v(()=>[!b(_).unmountOnHide.value||t?p(e.$slots,`default`,{key:0}):s(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=e({__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:r}){let i=x(e,r);return(e,r)=>(l(),n(b(D),h({"data-slot":`collapsible`},b(i)),{default:v(n=>[p(e.$slots,`default`,t(_(n)))]),_:3},16))}}),A=e({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,r)=>(l(),n(b(O),h({"data-slot":`collapsible-content`},t),{default:v(()=>[p(e.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{$ as e,J as t,dt as n,i as r,mt as i,o as a,ot as o,wt as s,zt as c}from"./button-DRFSckEU.js";import{r as l}from"./CollapsibleContent-BjJrE5cf.js";var u=e({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(e){let o=e;a();let u=l();return(e,a)=>(n(),t(c(r),{type:e.as===`button`?`button`:void 0,as:e.as,"as-child":o.asChild,"aria-controls":c(u).contentId,"aria-expanded":c(u).open.value,"data-state":c(u).open.value?`open`:`closed`,"data-disabled":c(u).disabled?.value?``:void 0,disabled:c(u).disabled?.value,onClick:c(u).onOpenToggle},{default:s(()=>[i(e.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),d=e({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(e){let r=e;return(e,a)=>(n(),t(c(u),o({"data-slot":`collapsible-trigger`},r),{default:s(()=>[i(e.$slots,`default`)]),_:3},16))}});export{d as t};
|