llm-simple-router 0.6.7 → 0.7.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/README.md +69 -0
- package/dist/admin/constants.d.ts +1 -1
- package/dist/admin/constants.js +2 -2
- package/dist/admin/logs.d.ts +2 -0
- package/dist/admin/logs.js +17 -1
- package/dist/admin/providers.d.ts +2 -2
- package/dist/admin/providers.js +29 -16
- package/dist/admin/proxy-enhancement.d.ts +2 -0
- package/dist/admin/proxy-enhancement.js +4 -10
- package/dist/admin/retry-rules.d.ts +2 -2
- package/dist/admin/retry-rules.js +4 -8
- package/dist/admin/routes.d.ts +5 -4
- package/dist/admin/routes.js +7 -7
- package/dist/admin/settings-import-export.d.ts +2 -4
- package/dist/admin/settings-import-export.js +9 -19
- package/dist/admin/settings.d.ts +1 -0
- package/dist/admin/settings.js +29 -1
- package/dist/admin/upgrade.d.ts +1 -0
- package/dist/admin/upgrade.js +37 -4
- package/dist/{constants.d.ts → core/constants.d.ts} +4 -1
- package/dist/{constants.js → core/constants.js} +21 -1
- package/dist/core/container.d.ts +31 -0
- package/dist/core/container.js +41 -0
- package/dist/core/errors.d.ts +26 -0
- package/dist/core/errors.js +42 -0
- package/dist/core/registry.d.ts +43 -0
- package/dist/core/registry.js +3 -0
- package/dist/core/types.d.ts +105 -0
- package/dist/core/types.js +3 -0
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/logs.d.ts +11 -24
- package/dist/db/logs.js +37 -38
- package/dist/db/metrics.js +1 -1
- package/dist/db/migrations/033_add_pipeline_snapshot.sql +1 -0
- package/dist/db/migrations/034_drop_redundant_log_columns.sql +13 -0
- package/dist/db/settings.d.ts +2 -0
- package/dist/db/settings.js +9 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +196 -108
- package/dist/metrics/metrics-extractor.d.ts +1 -24
- package/dist/metrics/metrics-extractor.js +1 -1
- package/dist/metrics/sse-metrics-transform.d.ts +1 -1
- package/dist/middleware/admin-auth.js +4 -0
- package/dist/middleware/auth.js +1 -2
- package/dist/monitor/request-tracker.d.ts +3 -4
- package/dist/monitor/request-tracker.js +6 -16
- package/dist/monitor/runtime-collector.js +1 -1
- package/dist/monitor/types.d.ts +8 -0
- package/dist/proxy/adaptive-controller.d.ts +4 -1
- package/dist/proxy/adaptive-controller.js +5 -0
- package/dist/proxy/enhancement/enhancement-handler.d.ts +19 -3
- package/dist/proxy/enhancement/enhancement-handler.js +80 -28
- package/dist/proxy/enhancement/index.d.ts +1 -0
- package/dist/proxy/handler/anthropic.d.ts +7 -0
- package/dist/proxy/{anthropic.js → handler/anthropic.js} +8 -7
- package/dist/proxy/handler/openai.d.ts +7 -0
- package/dist/proxy/{openai.js → handler/openai.js} +10 -9
- package/dist/proxy/handler/proxy-handler-utils.d.ts +9 -0
- package/dist/proxy/handler/proxy-handler-utils.js +63 -0
- package/dist/proxy/handler/proxy-handler.d.ts +13 -0
- package/dist/proxy/{proxy-handler.js → handler/proxy-handler.js} +104 -120
- package/dist/proxy/log-detail-policy.d.ts +12 -0
- package/dist/proxy/log-detail-policy.js +21 -0
- package/dist/proxy/log-helpers.d.ts +8 -0
- package/dist/proxy/log-helpers.js +16 -4
- package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +1 -1
- package/dist/proxy/loop-prevention/tool-loop-guard.js +9 -12
- package/dist/proxy/{orchestrator.d.ts → orchestration/orchestrator.d.ts} +6 -4
- package/dist/proxy/{orchestrator.js → orchestration/orchestrator.js} +2 -1
- package/dist/proxy/{resilience.d.ts → orchestration/resilience.d.ts} +2 -14
- package/dist/proxy/{resilience.js → orchestration/resilience.js} +2 -2
- package/dist/proxy/{retry-rules.d.ts → orchestration/retry-rules.d.ts} +1 -1
- package/dist/proxy/{retry-rules.js → orchestration/retry-rules.js} +1 -1
- package/dist/proxy/{scope.d.ts → orchestration/scope.d.ts} +3 -3
- package/dist/proxy/{semaphore.d.ts → orchestration/semaphore.d.ts} +7 -15
- package/dist/proxy/{semaphore.js → orchestration/semaphore.js} +12 -26
- package/dist/proxy/patch/index.d.ts +8 -2
- package/dist/proxy/patch/index.js +5 -2
- package/dist/proxy/pipeline-snapshot.d.ts +37 -0
- package/dist/proxy/pipeline-snapshot.js +15 -0
- package/dist/proxy/proxy-core.d.ts +1 -1
- package/dist/proxy/proxy-core.js +1 -1
- package/dist/proxy/proxy-logging.d.ts +10 -2
- package/dist/proxy/proxy-logging.js +23 -9
- package/dist/proxy/response-transform.d.ts +7 -0
- package/dist/proxy/response-transform.js +15 -0
- package/dist/proxy/{enhancement-config.js → routing/enhancement-config.js} +1 -1
- package/dist/proxy/{mapping-resolver.d.ts → routing/mapping-resolver.d.ts} +1 -1
- package/dist/proxy/{mapping-resolver.js → routing/mapping-resolver.js} +1 -1
- package/dist/proxy/{model-state.js → routing/model-state.js} +1 -1
- package/dist/proxy/{overflow.d.ts → routing/overflow.d.ts} +1 -1
- package/dist/proxy/{overflow.js → routing/overflow.js} +3 -3
- package/dist/proxy/{usage-window-tracker.js → routing/usage-window-tracker.js} +3 -3
- package/dist/proxy/{transport.d.ts → transport/http.d.ts} +2 -2
- package/dist/proxy/{transport.js → transport/http.js} +3 -3
- package/dist/proxy/{stream-proxy.d.ts → transport/stream.d.ts} +4 -4
- package/dist/proxy/{stream-proxy.js → transport/stream.js} +25 -7
- package/dist/proxy/{transport-fn.d.ts → transport/transport-fn.d.ts} +5 -5
- package/dist/proxy/{transport-fn.js → transport/transport-fn.js} +11 -9
- package/dist/proxy/types.d.ts +3 -64
- package/dist/proxy/types.js +5 -34
- package/dist/storage/log-file-compressor.d.ts +15 -0
- package/dist/storage/log-file-compressor.js +83 -0
- package/dist/storage/log-file-writer.d.ts +21 -0
- package/dist/storage/log-file-writer.js +103 -0
- package/dist/storage/types.d.ts +16 -0
- package/dist/storage/types.js +5 -0
- package/dist/upgrade/deployment.d.ts +13 -0
- package/dist/upgrade/deployment.js +40 -0
- package/frontend-dist/assets/{CardContent-jQcfCC7J.js → CardContent-CxOF1feY.js} +1 -1
- package/frontend-dist/assets/{CardTitle-BrCTvULL.js → CardTitle-BSEFcEOM.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-BFh67j5d.js → CascadingModelSelect-DTwksDPZ.js} +1 -1
- package/frontend-dist/assets/{Checkbox-Bbt7JpdE.js → Checkbox-RfsERG07.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-DMnEA0qC.js → CollapsibleTrigger-Dsjo7QlC.js} +1 -1
- package/frontend-dist/assets/{Collection-CVk3TPHc.js → Collection-rQ4eIYfa.js} +1 -1
- package/frontend-dist/assets/{Dashboard-Coftbg4B.js → Dashboard-YejfAPiB.js} +1 -1
- package/frontend-dist/assets/{DialogTitle-BbOAZzPQ.js → DialogTitle-DeFTnmgC.js} +1 -1
- package/frontend-dist/assets/{Input-DdHY9q0w.js → Input-CENz_g9t.js} +1 -1
- package/frontend-dist/assets/{Label-DRQv_Dr_.js → Label-BAciBrrd.js} +1 -1
- package/frontend-dist/assets/{Login-SV3ctFnJ.js → Login-DQkYFq7R.js} +1 -1
- package/frontend-dist/assets/{Logs-BG45kX6E.js → Logs-Dol8AX7z.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-DEaBnRU3.js → ModelMappings-VEYW1TrW.js} +1 -1
- package/frontend-dist/assets/{Monitor-ZHOt11n-.js → Monitor-C0r9WefB.js} +1 -1
- package/frontend-dist/assets/{PopoverTrigger-z-Z3EjBk.js → PopoverTrigger-Cyqik5SE.js} +1 -1
- package/frontend-dist/assets/{PopperContent-DPC-6a3n.js → PopperContent-B7IuAHeq.js} +1 -1
- package/frontend-dist/assets/{Providers-DpY6pAcg.js → Providers-D8Z97edN.js} +1 -1
- package/frontend-dist/assets/{ProxyEnhancement-D6KBDXMp.js → ProxyEnhancement-Kn8r2SN6.js} +1 -1
- package/frontend-dist/assets/{RetryRules-DWI7_WLZ.js → RetryRules-F0295m4_.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-CZ1657eX.js → RouterKeys-CFbPtUE_.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-BREE2YEV.js → RovingFocusItem-D291Vjh8.js} +1 -1
- package/frontend-dist/assets/{Schedules-BVPsBRPi.js → Schedules-DWhF3uod.js} +1 -1
- package/frontend-dist/assets/{SelectValue-H8hwQwbk.js → SelectValue-BWlgUZa3.js} +1 -1
- package/frontend-dist/assets/Settings-BnIzEF_k.js +6 -0
- package/frontend-dist/assets/{Setup-yOYNKkOG.js → Setup-BglKyQKq.js} +1 -1
- package/frontend-dist/assets/{Switch-CojD3rTH.js → Switch-DyCR-CPu.js} +1 -1
- package/frontend-dist/assets/{TableHeader-awoHTsWN.js → TableHeader-DVUlBL35.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-DTKSFj85.js → TabsTrigger-BU1DY-C8.js} +1 -1
- package/frontend-dist/assets/{Teleport-DehYAXud.js → Teleport-BQgusr9g.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-C2dl_dml.js → TooltipTrigger-Bv_QoBns.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-C8A-uSTR.js → UnifiedRequestDialog-f_evI835.js} +2 -2
- package/frontend-dist/assets/{VisuallyHidden-C8oaGi2S.js → VisuallyHidden-Con10z4F.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-BMc813t2.js → VisuallyHiddenInput-yrDtxucb.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-C8TZQmU6.js → alert-dialog-2Db6Z7JQ.js} +1 -1
- package/frontend-dist/assets/arrow-down-WyouvE7T.js +1 -0
- package/frontend-dist/assets/{badge-BVh2WpA5.js → badge-DEhZfeI0.js} +1 -1
- package/frontend-dist/assets/button-Cnkbp_6J.js +12 -0
- package/frontend-dist/assets/check-BuqB5Nyb.js +1 -0
- package/frontend-dist/assets/{copy-DTOecxa9.js → copy-CwqZSuIG.js} +1 -1
- package/frontend-dist/assets/{dialog-kA7AUNoc.js → dialog-CVMKSdPr.js} +1 -1
- package/frontend-dist/assets/{file-text-DzZCFO7y.js → file-text-D0K8Hovo.js} +1 -1
- package/frontend-dist/assets/index-Ct718O93.js +1 -0
- package/frontend-dist/assets/{lib-ClDokUbt.js → lib-H3YI7EK4.js} +1 -1
- package/frontend-dist/assets/loader-circle-Be82FnVY.js +1 -0
- package/frontend-dist/assets/{useClipboard-DU1ne-Jw.js → useClipboard-Cd7k-5Yq.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-Btmdbg_F.js → useFocusGuards-luoLXnwV.js} +1 -1
- package/frontend-dist/assets/useFormControl-Da4ViGZF.js +1 -0
- package/frontend-dist/assets/{useLogRetention--EGNWXig.js → useLogRetention-DB4Iu6o_.js} +1 -1
- package/frontend-dist/assets/useNonce-DvAdQ48J.js +1 -0
- package/frontend-dist/assets/x-DB22csQl.js +1 -0
- package/frontend-dist/index.html +19 -19
- package/package.json +1 -1
- package/dist/proxy/anthropic.d.ts +0 -19
- package/dist/proxy/openai.d.ts +0 -19
- package/dist/proxy/proxy-handler.d.ts +0 -19
- package/dist/proxy/strategy/types.d.ts +0 -21
- package/dist/proxy/strategy/types.js +0 -1
- package/frontend-dist/assets/Settings-DHYaYRgU.js +0 -6
- package/frontend-dist/assets/arrow-down-D-cQXxau.js +0 -1
- package/frontend-dist/assets/button-N59D1BGa.js +0 -12
- package/frontend-dist/assets/check-dDgrw3T3.js +0 -1
- package/frontend-dist/assets/index-DVTeNVaa.js +0 -1
- package/frontend-dist/assets/loader-circle-DVHRL-38.js +0 -1
- package/frontend-dist/assets/useFormControl-C5Kjziuj.js +0 -1
- package/frontend-dist/assets/useNonce-Cp31yRzV.js +0 -1
- package/frontend-dist/assets/x-DMktsI_w.js +0 -1
- /package/dist/{config.d.ts → config/index.d.ts} +0 -0
- /package/dist/{config.js → config/index.js} +0 -0
- /package/dist/proxy/{scope.js → orchestration/scope.js} +0 -0
- /package/dist/proxy/{enhancement-config.d.ts → routing/enhancement-config.d.ts} +0 -0
- /package/dist/proxy/{model-state.d.ts → routing/model-state.d.ts} +0 -0
- /package/dist/proxy/{usage-window-tracker.d.ts → routing/usage-window-tracker.d.ts} +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
-
import { loadEnhancementConfig } from "../enhancement-config.js";
|
|
2
|
+
import { loadEnhancementConfig } from "../routing/enhancement-config.js";
|
|
3
3
|
import { getActiveProviderModels, resolveByProviderModel } from "../../db/index.js";
|
|
4
|
-
import { resolveMapping } from "../mapping-resolver.js";
|
|
4
|
+
import { resolveMapping } from "../routing/mapping-resolver.js";
|
|
5
5
|
import { parseDirective, parseToolResult, TOOL_USE_ID_PREFIX, TOOL_USE_ID_PROVIDER_PREFIX } from "./directive-parser.js";
|
|
6
|
-
import { modelState } from "../model-state.js";
|
|
6
|
+
import { modelState } from "../routing/model-state.js";
|
|
7
7
|
import { cleanRouterResponses } from "./response-cleaner.js";
|
|
8
8
|
const MODEL_INFO_TAG_TYPE = "model-info";
|
|
9
9
|
const SKIP_LABEL = "不选择";
|
|
@@ -56,66 +56,79 @@ function buildDisplayModels(db, allowedModelsRaw) {
|
|
|
56
56
|
}
|
|
57
57
|
return displayModels;
|
|
58
58
|
}
|
|
59
|
+
const EMPTY_META = { router_tags_stripped: 0, directive: null };
|
|
59
60
|
/**
|
|
60
61
|
* 在代理转发前应用代理增强逻辑(指令解析 + 会话记忆 + 模型替换 + 命令拦截)。
|
|
61
62
|
* 仅当 proxy_enhancement.claude_code_enabled 开启时生效。
|
|
63
|
+
*
|
|
64
|
+
* 纯函数:不修改输入 body,返回变换后的新 body + 元数据。
|
|
62
65
|
*/
|
|
63
|
-
export function applyEnhancement(db,
|
|
64
|
-
const
|
|
66
|
+
export function applyEnhancement(db, body, clientModel, sessionId, routerKey, enhancementConfig) {
|
|
67
|
+
const earlyReturn = {
|
|
68
|
+
body, effectiveModel: clientModel, originalModel: null, interceptResponse: null, meta: EMPTY_META,
|
|
69
|
+
};
|
|
65
70
|
const enhancement = enhancementConfig ?? loadEnhancementConfig(db);
|
|
66
71
|
if (!enhancement.claude_code_enabled) {
|
|
67
|
-
return
|
|
72
|
+
return earlyReturn;
|
|
68
73
|
}
|
|
69
74
|
// 检测 AskUserQuestion 的 tool_result 回调(用户在 UI 上选择了模型或 provider)
|
|
70
|
-
const toolResult = parseToolResult(
|
|
75
|
+
const toolResult = parseToolResult(body);
|
|
71
76
|
if (toolResult.isRouterToolResult) {
|
|
72
|
-
const routerKeyId =
|
|
77
|
+
const routerKeyId = routerKey?.id ?? null;
|
|
73
78
|
const nonSkipAnswers = toolResult.allAnswers.filter(a => a !== SKIP_LABEL);
|
|
74
79
|
// 所有回答都是"不选择" → 取消
|
|
75
80
|
if (nonSkipAnswers.length === 0) {
|
|
76
81
|
return {
|
|
82
|
+
body,
|
|
77
83
|
effectiveModel: clientModel,
|
|
78
84
|
originalModel: null,
|
|
79
85
|
interceptResponse: {
|
|
80
86
|
...buildTextResponse("model-select-cancelled", "已取消选择"),
|
|
81
87
|
meta: { action: "取消模型选择" },
|
|
82
88
|
},
|
|
89
|
+
meta: EMPTY_META,
|
|
83
90
|
};
|
|
84
91
|
}
|
|
85
92
|
// 选择了多个 → 提示错误
|
|
86
93
|
if (nonSkipAnswers.length > 1) {
|
|
87
94
|
return {
|
|
95
|
+
body,
|
|
88
96
|
effectiveModel: clientModel,
|
|
89
97
|
originalModel: null,
|
|
90
98
|
interceptResponse: {
|
|
91
99
|
...buildTextResponse("model-select-error", "选择错误:只能选择一个模型或提供商,请重新输入 /select-model 选择"),
|
|
92
100
|
meta: { action: "选择错误" },
|
|
93
101
|
},
|
|
102
|
+
meta: EMPTY_META,
|
|
94
103
|
};
|
|
95
104
|
}
|
|
96
105
|
const answer = nonSkipAnswers[0];
|
|
97
106
|
// 两步式:用户选择了 provider → 返回该 provider 的模型列表
|
|
98
107
|
if (toolResult.isProviderSelection) {
|
|
99
|
-
const allModels = buildDisplayModels(db,
|
|
108
|
+
const allModels = buildDisplayModels(db, routerKey?.allowed_models ?? null);
|
|
100
109
|
const providerModels = getModelsForProvider(allModels, answer);
|
|
101
110
|
if (providerModels.length === 0) {
|
|
102
111
|
return {
|
|
112
|
+
body,
|
|
103
113
|
effectiveModel: clientModel,
|
|
104
114
|
originalModel: null,
|
|
105
115
|
interceptResponse: {
|
|
106
116
|
...buildTextResponse("error", `未找到 provider: ${answer}`),
|
|
107
117
|
meta: { action: "模型选择失败", detail: answer },
|
|
108
118
|
},
|
|
119
|
+
meta: EMPTY_META,
|
|
109
120
|
};
|
|
110
121
|
}
|
|
111
122
|
const questions = buildModelQuestions(providerModels);
|
|
112
123
|
return {
|
|
124
|
+
body,
|
|
113
125
|
effectiveModel: clientModel,
|
|
114
126
|
originalModel: null,
|
|
115
127
|
interceptResponse: {
|
|
116
128
|
...buildAskUserQuestionPayload(questions, false, providerModels),
|
|
117
129
|
meta: { action: `模型列表(provider=${answer})` },
|
|
118
130
|
},
|
|
131
|
+
meta: EMPTY_META,
|
|
119
132
|
};
|
|
120
133
|
}
|
|
121
134
|
// 模型选择(直接或两步式第二步)
|
|
@@ -123,30 +136,37 @@ export function applyEnhancement(db, request, clientModel, sessionId, enhancemen
|
|
|
123
136
|
if (resolvedClientModel) {
|
|
124
137
|
modelState.set(routerKeyId, answer, sessionId, clientModel, "command");
|
|
125
138
|
return {
|
|
139
|
+
body,
|
|
126
140
|
effectiveModel: answer,
|
|
127
141
|
originalModel: null,
|
|
128
142
|
interceptResponse: {
|
|
129
143
|
...buildTextResponse("model-selected", `已选择模型: ${answer}`),
|
|
130
144
|
meta: { action: "模型选择", detail: answer },
|
|
131
145
|
},
|
|
146
|
+
meta: EMPTY_META,
|
|
132
147
|
};
|
|
133
148
|
}
|
|
134
149
|
return {
|
|
150
|
+
body,
|
|
135
151
|
effectiveModel: clientModel,
|
|
136
152
|
originalModel: null,
|
|
137
153
|
interceptResponse: {
|
|
138
154
|
...buildTextResponse("error", `未找到模型: ${answer}`),
|
|
139
155
|
meta: { action: "模型选择失败", detail: answer },
|
|
140
156
|
},
|
|
157
|
+
meta: EMPTY_META,
|
|
141
158
|
};
|
|
142
159
|
}
|
|
143
|
-
// 清理历史消息中的 <router-response>
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const
|
|
160
|
+
// 清理历史消息中的 <router-response> 标签(纯函数,返回新对象)
|
|
161
|
+
const originalMessages = body.messages?.length ?? 0;
|
|
162
|
+
const cleaned = cleanRouterResponses(body);
|
|
163
|
+
const cleanedMessages = cleaned.messages?.length ?? 0;
|
|
164
|
+
const tagsStripped = originalMessages - cleanedMessages;
|
|
165
|
+
const currentBody = { ...body, messages: cleaned.messages };
|
|
166
|
+
const directive = parseDirective(currentBody);
|
|
147
167
|
// 命令拦截:select-model → 返回可用模型列表
|
|
148
168
|
if (directive.isCommandMessage && directive.command?.startsWith("select-model")) {
|
|
149
|
-
const routerKeyId =
|
|
169
|
+
const routerKeyId = routerKey?.id ?? null;
|
|
150
170
|
const parts = directive.command.trim().split(/\s+/);
|
|
151
171
|
const arg = parts.length > 1 ? parts.slice(1).join(" ") : null;
|
|
152
172
|
// 带参数:设置模型并返回确认
|
|
@@ -154,35 +174,41 @@ export function applyEnhancement(db, request, clientModel, sessionId, enhancemen
|
|
|
154
174
|
const resolvedClientModel = resolveProviderModel(db, arg);
|
|
155
175
|
if (!resolvedClientModel) {
|
|
156
176
|
return {
|
|
177
|
+
body: currentBody,
|
|
157
178
|
effectiveModel: clientModel,
|
|
158
179
|
originalModel: null,
|
|
159
180
|
interceptResponse: {
|
|
160
181
|
...buildTextResponse("error", `未找到模型: ${arg}`),
|
|
161
182
|
meta: { action: "模型选择失败", detail: arg },
|
|
162
183
|
},
|
|
184
|
+
meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: `select-model ${arg}` } },
|
|
163
185
|
};
|
|
164
186
|
}
|
|
165
187
|
modelState.set(routerKeyId, arg, sessionId, clientModel, "command");
|
|
166
188
|
return {
|
|
189
|
+
body: currentBody,
|
|
167
190
|
effectiveModel: arg,
|
|
168
191
|
originalModel: null,
|
|
169
192
|
interceptResponse: {
|
|
170
|
-
...buildSelectModelResponse(db,
|
|
193
|
+
...buildSelectModelResponse(db, routerKey?.allowed_models ?? null, arg),
|
|
171
194
|
meta: { action: "模型选择", detail: arg },
|
|
172
195
|
},
|
|
196
|
+
meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: `select-model ${arg}` } },
|
|
173
197
|
};
|
|
174
198
|
}
|
|
175
199
|
// 无参数:返回模型列表
|
|
176
|
-
if (hasAskUserQuestion(
|
|
177
|
-
const displayModels = buildDisplayModels(db,
|
|
200
|
+
if (hasAskUserQuestion(currentBody)) {
|
|
201
|
+
const displayModels = buildDisplayModels(db, routerKey?.allowed_models ?? null);
|
|
178
202
|
if (displayModels.length === 0) {
|
|
179
203
|
return {
|
|
204
|
+
body: currentBody,
|
|
180
205
|
effectiveModel: clientModel,
|
|
181
206
|
originalModel: null,
|
|
182
207
|
interceptResponse: {
|
|
183
208
|
...buildTextResponse("model-list", "(无可用模型)"),
|
|
184
209
|
meta: { action: "模型列表" },
|
|
185
210
|
},
|
|
211
|
+
meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: "select-model" } },
|
|
186
212
|
};
|
|
187
213
|
}
|
|
188
214
|
// >= TWO_STEP_THRESHOLD 且多个 provider → 两步式:先选 provider
|
|
@@ -191,12 +217,14 @@ export function applyEnhancement(db, request, clientModel, sessionId, enhancemen
|
|
|
191
217
|
if (providers.length >= 2) {
|
|
192
218
|
const providerQs = buildProviderQuestions(providers);
|
|
193
219
|
return {
|
|
220
|
+
body: currentBody,
|
|
194
221
|
effectiveModel: clientModel,
|
|
195
222
|
originalModel: null,
|
|
196
223
|
interceptResponse: {
|
|
197
224
|
...buildAskUserQuestionPayload(providerQs, true, displayModels),
|
|
198
225
|
meta: { action: "Provider列表(AskUserQuestion)" },
|
|
199
226
|
},
|
|
227
|
+
meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: "select-model" } },
|
|
200
228
|
};
|
|
201
229
|
}
|
|
202
230
|
// 单 provider 且模型过多 → AskUserQuestion 显示前 6 个 + 文本列出剩余
|
|
@@ -206,65 +234,89 @@ export function applyEnhancement(db, request, clientModel, sessionId, enhancemen
|
|
|
206
234
|
if (displayModels.length > capped.length) {
|
|
207
235
|
const extra = displayModels.slice(capped.length).map((m, i) => `${capped.length + i + 1}. ${m}`).join("\n");
|
|
208
236
|
const textBlock = { type: "text", text: `更多模型:\n${extra}\n\n可输入 /select-model provider/model 选择` };
|
|
209
|
-
const
|
|
210
|
-
|
|
237
|
+
const payloadBody = payload.body;
|
|
238
|
+
payloadBody.content = [textBlock, ...payloadBody.content];
|
|
211
239
|
}
|
|
212
240
|
return {
|
|
241
|
+
body: currentBody,
|
|
213
242
|
effectiveModel: clientModel,
|
|
214
243
|
originalModel: null,
|
|
215
244
|
interceptResponse: {
|
|
216
245
|
...payload,
|
|
217
246
|
meta: { action: "模型列表(AskUserQuestion)" },
|
|
218
247
|
},
|
|
248
|
+
meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: "select-model" } },
|
|
219
249
|
};
|
|
220
250
|
}
|
|
221
251
|
// < TWO_STEP_THRESHOLD → AskUserQuestion 2 组
|
|
222
252
|
const questions = buildModelQuestions(displayModels);
|
|
223
253
|
return {
|
|
254
|
+
body: currentBody,
|
|
224
255
|
effectiveModel: clientModel,
|
|
225
256
|
originalModel: null,
|
|
226
257
|
interceptResponse: {
|
|
227
258
|
...buildAskUserQuestionPayload(questions, false),
|
|
228
259
|
meta: { action: "模型列表(AskUserQuestion)" },
|
|
229
260
|
},
|
|
261
|
+
meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: "select-model" } },
|
|
230
262
|
};
|
|
231
263
|
}
|
|
232
264
|
return {
|
|
265
|
+
body: currentBody,
|
|
233
266
|
effectiveModel: clientModel,
|
|
234
267
|
originalModel: null,
|
|
235
268
|
interceptResponse: {
|
|
236
|
-
...buildSelectModelResponse(db,
|
|
269
|
+
...buildSelectModelResponse(db, routerKey?.allowed_models ?? null),
|
|
237
270
|
meta: { action: "模型列表" },
|
|
238
271
|
},
|
|
272
|
+
meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: "select-model" } },
|
|
239
273
|
};
|
|
240
274
|
}
|
|
241
275
|
if (directive.modelName) {
|
|
242
276
|
// 内联模型指令 → resolveMapping 验证(client_model 格式)
|
|
243
277
|
const resolvedDirective = resolveMapping(db, directive.modelName, { now: new Date() });
|
|
244
278
|
if (resolvedDirective) {
|
|
245
|
-
modelState.set(
|
|
246
|
-
|
|
247
|
-
return {
|
|
279
|
+
modelState.set(routerKey?.id ?? null, directive.modelName, sessionId, clientModel, "directive");
|
|
280
|
+
const directiveBody = { ...currentBody, messages: directive.cleanedBody.messages };
|
|
281
|
+
return {
|
|
282
|
+
body: directiveBody,
|
|
283
|
+
effectiveModel: directive.modelName,
|
|
284
|
+
originalModel: clientModel,
|
|
285
|
+
interceptResponse: null,
|
|
286
|
+
meta: { router_tags_stripped: tagsStripped, directive: { type: directive.isCommandMessage ? "router_command" : "router_model", value: directive.modelName } },
|
|
287
|
+
};
|
|
248
288
|
}
|
|
249
289
|
// 映射失败时保留原始请求(降级策略)
|
|
250
|
-
return
|
|
290
|
+
return { body: currentBody, effectiveModel: clientModel, originalModel: null, interceptResponse: null, meta: { router_tags_stripped: tagsStripped, directive: null } };
|
|
251
291
|
}
|
|
252
292
|
// 无指令 → 查询会话记忆
|
|
253
|
-
const remembered = modelState.get(
|
|
293
|
+
const remembered = modelState.get(routerKey?.id ?? null, sessionId);
|
|
254
294
|
if (remembered) {
|
|
255
295
|
// 优先尝试 provider_name/backend_model 格式(select-model 命令存储)
|
|
256
296
|
// 直接保留该格式,resolveMapping 会解析出 provider + model
|
|
257
297
|
const providerResolved = resolveProviderModel(db, remembered);
|
|
258
298
|
if (providerResolved) {
|
|
259
|
-
return {
|
|
299
|
+
return {
|
|
300
|
+
body: currentBody,
|
|
301
|
+
effectiveModel: remembered,
|
|
302
|
+
originalModel: clientModel,
|
|
303
|
+
interceptResponse: null,
|
|
304
|
+
meta: { router_tags_stripped: tagsStripped, directive: { type: "select_model", value: remembered } },
|
|
305
|
+
};
|
|
260
306
|
}
|
|
261
307
|
// 回退到 client_model 格式(内联指令存储)
|
|
262
308
|
const resolvedRemembered = resolveMapping(db, remembered, { now: new Date() });
|
|
263
309
|
if (resolvedRemembered) {
|
|
264
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
body: currentBody,
|
|
312
|
+
effectiveModel: remembered,
|
|
313
|
+
originalModel: clientModel,
|
|
314
|
+
interceptResponse: null,
|
|
315
|
+
meta: { router_tags_stripped: tagsStripped, directive: { type: "router_model", value: remembered } },
|
|
316
|
+
};
|
|
265
317
|
}
|
|
266
318
|
}
|
|
267
|
-
return
|
|
319
|
+
return { body: currentBody, effectiveModel: clientModel, originalModel: null, interceptResponse: null, meta: { router_tags_stripped: tagsStripped, directive: null } };
|
|
268
320
|
}
|
|
269
321
|
/** 构造 Anthropic 格式的 router 文本响应 */
|
|
270
322
|
function buildTextResponse(type, inner) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { applyEnhancement, buildModelInfoTag } from "./enhancement-handler.js";
|
|
2
|
+
export type { EnhancementResult, EnhancementMeta, RouterKeyInfo } from "./enhancement-handler.js";
|
|
2
3
|
export { parseDirective, parseToolResult, TOOL_USE_ID_PREFIX, TOOL_USE_ID_PROVIDER_PREFIX } from "./directive-parser.js";
|
|
3
4
|
export { cleanRouterResponses } from "./response-cleaner.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import type { FastifyPluginCallback } from "fastify";
|
|
3
|
+
export interface AnthropicProxyOptions {
|
|
4
|
+
db: Database.Database;
|
|
5
|
+
container: import("../../core/container.js").ServiceContainer;
|
|
6
|
+
}
|
|
7
|
+
export declare const anthropicProxy: FastifyPluginCallback<AnthropicProxyOptions>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import fp from "fastify-plugin";
|
|
3
|
-
import { insertRequestLog } from "
|
|
4
|
-
import { createErrorFormatter } from "
|
|
3
|
+
import { insertRequestLog } from "../../db/index.js";
|
|
4
|
+
import { createErrorFormatter } from "../proxy-core.js";
|
|
5
5
|
import { handleProxyRequest } from "./proxy-handler.js";
|
|
6
|
-
import { createOrchestrator } from "
|
|
7
|
-
import { HTTP_BAD_GATEWAY } from "
|
|
6
|
+
import { createOrchestrator } from "../orchestration/orchestrator.js";
|
|
7
|
+
import { HTTP_BAD_GATEWAY } from "../../core/constants.js";
|
|
8
|
+
import { SERVICE_KEYS } from "../../core/container.js";
|
|
8
9
|
const MESSAGES_PATH = "/v1/messages";
|
|
9
10
|
const ANTHROPIC_ERROR_TYPE = {
|
|
10
11
|
modelNotFound: "not_found_error",
|
|
@@ -18,8 +19,8 @@ const ANTHROPIC_ERROR_TYPE = {
|
|
|
18
19
|
};
|
|
19
20
|
const anthropicErrors = createErrorFormatter((kind, message) => ({ type: "error", error: { type: ANTHROPIC_ERROR_TYPE[kind], message } }));
|
|
20
21
|
const anthropicProxyRaw = (app, opts, done) => {
|
|
21
|
-
const { db,
|
|
22
|
-
const orchestrator = createOrchestrator(semaphoreManager, tracker, adaptiveController);
|
|
22
|
+
const { db, container } = opts;
|
|
23
|
+
const orchestrator = createOrchestrator(container.resolve(SERVICE_KEYS.semaphoreManager), container.resolve(SERVICE_KEYS.tracker), container.resolve(SERVICE_KEYS.adaptiveController));
|
|
23
24
|
app.post(MESSAGES_PATH, async (request, reply) => {
|
|
24
25
|
if (!orchestrator) {
|
|
25
26
|
const body = request.body;
|
|
@@ -34,7 +35,7 @@ const anthropicProxyRaw = (app, opts, done) => {
|
|
|
34
35
|
const e = anthropicErrors.providerUnavailable();
|
|
35
36
|
return reply.code(e.statusCode).send(e.body);
|
|
36
37
|
}
|
|
37
|
-
const deps = { db,
|
|
38
|
+
const deps = { db, orchestrator, container };
|
|
38
39
|
return handleProxyRequest(request, reply, "anthropic", MESSAGES_PATH, anthropicErrors, deps);
|
|
39
40
|
});
|
|
40
41
|
done();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FastifyPluginCallback } from "fastify";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
export interface OpenaiProxyOptions {
|
|
4
|
+
db: Database.Database;
|
|
5
|
+
container: import("../../core/container.js").ServiceContainer;
|
|
6
|
+
}
|
|
7
|
+
export declare const openaiProxy: FastifyPluginCallback<OpenaiProxyOptions>;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import fp from "fastify-plugin";
|
|
3
|
-
import { getActiveProviders, insertRequestLog } from "
|
|
4
|
-
import { getSetting } from "
|
|
5
|
-
import { decrypt } from "
|
|
6
|
-
import { proxyGetRequest, createErrorFormatter } from "
|
|
3
|
+
import { getActiveProviders, insertRequestLog } from "../../db/index.js";
|
|
4
|
+
import { getSetting } from "../../db/settings.js";
|
|
5
|
+
import { decrypt } from "../../utils/crypto.js";
|
|
6
|
+
import { proxyGetRequest, createErrorFormatter } from "../proxy-core.js";
|
|
7
7
|
import { handleProxyRequest } from "./proxy-handler.js";
|
|
8
|
-
import { createOrchestrator } from "
|
|
9
|
-
import { HTTP_NOT_FOUND, HTTP_BAD_GATEWAY } from "
|
|
8
|
+
import { createOrchestrator } from "../orchestration/orchestrator.js";
|
|
9
|
+
import { HTTP_NOT_FOUND, HTTP_BAD_GATEWAY } from "../../core/constants.js";
|
|
10
|
+
import { SERVICE_KEYS } from "../../core/container.js";
|
|
10
11
|
const CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
|
|
11
12
|
const MODELS_PATH = "/v1/models";
|
|
12
13
|
const OPENAI_ERROR_META = {
|
|
@@ -24,8 +25,8 @@ function sendError(reply, e) {
|
|
|
24
25
|
return reply.code(e.statusCode).send(e.body);
|
|
25
26
|
}
|
|
26
27
|
const openaiProxyRaw = (app, opts, done) => {
|
|
27
|
-
const { db,
|
|
28
|
-
const orchestrator = createOrchestrator(semaphoreManager, tracker, adaptiveController);
|
|
28
|
+
const { db, container } = opts;
|
|
29
|
+
const orchestrator = createOrchestrator(container.resolve(SERVICE_KEYS.semaphoreManager), container.resolve(SERVICE_KEYS.tracker), container.resolve(SERVICE_KEYS.adaptiveController));
|
|
29
30
|
app.post(CHAT_COMPLETIONS_PATH, async (request, reply) => {
|
|
30
31
|
if (!orchestrator) {
|
|
31
32
|
const body = request.body;
|
|
@@ -39,7 +40,7 @@ const openaiProxyRaw = (app, opts, done) => {
|
|
|
39
40
|
});
|
|
40
41
|
return sendError(reply, openaiErrors.providerUnavailable());
|
|
41
42
|
}
|
|
42
|
-
const deps = { db,
|
|
43
|
+
const deps = { db, orchestrator, container };
|
|
43
44
|
return handleProxyRequest(request, reply, "openai", CHAT_COMPLETIONS_PATH, openaiErrors, deps, {
|
|
44
45
|
beforeSendProxy: (body, isStream) => {
|
|
45
46
|
if (isStream && !body.stream_options) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ContentBlock } from "../../monitor/types.js";
|
|
2
|
+
import type { ToolCallRecord } from "../loop-prevention/types.js";
|
|
3
|
+
import type { TransportResult } from "../types.js";
|
|
4
|
+
/** 从 TransportResult 中提取最终 HTTP status code */
|
|
5
|
+
export declare function getTransportStatusCode(result: TransportResult): number | null;
|
|
6
|
+
/** 将 tracker blocks 序列化为前端 tryDirectParse 可解析的 JSON */
|
|
7
|
+
export declare function serializeBlocksForStorage(blocks: ContentBlock[] | undefined, apiType: "openai" | "anthropic"): string;
|
|
8
|
+
/** 从请求体中提取最后一次工具调用记录 */
|
|
9
|
+
export declare function extractLastToolUse(body: Record<string, unknown>): ToolCallRecord | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
const HASH_DIGEST_LENGTH = 16;
|
|
3
|
+
/** 从 TransportResult 中提取最终 HTTP status code */
|
|
4
|
+
export function getTransportStatusCode(result) {
|
|
5
|
+
if (result.kind === "success" || result.kind === "error" || result.kind === "stream_error")
|
|
6
|
+
return result.statusCode;
|
|
7
|
+
if (result.kind === "stream_success" || result.kind === "stream_abort")
|
|
8
|
+
return result.statusCode;
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
/** 将 tracker blocks 序列化为前端 tryDirectParse 可解析的 JSON */
|
|
12
|
+
export function serializeBlocksForStorage(blocks, apiType) {
|
|
13
|
+
if (!blocks || blocks.length === 0)
|
|
14
|
+
return "";
|
|
15
|
+
if (apiType === "anthropic") {
|
|
16
|
+
const content = blocks.map(b => {
|
|
17
|
+
if (b.type === "thinking")
|
|
18
|
+
return { type: "thinking", thinking: b.content };
|
|
19
|
+
if (b.type === "tool_use") {
|
|
20
|
+
let input = {};
|
|
21
|
+
// eslint-disable-next-line taste/no-silent-catch
|
|
22
|
+
try {
|
|
23
|
+
input = JSON.parse(b.content || "{}");
|
|
24
|
+
}
|
|
25
|
+
catch { /* tool_use content 非合法 JSON 时保留空对象 */ }
|
|
26
|
+
return { type: "tool_use", name: b.name ?? "", input };
|
|
27
|
+
}
|
|
28
|
+
return { type: "text", text: b.content };
|
|
29
|
+
});
|
|
30
|
+
return JSON.stringify({ content });
|
|
31
|
+
}
|
|
32
|
+
const text = blocks.filter(b => b.type === "text").map(b => b.content).join("");
|
|
33
|
+
return JSON.stringify({ choices: [{ message: { content: text } }] });
|
|
34
|
+
}
|
|
35
|
+
/** 从请求体中提取最后一次工具调用记录 */
|
|
36
|
+
export function extractLastToolUse(body) {
|
|
37
|
+
const messages = body.messages;
|
|
38
|
+
if (!messages)
|
|
39
|
+
return null;
|
|
40
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
41
|
+
const msg = messages[i];
|
|
42
|
+
if (msg.role !== "assistant")
|
|
43
|
+
continue;
|
|
44
|
+
const content = msg.content;
|
|
45
|
+
if (!Array.isArray(content))
|
|
46
|
+
continue;
|
|
47
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
48
|
+
const block = content[j];
|
|
49
|
+
if (block.type === "tool_use") {
|
|
50
|
+
const inputText = JSON.stringify(block.input ?? {});
|
|
51
|
+
const inputHash = createHash("sha256").update(inputText).digest("hex").slice(0, HASH_DIGEST_LENGTH);
|
|
52
|
+
return {
|
|
53
|
+
toolName: block.name ?? "unknown",
|
|
54
|
+
toolUseId: block.id,
|
|
55
|
+
inputHash,
|
|
56
|
+
inputText,
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
import type { ProxyOrchestrator } from "../orchestration/orchestrator.js";
|
|
4
|
+
import type { ProxyErrorFormatter } from "../proxy-core.js";
|
|
5
|
+
export interface RouteHandlerDeps {
|
|
6
|
+
db: Database.Database;
|
|
7
|
+
orchestrator: ProxyOrchestrator;
|
|
8
|
+
container: ServiceContainer;
|
|
9
|
+
}
|
|
10
|
+
import type { ServiceContainer } from "../../core/container.js";
|
|
11
|
+
export declare function handleProxyRequest(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "anthropic", upstreamPath: string, errors: ProxyErrorFormatter, deps: RouteHandlerDeps, options?: {
|
|
12
|
+
beforeSendProxy?: (body: Record<string, unknown>, isStream: boolean) => void;
|
|
13
|
+
}): Promise<FastifyReply>;
|