llm-simple-router 0.9.33 → 0.10.1
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/providers.js +1 -1
- package/dist/admin/routes.js +5 -0
- package/dist/admin/schedules.js +1 -1
- package/dist/admin/upgrade.js +3 -3
- package/dist/config/recommended.js +2 -2
- package/dist/core/constants.d.ts +1 -0
- package/dist/core/constants.js +1 -0
- package/dist/core/container.d.ts +1 -0
- package/dist/core/container.js +1 -0
- package/dist/db/db-size-monitor.js +1 -1
- package/dist/db/log-cleaner.js +1 -1
- package/dist/db/settings.js +3 -3
- package/dist/index.js +44 -8
- package/dist/proxy/format/adapters/anthropic.d.ts +2 -0
- package/dist/proxy/format/adapters/anthropic.js +18 -0
- package/dist/proxy/format/adapters/openai.d.ts +2 -0
- package/dist/proxy/format/adapters/openai.js +14 -0
- package/dist/proxy/format/adapters/responses.d.ts +2 -0
- package/dist/proxy/format/adapters/responses.js +9 -0
- package/dist/proxy/format/adapters/shared-error-meta.d.ts +9 -0
- package/dist/proxy/format/adapters/shared-error-meta.js +14 -0
- package/dist/proxy/format/converters/anthropic-openai.d.ts +1 -0
- package/dist/proxy/format/converters/anthropic-openai.js +11 -0
- package/dist/proxy/format/converters/anthropic-responses.d.ts +1 -0
- package/dist/proxy/format/converters/anthropic-responses.js +11 -0
- package/dist/proxy/format/converters/openai-anthropic.d.ts +1 -0
- package/dist/proxy/format/converters/openai-anthropic.js +11 -0
- package/dist/proxy/format/converters/openai-responses.d.ts +1 -0
- package/dist/proxy/format/converters/openai-responses.js +11 -0
- package/dist/proxy/format/converters/responses-anthropic.d.ts +1 -0
- package/dist/proxy/format/converters/responses-anthropic.js +11 -0
- package/dist/proxy/format/converters/responses-openai.d.ts +1 -0
- package/dist/proxy/format/converters/responses-openai.js +11 -0
- package/dist/proxy/format/registry.d.ts +17 -0
- package/dist/proxy/format/registry.js +50 -0
- package/dist/proxy/format/types.d.ts +27 -0
- package/dist/proxy/format/types.js +16 -0
- package/dist/proxy/handler/create-proxy-handler.d.ts +14 -0
- package/dist/proxy/handler/create-proxy-handler.js +235 -0
- package/dist/proxy/handler/failover-loop.d.ts +19 -0
- package/dist/proxy/handler/failover-loop.js +407 -0
- package/dist/proxy/handler/proxy-handler-utils.js +1 -1
- package/dist/proxy/hooks/builtin/allowed-models.d.ts +12 -0
- package/dist/proxy/hooks/builtin/allowed-models.js +37 -0
- package/dist/proxy/hooks/builtin/enhancement-preprocess.d.ts +2 -0
- package/dist/proxy/hooks/builtin/enhancement-preprocess.js +84 -0
- package/dist/proxy/hooks/builtin/error-logging.d.ts +2 -0
- package/dist/proxy/hooks/builtin/error-logging.js +86 -0
- package/dist/proxy/hooks/builtin/overflow-redirect.d.ts +2 -0
- package/dist/proxy/hooks/builtin/overflow-redirect.js +39 -0
- package/dist/proxy/hooks/builtin/plugin-request.d.ts +2 -0
- package/dist/proxy/hooks/builtin/plugin-request.js +49 -0
- package/dist/proxy/hooks/builtin/provider-patches.d.ts +2 -0
- package/dist/proxy/hooks/builtin/provider-patches.js +36 -0
- package/dist/proxy/hooks/builtin/request-logging.d.ts +2 -0
- package/dist/proxy/hooks/builtin/request-logging.js +72 -0
- package/dist/proxy/hooks/plugin-bridge.d.ts +7 -0
- package/dist/proxy/hooks/plugin-bridge.js +106 -0
- package/dist/proxy/hooks/sse-event-transform.d.ts +13 -0
- package/dist/proxy/hooks/sse-event-transform.js +59 -0
- package/dist/proxy/orchestration/resilience.js +2 -3
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +1 -1
- package/dist/proxy/patch/deepseek/utils.js +1 -1
- package/dist/proxy/pipeline/context.d.ts +3 -0
- package/dist/proxy/pipeline/context.js +31 -0
- package/dist/proxy/pipeline/hook-registry.d.ts +20 -0
- package/dist/proxy/pipeline/hook-registry.js +24 -0
- package/dist/proxy/pipeline/pipeline.d.ts +13 -0
- package/dist/proxy/pipeline/pipeline.js +26 -0
- package/dist/proxy/pipeline/register-hooks.d.ts +1 -0
- package/dist/proxy/pipeline/register-hooks.js +23 -0
- package/dist/proxy/pipeline/types.d.ts +63 -0
- package/dist/proxy/pipeline/types.js +10 -0
- package/dist/proxy/proxy-core.js +1 -1
- package/dist/proxy/transform/message-mapper.js +8 -8
- package/dist/proxy/transform/plugin-types.d.ts +47 -17
- package/dist/proxy/transform/request-bridge-responses.js +21 -21
- package/dist/proxy/transform/request-transform-responses.js +24 -24
- package/dist/proxy/transform/request-transform.js +1 -3
- package/dist/proxy/transform/response-bridge-responses.js +13 -13
- package/dist/proxy/transform/response-transform-responses.js +10 -10
- package/dist/proxy/transform/sanitize.js +5 -1
- package/dist/proxy/transform/stream-transform-base.js +2 -2
- package/dist/proxy/transport/transport-fn.js +0 -1
- package/frontend-dist/assets/{CardContent-CPeHI_vO.js → CardContent-go9x8eec.js} +1 -1
- package/frontend-dist/assets/{CardTitle-DiF3FpGs.js → CardTitle-BRlxAJ4B.js} +1 -1
- package/frontend-dist/assets/{Checkbox-BpQYgA_C.js → Checkbox-CNapa1C_.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-CUb_IZ--.js → CollapsibleContent-C_hdLxxg.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-DSh5Vnmj.js → CollapsibleTrigger-DnPjb7Dr.js} +1 -1
- package/frontend-dist/assets/{Dashboard-DMaq-T6I.js → Dashboard-BD10zBkl.js} +1 -1
- package/frontend-dist/assets/{Input-C9E7es5X.js → Input-DY_kIwJO.js} +1 -1
- package/frontend-dist/assets/{Label-kvNNc1bh.js → Label-DwD9QWfW.js} +1 -1
- package/frontend-dist/assets/Login-DVqJBb-O.js +1 -0
- package/frontend-dist/assets/{Logs-zr32VuD0.js → Logs-3ckBDY_r.js} +1 -1
- package/frontend-dist/assets/{MappingEntryEditor-D6ABslWN.js → MappingEntryEditor-DiuJMr6L.js} +1 -1
- package/frontend-dist/assets/{ModelCard-Dn_8Lnr-.js → ModelCard-M7BUbO4-.js} +1 -1
- package/frontend-dist/assets/ModelMappings-CAYfszaz.js +1 -0
- package/frontend-dist/assets/{Monitor-CnQrBEqa.js → Monitor-DcJP6XHy.js} +1 -1
- package/frontend-dist/assets/{Providers-CK4mOZS5.js → Providers-DoB7cAWp.js} +1 -1
- package/frontend-dist/assets/{ProxyEnhancement-DQIrpH8f.js → ProxyEnhancement-CPiIq5wS.js} +1 -1
- package/frontend-dist/assets/QuickSetup-BN_YqbgQ.js +1 -0
- package/frontend-dist/assets/{RetryRules-Botm2DuB.js → RetryRules-DIxgHt-G.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-C9ZluPMi.js → RouterKeys-hBEzqWaf.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-B4cr2rFZ.js → RovingFocusItem-DXXOzxYf.js} +1 -1
- package/frontend-dist/assets/Schedules-C8FBA29A.js +1 -0
- package/frontend-dist/assets/{Settings-Dd9LXNyo.js → Settings-CsLsfZkl.js} +2 -2
- package/frontend-dist/assets/{Setup-DXTFJRxq.js → Setup-DE72fHWm.js} +1 -1
- package/frontend-dist/assets/{Switch-CkJ3KppU.js → Switch-QKmayVBf.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-DtY0_zHb.js → TooltipTrigger-CTzZDwpq.js} +1 -1
- package/frontend-dist/assets/{TransformRulesForm-DpFk5xx0.js → TransformRulesForm-B1xYEDqV.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-Cpn9RNu1.js → UnifiedRequestDialog-D2Rmt-2C.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-DvHAsFOO.js → VisuallyHiddenInput-C0PhLv6-.js} +1 -1
- package/frontend-dist/assets/{button-CjKaq4D4.js → button-DIkr5-e9.js} +2 -2
- package/frontend-dist/assets/{copy-BYleSlPg.js → copy-wxL8rXSb.js} +1 -1
- package/frontend-dist/assets/{dialog-DlHPBQX7.js → dialog-VRNiQWvn.js} +1 -1
- package/frontend-dist/assets/{index-C19mbJF3.js → index-BkaQT-Ad.js} +2 -2
- package/frontend-dist/assets/{trash-2-C6EEjS9z.js → trash-2-DP4nWvuZ.js} +1 -1
- package/frontend-dist/assets/{useClipboard-C_hBlc9t.js → useClipboard-tOb7LUo7.js} +1 -1
- package/frontend-dist/assets/useLogRetention-WLn9pgNe.js +1 -0
- package/frontend-dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/proxy/handler/anthropic.d.ts +0 -7
- package/dist/proxy/handler/anthropic.js +0 -43
- package/dist/proxy/handler/openai.d.ts +0 -7
- package/dist/proxy/handler/openai.js +0 -132
- package/dist/proxy/handler/proxy-handler.d.ts +0 -15
- package/dist/proxy/handler/proxy-handler.js +0 -430
- package/dist/proxy/handler/responses.d.ts +0 -7
- package/dist/proxy/handler/responses.js +0 -48
- package/dist/proxy/transform/transform-coordinator.d.ts +0 -12
- package/dist/proxy/transform/transform-coordinator.js +0 -151
- package/frontend-dist/assets/Login-CC1aMDOU.js +0 -1
- package/frontend-dist/assets/ModelMappings-BD7Xd9u_.js +0 -1
- package/frontend-dist/assets/QuickSetup-Cp1nlz0G.js +0 -1
- package/frontend-dist/assets/Schedules-C3JE7gox.js +0 -1
- package/frontend-dist/assets/useLogRetention-DZGdtwFB.js +0 -1
package/dist/admin/providers.js
CHANGED
|
@@ -418,7 +418,7 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
418
418
|
return reply.send(modelIds);
|
|
419
419
|
}
|
|
420
420
|
catch (err) {
|
|
421
|
-
const message = err instanceof Error ? err.message :
|
|
421
|
+
const message = err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err);
|
|
422
422
|
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, `连接上游失败: ${message}`));
|
|
423
423
|
}
|
|
424
424
|
});
|
package/dist/admin/routes.js
CHANGED
|
@@ -18,6 +18,7 @@ import { adminQuickSetupRoutes } from "./quick-setup.js";
|
|
|
18
18
|
import { adminImportExportRoutes } from "./settings-import-export.js";
|
|
19
19
|
import { adminTransformRuleRoutes } from "./transform-rules.js";
|
|
20
20
|
import { adminScheduleRoutes } from "./schedules.js";
|
|
21
|
+
import { hookRegistry } from "../proxy/pipeline/hook-registry.js";
|
|
21
22
|
export const adminRoutes = (app, options, done) => {
|
|
22
23
|
// Setup 路由不需要 auth
|
|
23
24
|
app.register(adminSetupRoutes, { db: options.db });
|
|
@@ -41,5 +42,9 @@ export const adminRoutes = (app, options, done) => {
|
|
|
41
42
|
app.register(adminQuickSetupRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController });
|
|
42
43
|
app.register(adminUpgradeRoutes, { db: options.db, closeFn: options.closeFn ?? (async () => { }) });
|
|
43
44
|
app.register(adminTransformRuleRoutes, { db: options.db, pluginRegistry: options.pluginRegistry });
|
|
45
|
+
// Pipeline hooks 查询
|
|
46
|
+
app.get("/admin/api/pipeline/hooks", async () => {
|
|
47
|
+
return { hooks: hookRegistry.getAll() };
|
|
48
|
+
});
|
|
44
49
|
done();
|
|
45
50
|
};
|
package/dist/admin/schedules.js
CHANGED
|
@@ -95,7 +95,7 @@ function checkOverlap(db, groupId, excludeId, weekDays, startHour, endHour) {
|
|
|
95
95
|
}
|
|
96
96
|
const HOUR_PAD_WIDTH = 2;
|
|
97
97
|
function formatHour(h) {
|
|
98
|
-
return
|
|
98
|
+
return h.toString().padStart(HOUR_PAD_WIDTH, "0") + ":00";
|
|
99
99
|
}
|
|
100
100
|
export const adminScheduleRoutes = (app, options, done) => {
|
|
101
101
|
const { db } = options;
|
package/dist/admin/upgrade.js
CHANGED
|
@@ -93,7 +93,7 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
93
93
|
return reply.send({ ok: true, version });
|
|
94
94
|
}
|
|
95
95
|
catch (err) {
|
|
96
|
-
const msg = err instanceof Error ? err.message :
|
|
96
|
+
const msg = err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err);
|
|
97
97
|
return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `升级失败: ${msg}`));
|
|
98
98
|
}
|
|
99
99
|
});
|
|
@@ -125,7 +125,7 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
125
125
|
process.exit(0);
|
|
126
126
|
}
|
|
127
127
|
catch (err) {
|
|
128
|
-
const msg = err instanceof Error ? err.message :
|
|
128
|
+
const msg = err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err);
|
|
129
129
|
req.log.error({ err }, `Restart failed: ${msg}`);
|
|
130
130
|
process.exit(1);
|
|
131
131
|
}
|
|
@@ -162,7 +162,7 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
162
162
|
return reply.send({ ok: true });
|
|
163
163
|
}
|
|
164
164
|
catch (err) {
|
|
165
|
-
const msg = err instanceof Error ? err.message :
|
|
165
|
+
const msg = err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err);
|
|
166
166
|
return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `同步失败: ${msg}`));
|
|
167
167
|
}
|
|
168
168
|
});
|
|
@@ -12,7 +12,7 @@ function loadJson(filename) {
|
|
|
12
12
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
13
13
|
}
|
|
14
14
|
catch (err) {
|
|
15
|
-
process.stderr.write(`[recommended] 加载 ${filename} 失败: ${err instanceof Error ? err.message :
|
|
15
|
+
process.stderr.write(`[recommended] 加载 ${filename} 失败: ${err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err)}\n`);
|
|
16
16
|
return [];
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -31,7 +31,7 @@ export function getConfigVersions() {
|
|
|
31
31
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
32
32
|
}
|
|
33
33
|
catch (err) {
|
|
34
|
-
process.stderr.write(`[recommended] 加载 version.json 失败: ${err instanceof Error ? err.message :
|
|
34
|
+
process.stderr.write(`[recommended] 加载 version.json 失败: ${err instanceof Error ? err.message : err instanceof Error ? err.message : JSON.stringify(err)}\n`);
|
|
35
35
|
return { providers: 0, retryRules: 0 };
|
|
36
36
|
}
|
|
37
37
|
}
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const HTTP_CONFLICT = 409;
|
|
|
7
7
|
export declare const HTTP_UNPROCESSABLE_ENTITY = 422;
|
|
8
8
|
export declare const HTTP_INTERNAL_ERROR = 500;
|
|
9
9
|
export declare const HTTP_BAD_GATEWAY = 502;
|
|
10
|
+
export declare const HTTP_CLIENT_CLOSED = 499;
|
|
10
11
|
export declare const HTTP_SERVICE_UNAVAILABLE = 503;
|
|
11
12
|
export declare const PROXY_API_TYPES: Record<string, string>;
|
|
12
13
|
export declare function getProxyApiType(url: string): string | null;
|
package/dist/core/constants.js
CHANGED
|
@@ -9,6 +9,7 @@ export const HTTP_CONFLICT = 409;
|
|
|
9
9
|
export const HTTP_UNPROCESSABLE_ENTITY = 422;
|
|
10
10
|
export const HTTP_INTERNAL_ERROR = 500;
|
|
11
11
|
export const HTTP_BAD_GATEWAY = 502;
|
|
12
|
+
export const HTTP_CLIENT_CLOSED = 499; // nginx convention: client disconnected
|
|
12
13
|
export const HTTP_SERVICE_UNAVAILABLE = 503;
|
|
13
14
|
// api_type 路由映射:proxy path → api type,用于全局 hook/errorHandler 中识别代理请求
|
|
14
15
|
export const PROXY_API_TYPES = {
|
package/dist/core/container.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare const SERVICE_KEYS: {
|
|
|
8
8
|
readonly sessionTracker: "sessionTracker";
|
|
9
9
|
readonly adaptiveController: "adaptiveController";
|
|
10
10
|
readonly pluginRegistry: "pluginRegistry";
|
|
11
|
+
readonly formatRegistry: "formatRegistry";
|
|
11
12
|
readonly logFileWriter: "logFileWriter";
|
|
12
13
|
readonly proxyAgentFactory: "proxyAgentFactory";
|
|
13
14
|
};
|
package/dist/core/container.js
CHANGED
|
@@ -8,6 +8,7 @@ export const SERVICE_KEYS = {
|
|
|
8
8
|
sessionTracker: "sessionTracker",
|
|
9
9
|
adaptiveController: "adaptiveController",
|
|
10
10
|
pluginRegistry: "pluginRegistry",
|
|
11
|
+
formatRegistry: "formatRegistry",
|
|
11
12
|
logFileWriter: "logFileWriter",
|
|
12
13
|
proxyAgentFactory: "proxyAgentFactory",
|
|
13
14
|
};
|
|
@@ -57,7 +57,7 @@ export function scheduleDbSizeMonitor(db, dbPath, options) {
|
|
|
57
57
|
}
|
|
58
58
|
catch (e) {
|
|
59
59
|
// DB 可能已关闭(测试清理、进程关闭等)
|
|
60
|
-
options.log.info(`Size monitor check skipped: ${e instanceof Error ? e.message :
|
|
60
|
+
options.log.info(`Size monitor check skipped: ${e instanceof Error ? e.message : e instanceof Error ? e.message : JSON.stringify(e)}`);
|
|
61
61
|
}
|
|
62
62
|
finally {
|
|
63
63
|
running = false;
|
package/dist/db/log-cleaner.js
CHANGED
|
@@ -29,7 +29,7 @@ export function scheduleLogCleanup(db, log) {
|
|
|
29
29
|
}
|
|
30
30
|
catch (e) {
|
|
31
31
|
// DB 可能已关闭(测试清理、进程关闭等)
|
|
32
|
-
log.info(`Log cleanup skipped: ${e instanceof Error ? e.message :
|
|
32
|
+
log.info(`Log cleanup skipped: ${e instanceof Error ? e.message : e instanceof Error ? e.message : JSON.stringify(e)}`);
|
|
33
33
|
}
|
|
34
34
|
finally {
|
|
35
35
|
cleaning = false;
|
package/dist/db/settings.js
CHANGED
|
@@ -14,7 +14,7 @@ export function getLogRetentionDays(db) {
|
|
|
14
14
|
return val ? parseInt(val, 10) : DEFAULT_LOG_RETENTION_DAYS;
|
|
15
15
|
}
|
|
16
16
|
export function setLogRetentionDays(db, days) {
|
|
17
|
-
setSetting(db, "log_retention_days",
|
|
17
|
+
setSetting(db, "log_retention_days", days.toString());
|
|
18
18
|
}
|
|
19
19
|
const DEFAULT_DB_MAX_SIZE_MB = 1024;
|
|
20
20
|
const DEFAULT_LOG_TABLE_MAX_SIZE_MB = 800;
|
|
@@ -23,14 +23,14 @@ export function getDbMaxSizeMb(db) {
|
|
|
23
23
|
return val ? parseInt(val, 10) : DEFAULT_DB_MAX_SIZE_MB;
|
|
24
24
|
}
|
|
25
25
|
export function setDbMaxSizeMb(db, mb) {
|
|
26
|
-
setSetting(db, "db_max_size_mb",
|
|
26
|
+
setSetting(db, "db_max_size_mb", mb.toString());
|
|
27
27
|
}
|
|
28
28
|
export function getLogTableMaxSizeMb(db) {
|
|
29
29
|
const val = getSetting(db, "log_table_max_size_mb");
|
|
30
30
|
return val ? parseInt(val, 10) : DEFAULT_LOG_TABLE_MAX_SIZE_MB;
|
|
31
31
|
}
|
|
32
32
|
export function setLogTableMaxSizeMb(db, mb) {
|
|
33
|
-
setSetting(db, "log_table_max_size_mb",
|
|
33
|
+
setSetting(db, "log_table_max_size_mb", mb.toString());
|
|
34
34
|
}
|
|
35
35
|
export function getConfigSyncSource(db) {
|
|
36
36
|
const val = getSetting(db, "config_sync_source");
|
package/dist/index.js
CHANGED
|
@@ -15,12 +15,20 @@ import { getConfig, getBaseConfig } from "./config/index.js";
|
|
|
15
15
|
import { initDatabase, getAllProviders } from "./db/index.js";
|
|
16
16
|
import { loadRecommendedConfig } from "./config/recommended.js";
|
|
17
17
|
import { authMiddleware } from "./middleware/auth.js";
|
|
18
|
-
import {
|
|
19
|
-
import { anthropicProxy } from "./proxy/handler/anthropic.js";
|
|
20
|
-
import { responsesProxy } from "./proxy/handler/responses.js";
|
|
18
|
+
import { createProxyHandler } from "./proxy/handler/create-proxy-handler.js";
|
|
21
19
|
import { adminRoutes } from "./admin/routes.js";
|
|
22
20
|
import { RetryRuleMatcher } from "./proxy/orchestration/retry-rules.js";
|
|
23
21
|
import { PluginRegistry } from "./proxy/transform/plugin-registry.js";
|
|
22
|
+
import { FormatRegistry } from "./proxy/format/registry.js";
|
|
23
|
+
import { openaiAdapter } from "./proxy/format/adapters/openai.js";
|
|
24
|
+
import { anthropicAdapter } from "./proxy/format/adapters/anthropic.js";
|
|
25
|
+
import { responsesAdapter } from "./proxy/format/adapters/responses.js";
|
|
26
|
+
import { openaiToAnthropicConverter } from "./proxy/format/converters/openai-anthropic.js";
|
|
27
|
+
import { anthropicToOpenAIConverter } from "./proxy/format/converters/anthropic-openai.js";
|
|
28
|
+
import { openaiToResponsesConverter } from "./proxy/format/converters/openai-responses.js";
|
|
29
|
+
import { responsesToOpenAIConverter } from "./proxy/format/converters/responses-openai.js";
|
|
30
|
+
import { responsesToAnthropicConverter } from "./proxy/format/converters/responses-anthropic.js";
|
|
31
|
+
import { anthropicToResponsesConverter } from "./proxy/format/converters/anthropic-responses.js";
|
|
24
32
|
import { SemaphoreManager, AdaptiveController } from "@llm-router/core/concurrency";
|
|
25
33
|
import { RequestTracker } from "@llm-router/core/monitor";
|
|
26
34
|
import { UsageWindowTracker } from "./proxy/routing/usage-window-tracker.js";
|
|
@@ -32,6 +40,7 @@ import fastifyStatic from "@fastify/static";
|
|
|
32
40
|
import { ServiceContainer, SERVICE_KEYS } from "./core/container.js";
|
|
33
41
|
import { LogFileWriter } from "./storage/log-file-writer.js";
|
|
34
42
|
import { ProxyAgentFactory } from "./proxy/transport/proxy-agent.js";
|
|
43
|
+
import { registerBuiltinHooks } from "./proxy/pipeline/register-hooks.js";
|
|
35
44
|
import { scheduleLogFileMaintenance } from "./storage/log-file-compressor.js";
|
|
36
45
|
import { getDetailLogEnabled, getLogFileRetentionDays } from "./db/settings.js";
|
|
37
46
|
import { dirname, join } from "node:path";
|
|
@@ -216,6 +225,18 @@ export async function buildApp(options) {
|
|
|
216
225
|
const pluginsDir = path.resolve(__dirname, "../plugins/transform");
|
|
217
226
|
pluginRegistry.scanPluginsDir(pluginsDir);
|
|
218
227
|
container.register(SERVICE_KEYS.pluginRegistry, () => pluginRegistry);
|
|
228
|
+
// 注册 FormatRegistry(3 adapters + 6 converters 覆盖所有格式转换)
|
|
229
|
+
const formatRegistry = new FormatRegistry();
|
|
230
|
+
formatRegistry.registerAdapter(openaiAdapter);
|
|
231
|
+
formatRegistry.registerAdapter(anthropicAdapter);
|
|
232
|
+
formatRegistry.registerAdapter(responsesAdapter);
|
|
233
|
+
formatRegistry.registerConverter(openaiToAnthropicConverter);
|
|
234
|
+
formatRegistry.registerConverter(anthropicToOpenAIConverter);
|
|
235
|
+
formatRegistry.registerConverter(openaiToResponsesConverter);
|
|
236
|
+
formatRegistry.registerConverter(responsesToOpenAIConverter);
|
|
237
|
+
formatRegistry.registerConverter(responsesToAnthropicConverter);
|
|
238
|
+
formatRegistry.registerConverter(anthropicToResponsesConverter);
|
|
239
|
+
container.register(SERVICE_KEYS.formatRegistry, () => formatRegistry);
|
|
219
240
|
// 注册 ProxyAgentFactory
|
|
220
241
|
container.register(SERVICE_KEYS.proxyAgentFactory, () => new ProxyAgentFactory());
|
|
221
242
|
// 从容器解析所有服务
|
|
@@ -230,9 +251,24 @@ export async function buildApp(options) {
|
|
|
230
251
|
// 从 DB 读取已有 provider 的并发配置,初始化信号量/adaptive/tracker(共享逻辑)
|
|
231
252
|
initializeProviderState(db, semaphoreManager, adaptiveController, tracker);
|
|
232
253
|
app.register(authMiddleware, { db });
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
254
|
+
// 注册内置 hooks 到 hookRegistry(供 Admin API 查询)
|
|
255
|
+
registerBuiltinHooks();
|
|
256
|
+
// --- New pipeline-based proxy handlers (Phase 3) ---
|
|
257
|
+
const openaiHandler = createProxyHandler({
|
|
258
|
+
apiType: "openai",
|
|
259
|
+
paths: ["/v1/chat/completions", "/chat/completions"],
|
|
260
|
+
});
|
|
261
|
+
const anthropicHandler = createProxyHandler({
|
|
262
|
+
apiType: "anthropic",
|
|
263
|
+
paths: ["/v1/messages"],
|
|
264
|
+
});
|
|
265
|
+
const responsesHandler = createProxyHandler({
|
|
266
|
+
apiType: "openai-responses",
|
|
267
|
+
paths: ["/v1/responses", "/responses"],
|
|
268
|
+
});
|
|
269
|
+
app.register(openaiHandler, { db, container });
|
|
270
|
+
app.register(anthropicHandler, { db, container });
|
|
271
|
+
app.register(responsesHandler, { db, container });
|
|
236
272
|
// StateRegistry — Admin 层通过此接口触发 proxy 层状态刷新,消除 admin→proxy 依赖
|
|
237
273
|
const stateRegistry = {
|
|
238
274
|
refreshRetryRules: () => matcher.load(db),
|
|
@@ -262,7 +298,7 @@ export async function buildApp(options) {
|
|
|
262
298
|
});
|
|
263
299
|
// SPA fallback: /admin/ 下非 API 路径返回 index.html
|
|
264
300
|
app.setNotFoundHandler((request, reply) => {
|
|
265
|
-
if (request.url.startsWith("/admin") &&
|
|
301
|
+
if ((request.url.startsWith("/admin/") || request.url === "/admin") &&
|
|
266
302
|
!request.url.startsWith("/admin/api")) {
|
|
267
303
|
return reply.sendFile("index.html");
|
|
268
304
|
}
|
|
@@ -347,7 +383,7 @@ export async function main() {
|
|
|
347
383
|
});
|
|
348
384
|
process.on("unhandledRejection", (reason) => {
|
|
349
385
|
try {
|
|
350
|
-
app.log.error({ err: reason instanceof Error ? reason : new Error(
|
|
386
|
+
app.log.error({ err: reason instanceof Error ? reason : new Error(typeof reason === 'string' ? reason : JSON.stringify(reason)) }, "Unhandled rejection");
|
|
351
387
|
/* eslint-disable taste/no-silent-catch -- app.log 可能已崩溃,console 是最后手段 */
|
|
352
388
|
}
|
|
353
389
|
catch {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const ANTHROPIC_ERROR_META = {
|
|
2
|
+
modelNotFound: { type: "not_found_error", code: "model_not_found" },
|
|
3
|
+
modelNotAllowed: { type: "forbidden_error", code: "model_not_allowed" },
|
|
4
|
+
providerUnavailable: { type: "api_error", code: "provider_unavailable" },
|
|
5
|
+
providerTypeMismatch: { type: "api_error", code: "provider_type_mismatch" },
|
|
6
|
+
upstreamConnectionFailed: { type: "upstream_error", code: "upstream_connection_failed" },
|
|
7
|
+
concurrencyQueueFull: { type: "api_error", code: "concurrency_queue_full" },
|
|
8
|
+
concurrencyTimeout: { type: "api_error", code: "concurrency_timeout" },
|
|
9
|
+
promptTooLong: { type: "invalid_request_error", code: "context_window_exceeded" },
|
|
10
|
+
};
|
|
11
|
+
export const anthropicAdapter = {
|
|
12
|
+
apiType: "anthropic",
|
|
13
|
+
defaultPath: "/v1/messages",
|
|
14
|
+
errorMeta: ANTHROPIC_ERROR_META,
|
|
15
|
+
formatError(message) {
|
|
16
|
+
return { type: "error", error: { type: "api_error", message } };
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OPENAI_FAMILY_ERROR_META } from "./shared-error-meta.js";
|
|
2
|
+
export const openaiAdapter = {
|
|
3
|
+
apiType: "openai",
|
|
4
|
+
defaultPath: "/v1/chat/completions",
|
|
5
|
+
errorMeta: OPENAI_FAMILY_ERROR_META,
|
|
6
|
+
beforeSendProxy(body, isStream) {
|
|
7
|
+
if (isStream && !body.stream_options) {
|
|
8
|
+
body.stream_options = { include_usage: true };
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
formatError(message, code) {
|
|
12
|
+
return { error: { message, type: "upstream_error", code: code ?? "upstream_error" } };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { OPENAI_FAMILY_ERROR_META } from "./shared-error-meta.js";
|
|
2
|
+
export const responsesAdapter = {
|
|
3
|
+
apiType: "openai-responses",
|
|
4
|
+
defaultPath: "/v1/responses",
|
|
5
|
+
errorMeta: OPENAI_FAMILY_ERROR_META,
|
|
6
|
+
formatError(message, code) {
|
|
7
|
+
return { error: { message, type: "invalid_request_error", code: code ?? "upstream_error" } };
|
|
8
|
+
},
|
|
9
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ErrorKind } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI 和 Responses API 共用的错误元数据。
|
|
4
|
+
* 两者 error code/type 完全相同,仅 apiType/defaultPath/formatError 不同。
|
|
5
|
+
*/
|
|
6
|
+
export declare const OPENAI_FAMILY_ERROR_META: Record<ErrorKind, {
|
|
7
|
+
type: string;
|
|
8
|
+
code: string;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI 和 Responses API 共用的错误元数据。
|
|
3
|
+
* 两者 error code/type 完全相同,仅 apiType/defaultPath/formatError 不同。
|
|
4
|
+
*/
|
|
5
|
+
export const OPENAI_FAMILY_ERROR_META = {
|
|
6
|
+
modelNotFound: { type: "invalid_request_error", code: "model_not_found" },
|
|
7
|
+
modelNotAllowed: { type: "invalid_request_error", code: "model_not_allowed" },
|
|
8
|
+
providerUnavailable: { type: "server_error", code: "provider_unavailable" },
|
|
9
|
+
providerTypeMismatch: { type: "server_error", code: "provider_type_mismatch" },
|
|
10
|
+
upstreamConnectionFailed: { type: "upstream_error", code: "upstream_connection_failed" },
|
|
11
|
+
concurrencyQueueFull: { type: "server_error", code: "concurrency_queue_full" },
|
|
12
|
+
concurrencyTimeout: { type: "server_error", code: "concurrency_timeout" },
|
|
13
|
+
promptTooLong: { type: "invalid_request_error", code: "context_window_exceeded" },
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const anthropicToOpenAIConverter: import("../types.js").FormatConverter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createConverter } from "../types.js";
|
|
2
|
+
import { anthropicToOpenAIRequest } from "../../transform/request-transform.js";
|
|
3
|
+
import { anthropicResponseToOpenAI } from "../../transform/response-transform.js";
|
|
4
|
+
import { AnthropicToOpenAITransform } from "../../transform/stream-ant2oa.js";
|
|
5
|
+
export const anthropicToOpenAIConverter = createConverter({
|
|
6
|
+
sourceType: "anthropic",
|
|
7
|
+
targetType: "openai",
|
|
8
|
+
requestTransform: anthropicToOpenAIRequest,
|
|
9
|
+
responseTransform: anthropicResponseToOpenAI,
|
|
10
|
+
streamTransformClass: AnthropicToOpenAITransform,
|
|
11
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const anthropicToResponsesConverter: import("../types.js").FormatConverter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createConverter } from "../types.js";
|
|
2
|
+
import { anthropicToResponsesRequest } from "../../transform/request-transform-responses.js";
|
|
3
|
+
import { anthropicToResponsesResponse } from "../../transform/response-transform-responses.js";
|
|
4
|
+
import { AnthropicToResponsesTransform } from "../../transform/stream-ant2resp.js";
|
|
5
|
+
export const anthropicToResponsesConverter = createConverter({
|
|
6
|
+
sourceType: "anthropic",
|
|
7
|
+
targetType: "openai-responses",
|
|
8
|
+
requestTransform: anthropicToResponsesRequest,
|
|
9
|
+
responseTransform: anthropicToResponsesResponse,
|
|
10
|
+
streamTransformClass: AnthropicToResponsesTransform,
|
|
11
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const openaiToAnthropicConverter: import("../types.js").FormatConverter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createConverter } from "../types.js";
|
|
2
|
+
import { openaiToAnthropicRequest } from "../../transform/request-transform.js";
|
|
3
|
+
import { openaiResponseToAnthropic } from "../../transform/response-transform.js";
|
|
4
|
+
import { OpenAIToAnthropicTransform } from "../../transform/stream-oa2ant.js";
|
|
5
|
+
export const openaiToAnthropicConverter = createConverter({
|
|
6
|
+
sourceType: "openai",
|
|
7
|
+
targetType: "anthropic",
|
|
8
|
+
requestTransform: openaiToAnthropicRequest,
|
|
9
|
+
responseTransform: openaiResponseToAnthropic,
|
|
10
|
+
streamTransformClass: OpenAIToAnthropicTransform,
|
|
11
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const openaiToResponsesConverter: import("../types.js").FormatConverter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createConverter } from "../types.js";
|
|
2
|
+
import { chatToResponsesRequest } from "../../transform/request-bridge-responses.js";
|
|
3
|
+
import { chatToResponsesResponse } from "../../transform/response-bridge-responses.js";
|
|
4
|
+
import { ChatToResponsesBridgeTransform } from "../../transform/stream-bridge-chat2resp.js";
|
|
5
|
+
export const openaiToResponsesConverter = createConverter({
|
|
6
|
+
sourceType: "openai",
|
|
7
|
+
targetType: "openai-responses",
|
|
8
|
+
requestTransform: chatToResponsesRequest,
|
|
9
|
+
responseTransform: chatToResponsesResponse,
|
|
10
|
+
streamTransformClass: ChatToResponsesBridgeTransform,
|
|
11
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const responsesToAnthropicConverter: import("../types.js").FormatConverter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createConverter } from "../types.js";
|
|
2
|
+
import { responsesToAnthropicRequest } from "../../transform/request-transform-responses.js";
|
|
3
|
+
import { responsesToAnthropicResponse } from "../../transform/response-transform-responses.js";
|
|
4
|
+
import { ResponsesToAnthropicTransform } from "../../transform/stream-resp2ant.js";
|
|
5
|
+
export const responsesToAnthropicConverter = createConverter({
|
|
6
|
+
sourceType: "openai-responses",
|
|
7
|
+
targetType: "anthropic",
|
|
8
|
+
requestTransform: responsesToAnthropicRequest,
|
|
9
|
+
responseTransform: responsesToAnthropicResponse,
|
|
10
|
+
streamTransformClass: ResponsesToAnthropicTransform,
|
|
11
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const responsesToOpenAIConverter: import("../types.js").FormatConverter;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createConverter } from "../types.js";
|
|
2
|
+
import { responsesToChatRequest } from "../../transform/request-bridge-responses.js";
|
|
3
|
+
import { responsesToChatResponse } from "../../transform/response-bridge-responses.js";
|
|
4
|
+
import { ResponsesToChatBridgeTransform } from "../../transform/stream-bridge-resp2chat.js";
|
|
5
|
+
export const responsesToOpenAIConverter = createConverter({
|
|
6
|
+
sourceType: "openai-responses",
|
|
7
|
+
targetType: "openai",
|
|
8
|
+
requestTransform: responsesToChatRequest,
|
|
9
|
+
responseTransform: responsesToChatResponse,
|
|
10
|
+
streamTransformClass: ResponsesToChatBridgeTransform,
|
|
11
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Transform } from "stream";
|
|
2
|
+
import type { FormatAdapter, FormatConverter } from "./types.js";
|
|
3
|
+
export declare class FormatRegistry {
|
|
4
|
+
private adapters;
|
|
5
|
+
private converters;
|
|
6
|
+
registerAdapter(adapter: FormatAdapter): void;
|
|
7
|
+
registerConverter(converter: FormatConverter): void;
|
|
8
|
+
getAdapter(apiType: string): FormatAdapter | undefined;
|
|
9
|
+
needsTransform(source: string, target: string): boolean;
|
|
10
|
+
transformRequest(body: Record<string, unknown>, source: string, target: string, model: string): {
|
|
11
|
+
body: Record<string, unknown>;
|
|
12
|
+
upstreamPath: string;
|
|
13
|
+
};
|
|
14
|
+
transformResponse(bodyStr: string, source: string, target: string): string;
|
|
15
|
+
transformError(bodyStr: string, source: string, target: string): string;
|
|
16
|
+
createStreamTransform(source: string, target: string, model: string): Transform | undefined;
|
|
17
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export class FormatRegistry {
|
|
2
|
+
adapters = new Map();
|
|
3
|
+
converters = new Map();
|
|
4
|
+
registerAdapter(adapter) {
|
|
5
|
+
this.adapters.set(adapter.apiType, adapter);
|
|
6
|
+
}
|
|
7
|
+
registerConverter(converter) {
|
|
8
|
+
this.converters.set(`${converter.sourceType}→${converter.targetType}`, converter);
|
|
9
|
+
}
|
|
10
|
+
getAdapter(apiType) {
|
|
11
|
+
return this.adapters.get(apiType);
|
|
12
|
+
}
|
|
13
|
+
needsTransform(source, target) {
|
|
14
|
+
return source !== target;
|
|
15
|
+
}
|
|
16
|
+
transformRequest(body, source, target, model) {
|
|
17
|
+
const targetAdapter = this.adapters.get(target);
|
|
18
|
+
const upstreamPath = targetAdapter?.defaultPath ?? "/v1/chat/completions";
|
|
19
|
+
const converter = this.converters.get(`${source}→${target}`);
|
|
20
|
+
if (!converter)
|
|
21
|
+
return { body, upstreamPath };
|
|
22
|
+
return { body: converter.transformRequest(body, model), upstreamPath };
|
|
23
|
+
}
|
|
24
|
+
transformResponse(bodyStr, source, target) {
|
|
25
|
+
const converter = this.converters.get(`${source}→${target}`);
|
|
26
|
+
if (!converter)
|
|
27
|
+
return bodyStr;
|
|
28
|
+
return converter.transformResponse(bodyStr);
|
|
29
|
+
}
|
|
30
|
+
transformError(bodyStr, source, target) {
|
|
31
|
+
if (source === target)
|
|
32
|
+
return bodyStr;
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(bodyStr);
|
|
35
|
+
const message = parsed.error?.message ?? parsed.message ?? JSON.stringify(parsed);
|
|
36
|
+
const code = parsed.error?.code ?? parsed.code;
|
|
37
|
+
const targetAdapter = this.adapters.get(target);
|
|
38
|
+
if (!targetAdapter)
|
|
39
|
+
return bodyStr;
|
|
40
|
+
return JSON.stringify(targetAdapter.formatError(message, code));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return bodyStr;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
createStreamTransform(source, target, model) {
|
|
47
|
+
const converter = this.converters.get(`${source}→${target}`);
|
|
48
|
+
return converter?.createStreamTransform(model);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Transform } from "stream";
|
|
2
|
+
export type ErrorKind = "modelNotFound" | "modelNotAllowed" | "providerUnavailable" | "providerTypeMismatch" | "upstreamConnectionFailed" | "concurrencyQueueFull" | "concurrencyTimeout" | "promptTooLong";
|
|
3
|
+
export interface FormatAdapter {
|
|
4
|
+
readonly apiType: string;
|
|
5
|
+
readonly defaultPath: string;
|
|
6
|
+
readonly errorMeta: Record<ErrorKind, {
|
|
7
|
+
type: string;
|
|
8
|
+
code: string;
|
|
9
|
+
}>;
|
|
10
|
+
beforeSendProxy?(body: Record<string, unknown>, isStream: boolean): void;
|
|
11
|
+
formatError(message: string, code?: string): unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface FormatConverter {
|
|
14
|
+
readonly sourceType: string;
|
|
15
|
+
readonly targetType: string;
|
|
16
|
+
transformRequest(body: Record<string, unknown>, model: string): Record<string, unknown>;
|
|
17
|
+
transformResponse(bodyStr: string): string;
|
|
18
|
+
createStreamTransform(model: string): Transform;
|
|
19
|
+
}
|
|
20
|
+
/** Factory: eliminates repetitive object literal structure across 6 converters. */
|
|
21
|
+
export declare function createConverter(deps: {
|
|
22
|
+
sourceType: string;
|
|
23
|
+
targetType: string;
|
|
24
|
+
requestTransform: (body: Record<string, unknown>) => Record<string, unknown>;
|
|
25
|
+
responseTransform: (bodyStr: string) => string;
|
|
26
|
+
streamTransformClass: new (model: string) => Transform;
|
|
27
|
+
}): FormatConverter;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Factory: eliminates repetitive object literal structure across 6 converters. */
|
|
2
|
+
export function createConverter(deps) {
|
|
3
|
+
return {
|
|
4
|
+
sourceType: deps.sourceType,
|
|
5
|
+
targetType: deps.targetType,
|
|
6
|
+
transformRequest(body) {
|
|
7
|
+
return deps.requestTransform(body);
|
|
8
|
+
},
|
|
9
|
+
transformResponse(bodyStr) {
|
|
10
|
+
return deps.responseTransform(bodyStr);
|
|
11
|
+
},
|
|
12
|
+
createStreamTransform(model) {
|
|
13
|
+
return new deps.streamTransformClass(model);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FastifyPluginCallback } from "fastify";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
import type { ServiceContainer } from "../../core/container.js";
|
|
4
|
+
export interface ProxyHandlerConfig {
|
|
5
|
+
/** API 类型:openai | openai-responses | anthropic */
|
|
6
|
+
apiType: "openai" | "openai-responses" | "anthropic";
|
|
7
|
+
/** 注册 POST 路由的路径列表 */
|
|
8
|
+
paths: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ProxyHandlerOptions {
|
|
11
|
+
db: Database.Database;
|
|
12
|
+
container: ServiceContainer;
|
|
13
|
+
}
|
|
14
|
+
export declare function createProxyHandler(config: ProxyHandlerConfig): FastifyPluginCallback<ProxyHandlerOptions>;
|