llm-simple-router 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/routes.d.ts +1 -0
- package/dist/admin/routes.js +3 -1
- package/dist/admin/settings-import-export.d.ts +1 -0
- package/dist/admin/settings-import-export.js +7 -0
- package/dist/admin/transform-rules.d.ts +8 -0
- package/dist/admin/transform-rules.js +38 -0
- package/dist/admin/usage.js +1 -1
- package/dist/core/container.d.ts +1 -0
- package/dist/core/container.js +1 -0
- package/dist/db/migrations/034_create_provider_transform_rules.sql +11 -0
- package/dist/db/transform-rules.d.ts +16 -0
- package/dist/db/transform-rules.js +51 -0
- package/dist/index.js +8 -1
- package/dist/metrics/sse-parser.d.ts +2 -0
- package/dist/metrics/sse-parser.js +4 -0
- package/dist/monitor/request-tracker.js +8 -1
- package/dist/monitor/types.d.ts +1 -1
- package/dist/proxy/enhancement/response-cleaner.js +14 -6
- package/dist/proxy/handler/openai.js +13 -4
- package/dist/proxy/handler/proxy-handler-utils.js +2 -7
- package/dist/proxy/handler/proxy-handler.js +72 -15
- package/dist/proxy/patch/deepseek/index.d.ts +15 -3
- package/dist/proxy/patch/deepseek/index.js +29 -6
- package/dist/proxy/patch/deepseek/patch-cache-control.d.ts +6 -0
- package/dist/proxy/patch/deepseek/patch-cache-control.js +30 -0
- package/dist/proxy/patch/deepseek/patch-non-deepseek-tools.d.ts +16 -0
- package/dist/proxy/patch/deepseek/patch-non-deepseek-tools.js +74 -0
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +10 -1
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +58 -15
- package/dist/proxy/patch/deepseek/patch-thinking-blocks.d.ts +5 -1
- package/dist/proxy/patch/deepseek/patch-thinking-blocks.js +37 -4
- package/dist/proxy/patch/deepseek/patch-thinking-param.d.ts +6 -0
- package/dist/proxy/patch/deepseek/patch-thinking-param.js +32 -0
- package/dist/proxy/patch/deepseek/utils.d.ts +8 -0
- package/dist/proxy/patch/deepseek/utils.js +38 -0
- package/dist/proxy/patch/index.d.ts +2 -2
- package/dist/proxy/patch/index.js +50 -4
- package/dist/proxy/patch/router-cleanup.js +1 -24
- package/dist/proxy/patch/safe-sse-parser.d.ts +9 -0
- package/dist/proxy/patch/safe-sse-parser.js +16 -0
- package/dist/proxy/proxy-core.js +1 -0
- package/dist/proxy/proxy-logging.d.ts +1 -1
- package/dist/proxy/proxy-logging.js +3 -3
- package/dist/proxy/transform/id-utils.d.ts +3 -0
- package/dist/proxy/transform/id-utils.js +9 -0
- package/dist/proxy/transform/message-mapper.d.ts +15 -0
- package/dist/proxy/transform/message-mapper.js +173 -0
- package/dist/proxy/transform/plugin-registry.d.ts +23 -0
- package/dist/proxy/transform/plugin-registry.js +130 -0
- package/dist/proxy/transform/plugin-types.d.ts +46 -0
- package/dist/proxy/transform/plugin-types.js +15 -0
- package/dist/proxy/transform/provider-meta.d.ts +29 -0
- package/dist/proxy/transform/provider-meta.js +72 -0
- package/dist/proxy/transform/request-transform.d.ts +4 -0
- package/dist/proxy/transform/request-transform.js +151 -0
- package/dist/proxy/transform/response-transform.d.ts +4 -0
- package/dist/proxy/transform/response-transform.js +99 -0
- package/dist/proxy/transform/sanitize.d.ts +3 -0
- package/dist/proxy/transform/sanitize.js +24 -0
- package/dist/proxy/transform/stream-ant2oa.d.ts +20 -0
- package/dist/proxy/transform/stream-ant2oa.js +200 -0
- package/dist/proxy/transform/stream-oa2ant.d.ts +25 -0
- package/dist/proxy/transform/stream-oa2ant.js +201 -0
- package/dist/proxy/transform/stream-transform-base.d.ts +19 -0
- package/dist/proxy/transform/stream-transform-base.js +61 -0
- package/dist/proxy/transform/thinking-mapper.d.ts +4 -0
- package/dist/proxy/transform/thinking-mapper.js +15 -0
- package/dist/proxy/transform/tool-mapper.d.ts +8 -0
- package/dist/proxy/transform/tool-mapper.js +67 -0
- package/dist/proxy/transform/transform-coordinator.d.ts +11 -0
- package/dist/proxy/transform/transform-coordinator.js +32 -0
- package/dist/proxy/transform/types.d.ts +43 -0
- package/dist/proxy/transform/types.js +1 -0
- package/dist/proxy/transform/usage-mapper.d.ts +8 -0
- package/dist/proxy/transform/usage-mapper.js +46 -0
- package/dist/proxy/transport/stream.d.ts +1 -1
- package/dist/proxy/transport/stream.js +19 -10
- package/dist/proxy/transport/transport-fn.d.ts +3 -0
- package/dist/proxy/transport/transport-fn.js +11 -4
- package/frontend-dist/assets/{CardContent-CxOF1feY.js → CardContent-WAXChVto.js} +1 -1
- package/frontend-dist/assets/{CardTitle-BSEFcEOM.js → CardTitle-2TP7C40C.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-DTwksDPZ.js → CascadingModelSelect-CBe9pEx_.js} +1 -1
- package/frontend-dist/assets/{Checkbox-RfsERG07.js → Checkbox-Bbf909ia.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-Dsjo7QlC.js → CollapsibleTrigger-H2GGomkv.js} +1 -1
- package/frontend-dist/assets/{Collection-rQ4eIYfa.js → Collection-BdkMCE5V.js} +1 -1
- package/frontend-dist/assets/{Dashboard-YejfAPiB.js → Dashboard-BQvc6U98.js} +1 -1
- package/frontend-dist/assets/{DialogTitle-DeFTnmgC.js → DialogTitle-CS0Nuvko.js} +1 -1
- package/frontend-dist/assets/{Input-CENz_g9t.js → Input-B56t8UfI.js} +1 -1
- package/frontend-dist/assets/{Label-BAciBrrd.js → Label-CXRQoDIZ.js} +1 -1
- package/frontend-dist/assets/{Login-DQkYFq7R.js → Login-DRNqP0bt.js} +1 -1
- package/frontend-dist/assets/{Logs-Dol8AX7z.js → Logs-DeJosCWl.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-VEYW1TrW.js → ModelMappings-DV-NmdF7.js} +1 -1
- package/frontend-dist/assets/{Monitor-C0r9WefB.js → Monitor-DwqkpkuK.js} +1 -1
- package/frontend-dist/assets/{PopoverTrigger-Cyqik5SE.js → PopoverTrigger-srlKRM2q.js} +1 -1
- package/frontend-dist/assets/{PopperContent-B7IuAHeq.js → PopperContent-9j4ZA5oc.js} +1 -1
- package/frontend-dist/assets/Providers-oYOUgEsH.js +1 -0
- package/frontend-dist/assets/{ProxyEnhancement-Kn8r2SN6.js → ProxyEnhancement-3tQzXNGn.js} +1 -1
- package/frontend-dist/assets/{RetryRules-F0295m4_.js → RetryRules-rlrPpTd0.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-CFbPtUE_.js → RouterKeys-COpe69A8.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-D291Vjh8.js → RovingFocusItem-DbXUYGXA.js} +1 -1
- package/frontend-dist/assets/{Schedules-DWhF3uod.js → Schedules-gszIRN_S.js} +1 -1
- package/frontend-dist/assets/{SelectValue-BWlgUZa3.js → SelectValue-CpY2uWSk.js} +1 -1
- package/frontend-dist/assets/{Settings-BnIzEF_k.js → Settings-Xu6V0Sve.js} +1 -1
- package/frontend-dist/assets/{Setup-BglKyQKq.js → Setup-BfcLFnBR.js} +1 -1
- package/frontend-dist/assets/{Switch-DyCR-CPu.js → Switch-DVfy7Q4A.js} +1 -1
- package/frontend-dist/assets/{TableHeader-DVUlBL35.js → TableHeader-BqZo28x_.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-BU1DY-C8.js → TabsTrigger-0L00h4oy.js} +1 -1
- package/frontend-dist/assets/{Teleport-BQgusr9g.js → Teleport-Czq5P0IN.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-Bv_QoBns.js → TooltipTrigger-ChkMBqtC.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-f_evI835.js → UnifiedRequestDialog-B3GxGgwz.js} +1 -1
- package/frontend-dist/assets/{VisuallyHidden-Con10z4F.js → VisuallyHidden-DAFM-4dn.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-yrDtxucb.js → VisuallyHiddenInput-BwfFtaqi.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-2Db6Z7JQ.js → alert-dialog-vdmYFjPE.js} +1 -1
- package/frontend-dist/assets/arrow-down-Da-mukXD.js +1 -0
- package/frontend-dist/assets/{badge-DEhZfeI0.js → badge-0gUIxSR9.js} +1 -1
- package/frontend-dist/assets/button-CLKo-tBF.js +12 -0
- package/frontend-dist/assets/check-CEd3A-kB.js +1 -0
- package/frontend-dist/assets/{copy-CwqZSuIG.js → copy-7NlsO7pN.js} +1 -1
- package/frontend-dist/assets/{dialog-CVMKSdPr.js → dialog-DUkre9Rw.js} +1 -1
- package/frontend-dist/assets/{file-text-D0K8Hovo.js → file-text-Di1QQ-B6.js} +1 -1
- package/frontend-dist/assets/{index-Ct718O93.js → index-Be9MymBh.js} +1 -1
- package/frontend-dist/assets/index-Bz_ZaXNn.css +1 -0
- package/frontend-dist/assets/{lib-H3YI7EK4.js → lib-CweCSowO.js} +1 -1
- package/frontend-dist/assets/loader-circle-Cb19pB9Z.js +1 -0
- package/frontend-dist/assets/{useClipboard-Cd7k-5Yq.js → useClipboard-tSRRbabN.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-luoLXnwV.js → useFocusGuards-BxD_AgQe.js} +1 -1
- package/frontend-dist/assets/useFormControl-DP5JWFRS.js +1 -0
- package/frontend-dist/assets/{useLogRetention-DB4Iu6o_.js → useLogRetention-DeCxyOiV.js} +1 -1
- package/frontend-dist/assets/useNonce-DTHUE2m9.js +1 -0
- package/frontend-dist/assets/x-ztwrQbIz.js +1 -0
- package/frontend-dist/index.html +20 -20
- package/package.json +1 -1
- package/frontend-dist/assets/Providers-D8Z97edN.js +0 -1
- package/frontend-dist/assets/arrow-down-WyouvE7T.js +0 -1
- package/frontend-dist/assets/button-Cnkbp_6J.js +0 -12
- package/frontend-dist/assets/check-BuqB5Nyb.js +0 -1
- package/frontend-dist/assets/index-xjdbFKXJ.css +0 -1
- package/frontend-dist/assets/loader-circle-Be82FnVY.js +0 -1
- package/frontend-dist/assets/useFormControl-Da4ViGZF.js +0 -1
- package/frontend-dist/assets/useNonce-DvAdQ48J.js +0 -1
- package/frontend-dist/assets/x-DB22csQl.js +0 -1
package/dist/admin/routes.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ interface AdminRoutesOptions {
|
|
|
10
10
|
adaptiveController?: AdaptiveConcurrencyController;
|
|
11
11
|
logFileWriter?: import("../storage/log-file-writer.js").LogFileWriter | null;
|
|
12
12
|
logsDir?: string;
|
|
13
|
+
pluginRegistry?: import("../proxy/transform/plugin-registry.js").PluginRegistry;
|
|
13
14
|
closeFn?: () => Promise<void>;
|
|
14
15
|
}
|
|
15
16
|
export declare const adminRoutes: FastifyPluginCallback<AdminRoutesOptions>;
|
package/dist/admin/routes.js
CHANGED
|
@@ -15,6 +15,7 @@ import { adminRecommendedRoutes } from "./recommended.js";
|
|
|
15
15
|
import { adminUsageRoutes } from "./usage.js";
|
|
16
16
|
import { adminUpgradeRoutes } from "./upgrade.js";
|
|
17
17
|
import { adminImportExportRoutes } from "./settings-import-export.js";
|
|
18
|
+
import { adminTransformRuleRoutes } from "./transform-rules.js";
|
|
18
19
|
import { adminScheduleRoutes } from "./schedules.js";
|
|
19
20
|
export const adminRoutes = (app, options, done) => {
|
|
20
21
|
// Setup 路由不需要 auth
|
|
@@ -33,9 +34,10 @@ export const adminRoutes = (app, options, done) => {
|
|
|
33
34
|
app.register(adminProxyEnhancementRoutes, { db: options.db, stateRegistry: options.stateRegistry });
|
|
34
35
|
app.register(adminMonitorRoutes, { tracker: options.tracker });
|
|
35
36
|
app.register(adminSettingsRoutes, { db: options.db, logsDir: options.logsDir });
|
|
36
|
-
app.register(adminImportExportRoutes, { db: options.db, stateRegistry: options.stateRegistry });
|
|
37
|
+
app.register(adminImportExportRoutes, { db: options.db, stateRegistry: options.stateRegistry, pluginRegistry: options.pluginRegistry });
|
|
37
38
|
app.register(adminRecommendedRoutes, { db: options.db });
|
|
38
39
|
app.register(adminUsageRoutes, { db: options.db });
|
|
39
40
|
app.register(adminUpgradeRoutes, { db: options.db, closeFn: options.closeFn ?? (async () => { }) });
|
|
41
|
+
app.register(adminTransformRuleRoutes, { db: options.db, pluginRegistry: options.pluginRegistry });
|
|
40
42
|
done();
|
|
41
43
|
};
|
|
@@ -4,6 +4,7 @@ import type { StateRegistry } from "../core/registry.js";
|
|
|
4
4
|
interface ImportExportOptions {
|
|
5
5
|
db: Database.Database;
|
|
6
6
|
stateRegistry: StateRegistry;
|
|
7
|
+
pluginRegistry?: import("../proxy/transform/plugin-registry.js").PluginRegistry;
|
|
7
8
|
}
|
|
8
9
|
export declare const adminImportExportRoutes: FastifyPluginCallback<ImportExportOptions>;
|
|
9
10
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
|
+
import { resolve } from "path";
|
|
2
3
|
import { encrypt, decrypt } from "../utils/crypto.js";
|
|
3
4
|
import { getSetting } from "../db/settings.js";
|
|
4
5
|
import { API_CODE, apiError } from "./api-response.js";
|
|
@@ -12,6 +13,7 @@ const CONFIG_TABLES = [
|
|
|
12
13
|
"schedules",
|
|
13
14
|
"provider_model_info",
|
|
14
15
|
"session_model_history",
|
|
16
|
+
"provider_transform_rules",
|
|
15
17
|
];
|
|
16
18
|
// settings 表按 key 列的值过滤,不覆盖本地安全敏感配置
|
|
17
19
|
const PROTECTED_SETTING_KEYS = new Set(["admin_password_hash", "jwt_secret", "encryption_key"]);
|
|
@@ -129,6 +131,11 @@ export const adminImportExportRoutes = (app, options, done) => {
|
|
|
129
131
|
stateRegistry.removeAllProviders();
|
|
130
132
|
stateRegistry.clearModelState();
|
|
131
133
|
stateRegistry.reinitializeProviders();
|
|
134
|
+
// 刷新 transform plugin 缓存(从 DB 重新加载规则 + 扫描插件目录)
|
|
135
|
+
if (options.pluginRegistry) {
|
|
136
|
+
const pluginsDir = resolve(process.cwd(), "plugins/transform");
|
|
137
|
+
options.pluginRegistry.reload(options.db, pluginsDir);
|
|
138
|
+
}
|
|
132
139
|
return reply.send(counts);
|
|
133
140
|
});
|
|
134
141
|
done();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyPluginCallback } from "fastify";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
interface TransformRuleOptions {
|
|
4
|
+
db: Database.Database;
|
|
5
|
+
pluginRegistry?: import("../proxy/transform/plugin-registry.js").PluginRegistry;
|
|
6
|
+
}
|
|
7
|
+
export declare const adminTransformRuleRoutes: FastifyPluginCallback<TransformRuleOptions>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { resolve } from "path";
|
|
2
|
+
import { getTransformRule, upsertTransformRule, deleteTransformRule, getAllActiveRules } from "../db/transform-rules.js";
|
|
3
|
+
const ALLOWED_FIELDS = new Set([
|
|
4
|
+
"inject_headers", "request_defaults", "drop_fields", "field_overrides", "plugin_name", "is_active",
|
|
5
|
+
]);
|
|
6
|
+
export const adminTransformRuleRoutes = (app, options, done) => {
|
|
7
|
+
const { db } = options;
|
|
8
|
+
app.get("/admin/api/transform-rules/:providerId", async (req) => {
|
|
9
|
+
const { providerId } = req.params;
|
|
10
|
+
const rule = getTransformRule(db, providerId);
|
|
11
|
+
return { code: 0, message: "ok", data: rule };
|
|
12
|
+
});
|
|
13
|
+
app.put("/admin/api/transform-rules/:providerId", async (req) => {
|
|
14
|
+
const { providerId } = req.params;
|
|
15
|
+
const updates = {};
|
|
16
|
+
for (const [key, val] of Object.entries(req.body)) {
|
|
17
|
+
if (ALLOWED_FIELDS.has(key))
|
|
18
|
+
updates[key] = val;
|
|
19
|
+
}
|
|
20
|
+
upsertTransformRule(db, providerId, updates);
|
|
21
|
+
return { code: 0, message: "ok", data: { success: true } };
|
|
22
|
+
});
|
|
23
|
+
app.delete("/admin/api/transform-rules/:providerId", async (req) => {
|
|
24
|
+
const { providerId } = req.params;
|
|
25
|
+
deleteTransformRule(db, providerId);
|
|
26
|
+
return { code: 0, message: "ok", data: { success: true } };
|
|
27
|
+
});
|
|
28
|
+
app.post("/admin/api/transform-rules/reload", async () => {
|
|
29
|
+
if (options.pluginRegistry) {
|
|
30
|
+
const pluginsDir = resolve(process.cwd(), "plugins/transform");
|
|
31
|
+
const result = options.pluginRegistry.reload(options.db, pluginsDir);
|
|
32
|
+
return { code: 0, message: "ok", data: result };
|
|
33
|
+
}
|
|
34
|
+
const rules = getAllActiveRules(db);
|
|
35
|
+
return { code: 0, message: "ok", data: { loadedPlugins: [], rulesCount: rules.length } };
|
|
36
|
+
});
|
|
37
|
+
done();
|
|
38
|
+
};
|
package/dist/admin/usage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getWindowsInRange, getWindowUsage } from "../db/usage-windows.js";
|
|
3
|
-
import { getProviderById } from "../db/
|
|
3
|
+
import { getProviderById } from "../db/index.js";
|
|
4
4
|
import { resolveTimeRange } from "../utils/time-range.js";
|
|
5
5
|
const UsageQuerySchema = Type.Object({
|
|
6
6
|
router_key_id: Type.Optional(Type.String()),
|
package/dist/core/container.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const SERVICE_KEYS: {
|
|
|
7
7
|
readonly usageWindowTracker: "usageWindowTracker";
|
|
8
8
|
readonly sessionTracker: "sessionTracker";
|
|
9
9
|
readonly adaptiveController: "adaptiveController";
|
|
10
|
+
readonly pluginRegistry: "pluginRegistry";
|
|
10
11
|
readonly logFileWriter: "logFileWriter";
|
|
11
12
|
};
|
|
12
13
|
export type ServiceKey = (typeof SERVICE_KEYS)[keyof typeof SERVICE_KEYS];
|
package/dist/core/container.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS provider_transform_rules (
|
|
2
|
+
provider_id TEXT PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,
|
|
3
|
+
inject_headers TEXT,
|
|
4
|
+
request_defaults TEXT,
|
|
5
|
+
drop_fields TEXT,
|
|
6
|
+
field_overrides TEXT,
|
|
7
|
+
plugin_name TEXT,
|
|
8
|
+
is_active INTEGER DEFAULT 1,
|
|
9
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
10
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
11
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
export interface TransformRules {
|
|
3
|
+
provider_id: string;
|
|
4
|
+
inject_headers: Record<string, string> | null;
|
|
5
|
+
request_defaults: Record<string, unknown> | null;
|
|
6
|
+
drop_fields: string[] | null;
|
|
7
|
+
field_overrides: Record<string, unknown> | null;
|
|
8
|
+
plugin_name: string | null;
|
|
9
|
+
is_active: number;
|
|
10
|
+
created_at?: string;
|
|
11
|
+
updated_at?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function getTransformRule(db: Database.Database, providerId: string): TransformRules | null;
|
|
14
|
+
export declare function upsertTransformRule(db: Database.Database, providerId: string, rules: Partial<Omit<TransformRules, "provider_id">>): void;
|
|
15
|
+
export declare function deleteTransformRule(db: Database.Database, providerId: string): void;
|
|
16
|
+
export declare function getAllActiveRules(db: Database.Database): TransformRules[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const JSON_COLUMNS = ["inject_headers", "request_defaults", "drop_fields", "field_overrides"];
|
|
2
|
+
function parseJsonColumns(row) {
|
|
3
|
+
const result = { ...row };
|
|
4
|
+
for (const col of JSON_COLUMNS) {
|
|
5
|
+
if (result[col]) {
|
|
6
|
+
try {
|
|
7
|
+
result[col] = JSON.parse(result[col]);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
console.error(`[transform-rules] Failed to parse JSON column "${col}", keeping raw value`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
export function getTransformRule(db, providerId) {
|
|
17
|
+
const row = db.prepare("SELECT * FROM provider_transform_rules WHERE provider_id = ?").get(providerId);
|
|
18
|
+
if (!row)
|
|
19
|
+
return null;
|
|
20
|
+
return parseJsonColumns(row);
|
|
21
|
+
}
|
|
22
|
+
export function upsertTransformRule(db, providerId, rules) {
|
|
23
|
+
const existing = db.prepare("SELECT provider_id FROM provider_transform_rules WHERE provider_id = ?").get(providerId);
|
|
24
|
+
if (existing) {
|
|
25
|
+
const fields = [];
|
|
26
|
+
const values = [];
|
|
27
|
+
const jsonFields = new Set(["inject_headers", "request_defaults", "drop_fields", "field_overrides"]);
|
|
28
|
+
for (const [key, val] of Object.entries(rules)) {
|
|
29
|
+
if (key === "provider_id")
|
|
30
|
+
continue;
|
|
31
|
+
fields.push(`${key} = ?`);
|
|
32
|
+
values.push(jsonFields.has(key) && val ? JSON.stringify(val) : val);
|
|
33
|
+
}
|
|
34
|
+
if (fields.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
fields.push("updated_at = datetime('now')");
|
|
37
|
+
values.push(providerId);
|
|
38
|
+
db.prepare(`UPDATE provider_transform_rules SET ${fields.join(", ")} WHERE provider_id = ?`).run(...values);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
db.prepare(`INSERT INTO provider_transform_rules (provider_id, inject_headers, request_defaults, drop_fields, field_overrides, plugin_name, is_active)
|
|
42
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(providerId, rules.inject_headers ? JSON.stringify(rules.inject_headers) : null, rules.request_defaults ? JSON.stringify(rules.request_defaults) : null, rules.drop_fields ? JSON.stringify(rules.drop_fields) : null, rules.field_overrides ? JSON.stringify(rules.field_overrides) : null, rules.plugin_name ?? null, rules.is_active ?? 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function deleteTransformRule(db, providerId) {
|
|
46
|
+
db.prepare("DELETE FROM provider_transform_rules WHERE provider_id = ?").run(providerId);
|
|
47
|
+
}
|
|
48
|
+
export function getAllActiveRules(db) {
|
|
49
|
+
const rows = db.prepare("SELECT * FROM provider_transform_rules WHERE is_active = 1").all();
|
|
50
|
+
return rows.map(parseJsonColumns);
|
|
51
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { openaiProxy } from "./proxy/handler/openai.js";
|
|
|
19
19
|
import { anthropicProxy } from "./proxy/handler/anthropic.js";
|
|
20
20
|
import { adminRoutes } from "./admin/routes.js";
|
|
21
21
|
import { RetryRuleMatcher } from "./proxy/orchestration/retry-rules.js";
|
|
22
|
+
import { PluginRegistry } from "./proxy/transform/plugin-registry.js";
|
|
22
23
|
import { ProviderSemaphoreManager } from "./proxy/orchestration/semaphore.js";
|
|
23
24
|
import { AdaptiveConcurrencyController } from "./proxy/adaptive-controller.js";
|
|
24
25
|
import { loadEnhancementConfig } from "./proxy/routing/enhancement-config.js";
|
|
@@ -210,6 +211,12 @@ export async function buildApp(options) {
|
|
|
210
211
|
const ac = new AdaptiveConcurrencyController(c.resolve(SERVICE_KEYS.semaphoreManager), app.log);
|
|
211
212
|
return ac;
|
|
212
213
|
});
|
|
214
|
+
// 注册 PluginRegistry(从 DB 和 plugins 目录加载转换插件)
|
|
215
|
+
const pluginRegistry = new PluginRegistry();
|
|
216
|
+
pluginRegistry.loadFromDB(db);
|
|
217
|
+
const pluginsDir = path.resolve(__dirname, "../plugins/transform");
|
|
218
|
+
pluginRegistry.scanPluginsDir(pluginsDir);
|
|
219
|
+
container.register(SERVICE_KEYS.pluginRegistry, () => pluginRegistry);
|
|
213
220
|
// 从容器解析所有服务
|
|
214
221
|
const matcher = container.resolve(SERVICE_KEYS.matcher);
|
|
215
222
|
const semaphoreManager = container.resolve(SERVICE_KEYS.semaphoreManager);
|
|
@@ -244,7 +251,7 @@ export async function buildApp(options) {
|
|
|
244
251
|
// Late-bound close ref — close 函数在 adminRoutes 注册之后才定义,
|
|
245
252
|
// 但 restart API 需要在运行时调用它
|
|
246
253
|
const closeRef = { fn: async () => { } };
|
|
247
|
-
app.register(adminRoutes, { db, stateRegistry, tracker, adaptiveController, logFileWriter, logsDir, closeFn: () => closeRef.fn() });
|
|
254
|
+
app.register(adminRoutes, { db, stateRegistry, tracker, adaptiveController, logFileWriter, logsDir, closeFn: () => closeRef.fn(), pluginRegistry });
|
|
248
255
|
// 前端静态文件服务(生产环境)
|
|
249
256
|
const frontendDist = path.resolve(process.env.FRONTEND_DIST || path.join(__dirname, "../frontend-dist"));
|
|
250
257
|
if (existsSync(frontendDist)) {
|
|
@@ -224,7 +224,14 @@ export class RequestTracker {
|
|
|
224
224
|
// request_start: 无需处理,已是原始数据
|
|
225
225
|
// request_complete: strip clientRequest(完成后从 DB 加载详情)
|
|
226
226
|
let payload = data;
|
|
227
|
-
if (event === "
|
|
227
|
+
if (event === "request_update" && Array.isArray(data)) {
|
|
228
|
+
payload = data.map((req) => {
|
|
229
|
+
const copy = { ...req };
|
|
230
|
+
delete copy.clientRequest;
|
|
231
|
+
return copy;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else if ((event === "request_complete" || event === "request_start") && data && typeof data === "object") {
|
|
228
235
|
const copy = { ...data };
|
|
229
236
|
delete copy.clientRequest;
|
|
230
237
|
payload = copy;
|
package/dist/monitor/types.d.ts
CHANGED
|
@@ -40,19 +40,27 @@ export function cleanRouterResponses(body) {
|
|
|
40
40
|
}
|
|
41
41
|
if (msg.role === "assistant") {
|
|
42
42
|
const blocks = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
43
|
-
// 清理 router synthetic AskUserQuestion 的 tool_use
|
|
43
|
+
// 清理 router synthetic AskUserQuestion 的 tool_use 消息(Anthropic content blocks)
|
|
44
44
|
const toolUseBlocks = blocks.filter((b) => b && typeof b === "object" && b.type === "tool_use"
|
|
45
45
|
&& b.name === "AskUserQuestion"
|
|
46
46
|
&& typeof b.id === "string"
|
|
47
47
|
&& b.id.startsWith(TOOL_USE_ID_PREFIX));
|
|
48
48
|
if (toolUseBlocks.length > 0 && toolUseBlocks.length === blocks.length)
|
|
49
49
|
return false;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
// 提取文本:兼容 Anthropic content blocks 和 OpenAI 纯字符串
|
|
51
|
+
const textParts = [];
|
|
52
|
+
for (const b of blocks) {
|
|
53
|
+
if (typeof b === "string") {
|
|
54
|
+
textParts.push(b);
|
|
55
|
+
}
|
|
56
|
+
else if (b && typeof b === "object" && b.type === "text" && typeof b.text === "string") {
|
|
57
|
+
textParts.push(b.text);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const combined = textParts.join("");
|
|
54
61
|
const stripped = combined.replace(RE_ROUTER_RESPONSE, "").trim();
|
|
55
|
-
|
|
62
|
+
// 有 tool_calls 的消息即使 text 为空也保留(OpenAI 格式)
|
|
63
|
+
if (!stripped && !msg.tool_calls)
|
|
56
64
|
return false;
|
|
57
65
|
}
|
|
58
66
|
return true;
|
|
@@ -10,6 +10,9 @@ import { HTTP_NOT_FOUND, HTTP_BAD_GATEWAY } from "../../core/constants.js";
|
|
|
10
10
|
import { SERVICE_KEYS } from "../../core/container.js";
|
|
11
11
|
const CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
|
|
12
12
|
const MODELS_PATH = "/v1/models";
|
|
13
|
+
/** OpenAI 兼容路径(不带 /v1 前缀),供部分客户端使用 */
|
|
14
|
+
const CHAT_COMPLETIONS_COMPAT_PATH = "/chat/completions";
|
|
15
|
+
const MODELS_COMPAT_PATH = "/models";
|
|
13
16
|
const OPENAI_ERROR_META = {
|
|
14
17
|
modelNotFound: { type: "invalid_request_error", code: "model_not_found" },
|
|
15
18
|
modelNotAllowed: { type: "invalid_request_error", code: "model_not_allowed" },
|
|
@@ -27,7 +30,7 @@ function sendError(reply, e) {
|
|
|
27
30
|
const openaiProxyRaw = (app, opts, done) => {
|
|
28
31
|
const { db, container } = opts;
|
|
29
32
|
const orchestrator = createOrchestrator(container.resolve(SERVICE_KEYS.semaphoreManager), container.resolve(SERVICE_KEYS.tracker), container.resolve(SERVICE_KEYS.adaptiveController));
|
|
30
|
-
|
|
33
|
+
const handleChatCompletions = async (request, reply) => {
|
|
31
34
|
if (!orchestrator) {
|
|
32
35
|
const body = request.body;
|
|
33
36
|
insertRequestLog(db, {
|
|
@@ -48,8 +51,11 @@ const openaiProxyRaw = (app, opts, done) => {
|
|
|
48
51
|
}
|
|
49
52
|
},
|
|
50
53
|
});
|
|
51
|
-
}
|
|
52
|
-
|
|
54
|
+
};
|
|
55
|
+
// 规范路径 + 兼容路径(不带 /v1 前缀)
|
|
56
|
+
app.post(CHAT_COMPLETIONS_PATH, handleChatCompletions);
|
|
57
|
+
app.post(CHAT_COMPLETIONS_COMPAT_PATH, handleChatCompletions);
|
|
58
|
+
const handleModels = async (request, reply) => {
|
|
53
59
|
const startTime = Date.now();
|
|
54
60
|
const providers = getActiveProviders(db, "openai");
|
|
55
61
|
if (providers.length === 0) {
|
|
@@ -97,7 +103,10 @@ const openaiProxyRaw = (app, opts, done) => {
|
|
|
97
103
|
body: { error: { message: "Failed to reach backend service", type: "server_error", code: "upstream_error" } },
|
|
98
104
|
});
|
|
99
105
|
}
|
|
100
|
-
}
|
|
106
|
+
};
|
|
107
|
+
// 规范路径 + 兼容路径
|
|
108
|
+
app.get(MODELS_PATH, handleModels);
|
|
109
|
+
app.get(MODELS_COMPAT_PATH, handleModels);
|
|
101
110
|
done();
|
|
102
111
|
};
|
|
103
112
|
export const openaiProxy = fp(openaiProxyRaw, { name: "openai-proxy" });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
|
+
import { parseToolArguments } from "../transform/sanitize.js";
|
|
2
3
|
const HASH_DIGEST_LENGTH = 16;
|
|
3
4
|
/** 从 TransportResult 中提取最终 HTTP status code */
|
|
4
5
|
export function getTransportStatusCode(result) {
|
|
@@ -17,13 +18,7 @@ export function serializeBlocksForStorage(blocks, apiType) {
|
|
|
17
18
|
if (b.type === "thinking")
|
|
18
19
|
return { type: "thinking", thinking: b.content };
|
|
19
20
|
if (b.type === "tool_use") {
|
|
20
|
-
|
|
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 };
|
|
21
|
+
return { type: "tool_use", name: b.name ?? "", input: parseToolArguments(b.content) };
|
|
27
22
|
}
|
|
28
23
|
return { type: "text", text: b.content };
|
|
29
24
|
});
|
|
@@ -37,6 +37,7 @@ function rejectAndReply(reply, params, error, errorMessage, providerId) {
|
|
|
37
37
|
}
|
|
38
38
|
import { getConfig } from "../../config/index.js";
|
|
39
39
|
import { SERVICE_KEYS } from "../../core/container.js";
|
|
40
|
+
import { TransformCoordinator } from "../transform/transform-coordinator.js";
|
|
40
41
|
// ---------- Main entry ----------
|
|
41
42
|
export async function handleProxyRequest(request, reply, apiType, upstreamPath, errors, deps, options) {
|
|
42
43
|
const socketErrorHandler = (err) => request.log.debug({ err }, "client socket error");
|
|
@@ -119,6 +120,8 @@ async function executeFailoverLoop(ctx) {
|
|
|
119
120
|
const config = getConfig();
|
|
120
121
|
const excludeTargets = [];
|
|
121
122
|
let rootLogId = null;
|
|
123
|
+
// TransformCoordinator 无状态,只需创建一次
|
|
124
|
+
const coordinator = new TransformCoordinator();
|
|
122
125
|
while (true) {
|
|
123
126
|
const startTime = Date.now();
|
|
124
127
|
const logId = randomUUID();
|
|
@@ -169,25 +172,46 @@ async function executeFailoverLoop(ctx) {
|
|
|
169
172
|
if (!provider || !provider.is_active) {
|
|
170
173
|
return rejectAndReply(reply, rCtx, errors.providerUnavailable(), `Provider '${resolved.provider_id}' unavailable`, resolved.provider_id);
|
|
171
174
|
}
|
|
172
|
-
|
|
173
|
-
return rejectAndReply(reply, rCtx, errors.providerTypeMismatch(), `API type mismatch: expected '${apiType}'`, resolved.provider_id);
|
|
174
|
-
}
|
|
175
|
-
// routing — 创建新对象而非 in-place mutation
|
|
176
|
-
currentBody = { ...currentBody, model: resolved.backend_model };
|
|
177
|
-
iterationSnapshot.add({ stage: "routing", client_model: effectiveModel, backend_model: resolved.backend_model, provider_id: resolved.provider_id, strategy: resolveResult.targetCount > 1 ? "failover" : "scheduled" });
|
|
178
|
-
// --- 溢出重定向:上下文超出时切换到更大模型 ---
|
|
175
|
+
// --- 溢出重定向:上下文超出时切换到更大模型(必须在 transform 之前,确保使用正确的 api_type) ---
|
|
179
176
|
const overflowResult = applyOverflowRedirect(resolved, deps.db, currentBody);
|
|
180
177
|
if (overflowResult) {
|
|
181
178
|
const overflowProvider = getProviderById(deps.db, overflowResult.provider_id);
|
|
182
|
-
if (overflowProvider && overflowProvider.is_active
|
|
179
|
+
if (overflowProvider && overflowProvider.is_active) {
|
|
183
180
|
resolved = { ...resolved, provider_id: overflowResult.provider_id, backend_model: overflowResult.backend_model };
|
|
184
181
|
provider = overflowProvider;
|
|
185
182
|
currentBody = { ...currentBody, model: overflowResult.backend_model };
|
|
186
|
-
iterationSnapshot.add({ stage: "overflow", triggered: true, redirect_to: overflowResult.backend_model, redirect_provider: overflowResult.provider_id });
|
|
187
183
|
}
|
|
188
184
|
}
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
// 格式转换:apiType 不匹配时转换请求体和路径
|
|
186
|
+
const needsTransform = coordinator.needsTransform(apiType, provider.api_type);
|
|
187
|
+
let effectiveApiType = apiType;
|
|
188
|
+
let effectiveUpstreamPath = upstreamPath;
|
|
189
|
+
if (needsTransform) {
|
|
190
|
+
const transformed = coordinator.transformRequest(currentBody, apiType, provider.api_type, resolved.backend_model);
|
|
191
|
+
// 用转换后的结果替换 currentBody
|
|
192
|
+
currentBody = transformed.body;
|
|
193
|
+
effectiveUpstreamPath = transformed.upstreamPath;
|
|
194
|
+
effectiveApiType = provider.api_type;
|
|
195
|
+
}
|
|
196
|
+
// routing — 创建新对象而非 in-place mutation
|
|
197
|
+
currentBody = { ...currentBody, model: resolved.backend_model };
|
|
198
|
+
iterationSnapshot.add({ stage: "routing", client_model: effectiveModel, backend_model: resolved.backend_model, provider_id: resolved.provider_id, strategy: resolveResult.targetCount > 1 ? "failover" : "scheduled" });
|
|
199
|
+
// overflow redirect 已在 transform 之前完成,此处不再重复
|
|
200
|
+
iterationSnapshot.add({ stage: "overflow", triggered: overflowResult != null });
|
|
201
|
+
// Plugin 调整 body 和 headers(不受 needsTransform 限制,inject_headers 等同格式也需要)
|
|
202
|
+
let injectedHeaders = {};
|
|
203
|
+
const pluginRegistry = deps.container.resolve(SERVICE_KEYS.pluginRegistry);
|
|
204
|
+
if (pluginRegistry) {
|
|
205
|
+
const pluginCtx = {
|
|
206
|
+
body: currentBody,
|
|
207
|
+
headers: {},
|
|
208
|
+
sourceApiType: apiType,
|
|
209
|
+
targetApiType: provider.api_type,
|
|
210
|
+
provider: { id: provider.id, name: provider.name, base_url: provider.base_url, api_type: provider.api_type },
|
|
211
|
+
};
|
|
212
|
+
pluginRegistry.applyBeforeRequest(pluginCtx);
|
|
213
|
+
pluginRegistry.applyAfterRequest(pluginCtx);
|
|
214
|
+
injectedHeaders = pluginCtx.headers;
|
|
191
215
|
}
|
|
192
216
|
// provider patches — 使用返回值
|
|
193
217
|
const { body: patchedBody, meta: patchMeta } = applyProviderPatches(currentBody, provider);
|
|
@@ -202,15 +226,48 @@ async function executeFailoverLoop(ctx) {
|
|
|
202
226
|
const reqBodyStr = JSON.stringify(patchedBody);
|
|
203
227
|
const clientReq = JSON.stringify({ headers: cliHdrs, body: rawBody });
|
|
204
228
|
const upstreamReqBase = JSON.stringify({
|
|
205
|
-
url: buildUpstreamUrl(provider.base_url,
|
|
206
|
-
headers: sanitizeHeadersForLog(buildUpstreamHeaders(cliHdrs, apiKey, Buffer.byteLength(reqBodyStr),
|
|
229
|
+
url: buildUpstreamUrl(provider.base_url, effectiveUpstreamPath),
|
|
230
|
+
headers: sanitizeHeadersForLog(buildUpstreamHeaders(cliHdrs, apiKey, Buffer.byteLength(reqBodyStr), effectiveApiType)),
|
|
207
231
|
body: reqBodyStr,
|
|
208
232
|
});
|
|
233
|
+
const formatTransform = needsTransform ? coordinator.createFormatTransform(apiType, provider.api_type, resolved.backend_model) : undefined;
|
|
234
|
+
if (formatTransform) {
|
|
235
|
+
formatTransform.on("warning", (err) => request.log.warn({ err, logId }, "formatTransform warning"));
|
|
236
|
+
}
|
|
237
|
+
const responseTransform = needsTransform ? (bodyStr) => {
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(bodyStr);
|
|
240
|
+
if (parsed.type === "error" || parsed.error) {
|
|
241
|
+
return coordinator.transformErrorResponse(bodyStr, provider.api_type, apiType);
|
|
242
|
+
}
|
|
243
|
+
let transformed = coordinator.transformResponse(bodyStr, provider.api_type, apiType);
|
|
244
|
+
if (pluginRegistry && !isStream) {
|
|
245
|
+
try {
|
|
246
|
+
const respObj = JSON.parse(transformed);
|
|
247
|
+
const respCtx = {
|
|
248
|
+
response: respObj,
|
|
249
|
+
sourceApiType: provider.api_type,
|
|
250
|
+
targetApiType: apiType,
|
|
251
|
+
provider: { id: provider.id, name: provider.name, base_url: provider.base_url, api_type: provider.api_type },
|
|
252
|
+
};
|
|
253
|
+
pluginRegistry.applyBeforeResponse(respCtx);
|
|
254
|
+
pluginRegistry.applyAfterResponse(respCtx);
|
|
255
|
+
transformed = JSON.stringify(respCtx.response);
|
|
256
|
+
}
|
|
257
|
+
catch { /* response hooks best-effort */ }
|
|
258
|
+
}
|
|
259
|
+
return transformed;
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
request.log.error({ err }, "responseTransform failed");
|
|
263
|
+
return bodyStr;
|
|
264
|
+
}
|
|
265
|
+
} : undefined;
|
|
209
266
|
const transportFn = buildTransportFn({
|
|
210
|
-
provider, apiKey, body: patchedBody, cliHdrs, reply, upstreamPath, apiType,
|
|
267
|
+
provider, apiKey, body: patchedBody, cliHdrs, reply, upstreamPath: effectiveUpstreamPath, apiType: effectiveApiType,
|
|
211
268
|
isStream, startTime, logId, effectiveModel, originalModel,
|
|
212
269
|
streamTimeoutMs: config.STREAM_TIMEOUT_MS, tracker, matcher, request,
|
|
213
|
-
streamLoopEnabled,
|
|
270
|
+
streamLoopEnabled, formatTransform, responseTransform, injectedHeaders,
|
|
214
271
|
});
|
|
215
272
|
const pipelineSnapshot = iterationSnapshot.toJSON();
|
|
216
273
|
try {
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 按序执行所有 DeepSeek 特定补丁。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Patch 在格式转换之后执行,body 已经是 provider 的 api_type 格式。
|
|
5
|
+
* DeepSeek 的 api_type 为 openai,但 Anthropic 端点也受支持,
|
|
6
|
+
* 因此按 apiType 分发不同的 patch 流程。
|
|
7
|
+
*
|
|
8
|
+
* Anthropic 格式执行顺序:
|
|
9
|
+
* 1. patchThinkingParam — 注入 thinking 参数
|
|
10
|
+
* 2. stripCacheControl — 剥离 cache_control
|
|
11
|
+
* 3. patchMissingThinkingBlocks — 补 thinking block
|
|
12
|
+
* 4. patchOrphanToolResults — 清理孤儿 tool_result
|
|
13
|
+
*
|
|
14
|
+
* OpenAI 格式执行顺序(参考 docs/deepseek-patch-investigation.md §5.5):
|
|
15
|
+
* 1. patchNonDeepSeekToolMessages — 将非 DeepSeek 生成的 tool_calls 降级为 text
|
|
16
|
+
* 2. patchOrphanToolResultsOA — 处理孤儿 tool 消息
|
|
5
17
|
*/
|
|
6
|
-
export declare function applyDeepSeekPatches(body: Record<string, unknown
|
|
18
|
+
export declare function applyDeepSeekPatches(body: Record<string, unknown>, apiType: "openai" | "anthropic"): void;
|
|
@@ -1,11 +1,34 @@
|
|
|
1
|
+
import { patchThinkingParam } from "./patch-thinking-param.js";
|
|
2
|
+
import { stripCacheControl } from "./patch-cache-control.js";
|
|
1
3
|
import { patchMissingThinkingBlocks } from "./patch-thinking-blocks.js";
|
|
2
|
-
import {
|
|
4
|
+
import { patchNonDeepSeekToolMessages } from "./patch-non-deepseek-tools.js";
|
|
5
|
+
import { patchOrphanToolResults, patchOrphanToolResultsOA } from "./patch-orphan-tool-results.js";
|
|
3
6
|
/**
|
|
4
7
|
* 按序执行所有 DeepSeek 特定补丁。
|
|
5
|
-
*
|
|
6
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* Patch 在格式转换之后执行,body 已经是 provider 的 api_type 格式。
|
|
10
|
+
* DeepSeek 的 api_type 为 openai,但 Anthropic 端点也受支持,
|
|
11
|
+
* 因此按 apiType 分发不同的 patch 流程。
|
|
12
|
+
*
|
|
13
|
+
* Anthropic 格式执行顺序:
|
|
14
|
+
* 1. patchThinkingParam — 注入 thinking 参数
|
|
15
|
+
* 2. stripCacheControl — 剥离 cache_control
|
|
16
|
+
* 3. patchMissingThinkingBlocks — 补 thinking block
|
|
17
|
+
* 4. patchOrphanToolResults — 清理孤儿 tool_result
|
|
18
|
+
*
|
|
19
|
+
* OpenAI 格式执行顺序(参考 docs/deepseek-patch-investigation.md §5.5):
|
|
20
|
+
* 1. patchNonDeepSeekToolMessages — 将非 DeepSeek 生成的 tool_calls 降级为 text
|
|
21
|
+
* 2. patchOrphanToolResultsOA — 处理孤儿 tool 消息
|
|
7
22
|
*/
|
|
8
|
-
export function applyDeepSeekPatches(body) {
|
|
9
|
-
|
|
10
|
-
|
|
23
|
+
export function applyDeepSeekPatches(body, apiType) {
|
|
24
|
+
if (apiType === "anthropic") {
|
|
25
|
+
patchThinkingParam(body, apiType);
|
|
26
|
+
stripCacheControl(body);
|
|
27
|
+
patchMissingThinkingBlocks(body);
|
|
28
|
+
patchOrphanToolResults(body);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
patchNonDeepSeekToolMessages(body);
|
|
32
|
+
patchOrphanToolResultsOA(body);
|
|
33
|
+
}
|
|
11
34
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek 的 Anthropic 兼容 API 不支持 cache_control。
|
|
3
|
+
* Claude Code 等客户端会在 content block 和 system prompt 上标注
|
|
4
|
+
* cache_control: { type: "ephemeral" },需要剥离以避免上游报错。
|
|
5
|
+
*/
|
|
6
|
+
export function stripCacheControl(body) {
|
|
7
|
+
// 处理顶级 system 字段(Anthropic 协议中 system 可以是 content block 数组)
|
|
8
|
+
if (Array.isArray(body.system)) {
|
|
9
|
+
for (const block of body.system) {
|
|
10
|
+
delete block.cache_control;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// 处理 messages 中的 content block
|
|
14
|
+
if (!body.messages)
|
|
15
|
+
return;
|
|
16
|
+
const messages = body.messages;
|
|
17
|
+
for (const msg of messages) {
|
|
18
|
+
if (Array.isArray(msg.content)) {
|
|
19
|
+
for (const block of msg.content) {
|
|
20
|
+
delete block.cache_control;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// 处理 tools 上的 cache_control
|
|
25
|
+
if (Array.isArray(body.tools)) {
|
|
26
|
+
for (const tool of body.tools) {
|
|
27
|
+
delete tool.cache_control;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|