llm-simple-router 0.9.23 → 0.9.25
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.d.ts +2 -0
- package/dist/admin/providers.js +42 -1
- package/dist/admin/proxy-enhancement.js +4 -1
- package/dist/admin/routes.d.ts +2 -0
- package/dist/admin/routes.js +1 -1
- package/dist/core/container.d.ts +1 -0
- package/dist/core/container.js +1 -0
- package/dist/db/log-cleaner.js +4 -1
- package/dist/db/migrations/041_add_provider_proxy.sql +7 -0
- package/dist/db/migrations/041_create_tool_error_logs.sql +28 -0
- package/dist/db/providers.d.ts +9 -1
- package/dist/db/providers.js +3 -3
- package/dist/db/tool-error-logs.d.ts +6 -0
- package/dist/db/tool-error-logs.js +8 -0
- package/dist/index.js +6 -1
- package/dist/proxy/handler/anthropic.js +1 -1
- package/dist/proxy/handler/openai.js +1 -1
- package/dist/proxy/handler/proxy-handler-utils.d.ts +24 -0
- package/dist/proxy/handler/proxy-handler-utils.js +81 -0
- package/dist/proxy/handler/proxy-handler.d.ts +2 -0
- package/dist/proxy/handler/proxy-handler.js +22 -1
- package/dist/proxy/handler/responses.js +1 -1
- package/dist/proxy/routing/enhancement-config.d.ts +1 -0
- package/dist/proxy/routing/enhancement-config.js +2 -0
- package/dist/proxy/tool-error-logger.d.ts +16 -0
- package/dist/proxy/tool-error-logger.js +39 -0
- package/dist/proxy/transport/http.d.ts +4 -3
- package/dist/proxy/transport/http.js +8 -7
- package/dist/proxy/transport/proxy-agent.d.ts +16 -0
- package/dist/proxy/transport/proxy-agent.js +54 -0
- package/dist/proxy/transport/stream.d.ts +2 -1
- package/dist/proxy/transport/stream.js +2 -2
- package/dist/proxy/transport/transport-fn.d.ts +2 -0
- package/dist/proxy/transport/transport-fn.js +3 -2
- package/frontend-dist/assets/{CardContent-Cn7OKkf4.js → CardContent-D2E8XPMF.js} +1 -1
- package/frontend-dist/assets/{CardTitle-4FM9jzOA.js → CardTitle-Bvn47Yr0.js} +1 -1
- package/frontend-dist/assets/{Checkbox-peXPi_YE.js → Checkbox-CHMJbyg6.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-CgqAq4Jf.js → CollapsibleContent-5Mrc8gGt.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-DNaZoRW8.js → CollapsibleTrigger-DaAYAs8_.js} +1 -1
- package/frontend-dist/assets/{Dashboard-Cm4JcnYb.js → Dashboard-JuNvaAgL.js} +1 -1
- package/frontend-dist/assets/{Input-Br6PVSFy.js → Input-D34hdiws.js} +1 -1
- package/frontend-dist/assets/{Label-GZl32pWd.js → Label-NPWP7UVZ.js} +1 -1
- package/frontend-dist/assets/{Login-DoFRYUFk.js → Login-CiMHu-aw.js} +1 -1
- package/frontend-dist/assets/{Logs-3m2BEWDo.js → Logs-RRwgGUbN.js} +1 -1
- package/frontend-dist/assets/{MappingEntryEditor-Wpkde3VI.js → MappingEntryEditor-B4fiJi8Q.js} +1 -1
- package/frontend-dist/assets/{ModelCard-od6KpofN.js → ModelCard-DSpT9oxm.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-RLgKMlDj.js → ModelMappings-CsgtxPOH.js} +1 -1
- package/frontend-dist/assets/{Monitor-DWQbOIYI.js → Monitor-KQ4-zFJ3.js} +1 -1
- package/frontend-dist/assets/Providers-B8kM2PFx.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-Cczah5af.js +1 -0
- package/frontend-dist/assets/{QuickSetup-Cy7xPzpP.js → QuickSetup-BPa2psLw.js} +1 -1
- package/frontend-dist/assets/{RetryRules-D6zhJ-YT.js → RetryRules-B4kfx7KE.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-V4BYi2Xr.js → RouterKeys-B6gaOE5V.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-DcZ3wJIC.js → RovingFocusItem-ZGq4Eu8v.js} +1 -1
- package/frontend-dist/assets/{Schedules-romgDz2G.js → Schedules-tkI3OZrg.js} +1 -1
- package/frontend-dist/assets/{Settings-C4kcd3m9.js → Settings-DRcVz0VH.js} +1 -1
- package/frontend-dist/assets/{Setup-Df7MMOO9.js → Setup-CPx8uTQg.js} +1 -1
- package/frontend-dist/assets/{Switch-CzzBLx2L.js → Switch-BgKvsuZd.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-UwQzn5Jd.js → TooltipTrigger-C1VLFDy4.js} +1 -1
- package/frontend-dist/assets/{TransformRulesForm-CwHjFf_8.js → TransformRulesForm-CxUVIzWH.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-BbafY6jV.js → UnifiedRequestDialog-D-sQqFxg.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-h1_JJr8A.js → VisuallyHiddenInput-WLtFW_E8.js} +1 -1
- package/frontend-dist/assets/{button-BwrBFTfJ.js → button-CGbcdJgN.js} +2 -2
- package/frontend-dist/assets/{copy-ZvTZUKRc.js → copy-CpYWP1uM.js} +1 -1
- package/frontend-dist/assets/{dialog-CVXqy9Fc.js → dialog-CWAFinoK.js} +1 -1
- package/frontend-dist/assets/{index-C8xJ1Vhe.js → index-DDiesvp7.js} +1 -1
- package/frontend-dist/assets/providers-BsmC9XuH.js +1 -0
- package/frontend-dist/assets/providers-DWTdDUnh.js +1 -0
- package/frontend-dist/assets/{proxyEnhancement-mmsextmb.js → proxyEnhancement-BlhJq5sA.js} +1 -1
- package/frontend-dist/assets/{proxyEnhancement-Rllg4r9y.js → proxyEnhancement-Cx7MC-ly.js} +1 -1
- package/frontend-dist/assets/{trash-2-DSx-MkZy.js → trash-2-CAPUkICH.js} +1 -1
- package/frontend-dist/assets/{useClipboard-CszJHvCV.js → useClipboard-CLRvABjT.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-BZZYkmu1.js → useLogRetention-B7v1HgoB.js} +1 -1
- package/frontend-dist/index.html +2 -2
- package/package.json +5 -3
- package/frontend-dist/assets/Providers-OKgt7MLq.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-BG3lHGzz.js +0 -1
- package/frontend-dist/assets/providers-BS8I-elL.js +0 -1
- package/frontend-dist/assets/providers-Bgp5lP_R.js +0 -1
|
@@ -3,11 +3,13 @@ import Database from "better-sqlite3";
|
|
|
3
3
|
import type { StateRegistry } from "../core/registry.js";
|
|
4
4
|
import type { AdaptiveController } from "@llm-router/core/concurrency";
|
|
5
5
|
import type { RequestTracker } from "@llm-router/core/monitor";
|
|
6
|
+
import type { ProxyAgentFactory } from "../proxy/transport/proxy-agent.js";
|
|
6
7
|
interface ProviderRoutesOptions {
|
|
7
8
|
db: Database.Database;
|
|
8
9
|
stateRegistry?: StateRegistry;
|
|
9
10
|
tracker?: RequestTracker;
|
|
10
11
|
adaptiveController?: AdaptiveController;
|
|
12
|
+
proxyAgentFactory?: ProxyAgentFactory;
|
|
11
13
|
}
|
|
12
14
|
export declare const adminProviderRoutes: FastifyPluginCallback<ProviderRoutesOptions>;
|
|
13
15
|
export {};
|
package/dist/admin/providers.js
CHANGED
|
@@ -96,6 +96,10 @@ const CreateProviderSchema = Type.Object({
|
|
|
96
96
|
queue_timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
97
97
|
max_queue_size: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
98
98
|
adaptive_enabled: Type.Optional(Type.Integer({ minimum: 0, maximum: 1 })),
|
|
99
|
+
proxy_type: Type.Optional(Type.Union([Type.Literal("http"), Type.Literal("socks5")])),
|
|
100
|
+
proxy_url: Type.Optional(Type.String({ minLength: 1 })),
|
|
101
|
+
proxy_username: Type.Optional(Type.String()),
|
|
102
|
+
proxy_password: Type.Optional(Type.String()),
|
|
99
103
|
});
|
|
100
104
|
const UpdateProviderSchema = Type.Object({
|
|
101
105
|
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
@@ -113,9 +117,13 @@ const UpdateProviderSchema = Type.Object({
|
|
|
113
117
|
queue_timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
114
118
|
max_queue_size: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
115
119
|
adaptive_enabled: Type.Optional(Type.Integer({ minimum: 0, maximum: 1 })),
|
|
120
|
+
proxy_type: Type.Optional(Type.Union([Type.Literal("http"), Type.Literal("socks5")])),
|
|
121
|
+
proxy_url: Type.Optional(Type.String({ minLength: 1 })),
|
|
122
|
+
proxy_username: Type.Optional(Type.String()),
|
|
123
|
+
proxy_password: Type.Optional(Type.String()),
|
|
116
124
|
});
|
|
117
125
|
export const adminProviderRoutes = (app, options, done) => {
|
|
118
|
-
const { db, stateRegistry, tracker, adaptiveController } = options;
|
|
126
|
+
const { db, stateRegistry, tracker, adaptiveController, proxyAgentFactory } = options;
|
|
119
127
|
app.get("/admin/api/providers", async (_request, reply) => {
|
|
120
128
|
const encryptionKey = getSetting(db, "encryption_key");
|
|
121
129
|
const providers = getAllProviders(db);
|
|
@@ -134,6 +142,10 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
134
142
|
queue_timeout_ms: s.queue_timeout_ms,
|
|
135
143
|
max_queue_size: s.max_queue_size,
|
|
136
144
|
adaptive_enabled: s.adaptive_enabled,
|
|
145
|
+
proxy_type: s.proxy_type,
|
|
146
|
+
proxy_url: s.proxy_url,
|
|
147
|
+
proxy_username: s.proxy_username ? decrypt(s.proxy_username, encryptionKey) : null,
|
|
148
|
+
proxy_password: s.proxy_password ? decrypt(s.proxy_password, encryptionKey) : null,
|
|
137
149
|
concurrency_status: stateRegistry?.getProviderStatus(s.id) ?? { active: 0, queued: 0 },
|
|
138
150
|
created_at: s.created_at,
|
|
139
151
|
updated_at: s.updated_at,
|
|
@@ -152,6 +164,11 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
152
164
|
const encryptedKey = encrypt(body.api_key, getSetting(db, "encryption_key"));
|
|
153
165
|
const { entries: normalizedModels, overrides: contextOverrides } = extractModelOverrides((body.models ?? []));
|
|
154
166
|
const isAdaptiveEnabled = body.adaptive_enabled ?? 0;
|
|
167
|
+
if (body.proxy_type && !body.proxy_url) {
|
|
168
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "proxy_url is required when proxy_type is set"));
|
|
169
|
+
}
|
|
170
|
+
const encryptedProxyUsername = body.proxy_username ? encrypt(body.proxy_username, getSetting(db, "encryption_key")) : null;
|
|
171
|
+
const encryptedProxyPassword = body.proxy_password ? encrypt(body.proxy_password, getSetting(db, "encryption_key")) : null;
|
|
155
172
|
const id = createProvider(db, {
|
|
156
173
|
name: body.name,
|
|
157
174
|
api_type: body.api_type,
|
|
@@ -165,6 +182,10 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
165
182
|
queue_timeout_ms: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
166
183
|
max_queue_size: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
167
184
|
adaptive_enabled: isAdaptiveEnabled,
|
|
185
|
+
proxy_type: body.proxy_type ?? null,
|
|
186
|
+
proxy_url: body.proxy_type ? body.proxy_url : null,
|
|
187
|
+
proxy_username: encryptedProxyUsername,
|
|
188
|
+
proxy_password: encryptedProxyPassword,
|
|
168
189
|
});
|
|
169
190
|
if (contextOverrides.length > 0) {
|
|
170
191
|
setModelInfoForProvider(db, id, contextOverrides.map(o => ({ model_name: o.name, context_window: o.context_window })));
|
|
@@ -234,7 +255,26 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
234
255
|
fields.api_key = encrypt(body.api_key, getSetting(db, "encryption_key"));
|
|
235
256
|
fields.api_key_preview = body.api_key.length > API_KEY_PREVIEW_MIN_LENGTH ? `${body.api_key.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${body.api_key.slice(-API_KEY_PREVIEW_PREFIX_LEN)}` : "****";
|
|
236
257
|
}
|
|
258
|
+
// Proxy field handling
|
|
259
|
+
if (body.proxy_type !== undefined) {
|
|
260
|
+
fields.proxy_type = body.proxy_type || null;
|
|
261
|
+
if (!body.proxy_type) {
|
|
262
|
+
fields.proxy_url = null;
|
|
263
|
+
fields.proxy_username = null;
|
|
264
|
+
fields.proxy_password = null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (body.proxy_url !== undefined && body.proxy_type) {
|
|
268
|
+
fields.proxy_url = body.proxy_url;
|
|
269
|
+
}
|
|
270
|
+
if (body.proxy_username !== undefined && body.proxy_type) {
|
|
271
|
+
fields.proxy_username = body.proxy_username ? encrypt(body.proxy_username, getSetting(db, "encryption_key")) : null;
|
|
272
|
+
}
|
|
273
|
+
if (body.proxy_password !== undefined && body.proxy_type) {
|
|
274
|
+
fields.proxy_password = body.proxy_password ? encrypt(body.proxy_password, getSetting(db, "encryption_key")) : null;
|
|
275
|
+
}
|
|
237
276
|
updateProvider(db, id, fields);
|
|
277
|
+
proxyAgentFactory?.invalidate(id);
|
|
238
278
|
const updated = getProviderById(db, id);
|
|
239
279
|
let cascade;
|
|
240
280
|
if (existing.is_active === 1 && body.is_active === 0) {
|
|
@@ -327,6 +367,7 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
327
367
|
continue;
|
|
328
368
|
}
|
|
329
369
|
}
|
|
370
|
+
proxyAgentFactory?.invalidate(id);
|
|
330
371
|
deleteProvider(db, id);
|
|
331
372
|
stateRegistry?.removeProvider(id);
|
|
332
373
|
adaptiveController?.remove(id);
|
|
@@ -4,12 +4,13 @@ const UpdateProxyEnhancementSchema = Type.Object({
|
|
|
4
4
|
tool_call_loop_enabled: Type.Boolean(),
|
|
5
5
|
stream_loop_enabled: Type.Boolean(),
|
|
6
6
|
tool_round_limit_enabled: Type.Boolean(),
|
|
7
|
+
tool_error_logging_enabled: Type.Boolean(),
|
|
7
8
|
});
|
|
8
9
|
export const adminProxyEnhancementRoutes = (app, options, done) => {
|
|
9
10
|
const { db } = options;
|
|
10
11
|
app.get("/admin/api/proxy-enhancement", async (_request, reply) => {
|
|
11
12
|
const raw = getSetting(db, "proxy_enhancement");
|
|
12
|
-
const defaults = { tool_call_loop_enabled: false, stream_loop_enabled: false, tool_round_limit_enabled: true };
|
|
13
|
+
const defaults = { tool_call_loop_enabled: false, stream_loop_enabled: false, tool_round_limit_enabled: true, tool_error_logging_enabled: false };
|
|
13
14
|
let config = defaults;
|
|
14
15
|
if (raw) {
|
|
15
16
|
try {
|
|
@@ -18,6 +19,7 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
|
|
|
18
19
|
tool_call_loop_enabled: parsed.tool_call_loop_enabled ?? false,
|
|
19
20
|
stream_loop_enabled: parsed.stream_loop_enabled ?? false,
|
|
20
21
|
tool_round_limit_enabled: parsed.tool_round_limit_enabled ?? true,
|
|
22
|
+
tool_error_logging_enabled: parsed.tool_error_logging_enabled ?? false,
|
|
21
23
|
};
|
|
22
24
|
}
|
|
23
25
|
catch { /* eslint-disable-line taste/no-silent-catch -- invalid JSON, return defaults */ }
|
|
@@ -30,6 +32,7 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
|
|
|
30
32
|
tool_call_loop_enabled: body.tool_call_loop_enabled,
|
|
31
33
|
stream_loop_enabled: body.stream_loop_enabled,
|
|
32
34
|
tool_round_limit_enabled: body.tool_round_limit_enabled,
|
|
35
|
+
tool_error_logging_enabled: body.tool_error_logging_enabled,
|
|
33
36
|
};
|
|
34
37
|
setSetting(db, "proxy_enhancement", JSON.stringify(config));
|
|
35
38
|
return reply.send({ success: true });
|
package/dist/admin/routes.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import Database from "better-sqlite3";
|
|
|
3
3
|
import type { StateRegistry } from "../core/registry.js";
|
|
4
4
|
import type { RequestTracker } from "@llm-router/core/monitor";
|
|
5
5
|
import type { AdaptiveController } from "@llm-router/core/concurrency";
|
|
6
|
+
import type { ProxyAgentFactory } from "../proxy/transport/proxy-agent.js";
|
|
6
7
|
interface AdminRoutesOptions {
|
|
7
8
|
db: Database.Database;
|
|
8
9
|
stateRegistry: StateRegistry;
|
|
@@ -12,6 +13,7 @@ interface AdminRoutesOptions {
|
|
|
12
13
|
logsDir?: string;
|
|
13
14
|
pluginRegistry?: import("../proxy/transform/plugin-registry.js").PluginRegistry;
|
|
14
15
|
closeFn?: () => Promise<void>;
|
|
16
|
+
proxyAgentFactory?: ProxyAgentFactory;
|
|
15
17
|
}
|
|
16
18
|
export declare const adminRoutes: FastifyPluginCallback<AdminRoutesOptions>;
|
|
17
19
|
export {};
|
package/dist/admin/routes.js
CHANGED
|
@@ -23,7 +23,7 @@ export const adminRoutes = (app, options, done) => {
|
|
|
23
23
|
app.register(adminSetupRoutes, { db: options.db });
|
|
24
24
|
app.register(adminAuthPlugin, { db: options.db });
|
|
25
25
|
app.register(adminLoginRoutes, { db: options.db });
|
|
26
|
-
app.register(adminProviderRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController });
|
|
26
|
+
app.register(adminProviderRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController, proxyAgentFactory: options.proxyAgentFactory });
|
|
27
27
|
app.register(adminMappingRoutes, { db: options.db });
|
|
28
28
|
app.register(adminGroupRoutes, { db: options.db });
|
|
29
29
|
app.register(adminScheduleRoutes, { db: options.db });
|
package/dist/core/container.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare const SERVICE_KEYS: {
|
|
|
9
9
|
readonly adaptiveController: "adaptiveController";
|
|
10
10
|
readonly pluginRegistry: "pluginRegistry";
|
|
11
11
|
readonly logFileWriter: "logFileWriter";
|
|
12
|
+
readonly proxyAgentFactory: "proxyAgentFactory";
|
|
12
13
|
};
|
|
13
14
|
export type ServiceKey = (typeof SERVICE_KEYS)[keyof typeof SERVICE_KEYS];
|
|
14
15
|
/**
|
package/dist/core/container.js
CHANGED
package/dist/db/log-cleaner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { deleteLogsBefore } from "./logs.js";
|
|
2
2
|
import { getLogRetentionDays } from "./settings.js";
|
|
3
|
+
import { deleteToolErrorLogsBefore } from "./tool-error-logs.js";
|
|
3
4
|
const MS_PER_DAY = 86_400_000;
|
|
4
5
|
const CLEANUP_INTERVAL_MS = 3_600_000; // 1 小时
|
|
5
6
|
/** 运行一次清理,返回删除条数 */
|
|
@@ -8,7 +9,9 @@ export function runLogCleanup(db) {
|
|
|
8
9
|
if (days <= 0)
|
|
9
10
|
return 0;
|
|
10
11
|
const cutoff = new Date(Date.now() - days * MS_PER_DAY).toISOString();
|
|
11
|
-
|
|
12
|
+
const logDeleted = deleteLogsBefore(db, cutoff);
|
|
13
|
+
const toolErrorDeleted = deleteToolErrorLogsBefore(db, cutoff);
|
|
14
|
+
return logDeleted + toolErrorDeleted;
|
|
12
15
|
}
|
|
13
16
|
/** 启动定时清理,返回 handle 用于停止 */
|
|
14
17
|
export function scheduleLogCleanup(db, log) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- 041_add_provider_proxy.sql
|
|
2
|
+
-- Add per-provider proxy support (SOCKS5 / HTTP CONNECT)
|
|
3
|
+
|
|
4
|
+
ALTER TABLE providers ADD COLUMN proxy_type TEXT DEFAULT NULL;
|
|
5
|
+
ALTER TABLE providers ADD COLUMN proxy_url TEXT DEFAULT NULL;
|
|
6
|
+
ALTER TABLE providers ADD COLUMN proxy_username TEXT DEFAULT NULL;
|
|
7
|
+
ALTER TABLE providers ADD COLUMN proxy_password TEXT DEFAULT NULL;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS tool_error_logs (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
request_log_id TEXT REFERENCES request_logs(id) ON DELETE SET NULL,
|
|
4
|
+
provider_id TEXT NOT NULL,
|
|
5
|
+
backend_model TEXT NOT NULL,
|
|
6
|
+
client_agent_type TEXT NOT NULL DEFAULT 'unknown'
|
|
7
|
+
CHECK(client_agent_type IN ('claude-code', 'pi', 'unknown')),
|
|
8
|
+
tool_name TEXT NOT NULL,
|
|
9
|
+
tool_use_id TEXT,
|
|
10
|
+
tool_input TEXT,
|
|
11
|
+
error_content TEXT,
|
|
12
|
+
router_key_id TEXT,
|
|
13
|
+
session_id TEXT,
|
|
14
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_tool_error_logs_time
|
|
18
|
+
ON tool_error_logs(created_at);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_tool_error_logs_provider
|
|
20
|
+
ON tool_error_logs(provider_id, created_at);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_tool_error_logs_model
|
|
22
|
+
ON tool_error_logs(backend_model, created_at);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_tool_error_logs_tool
|
|
24
|
+
ON tool_error_logs(tool_name);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_tool_error_logs_agent
|
|
26
|
+
ON tool_error_logs(client_agent_type);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_tool_error_logs_session
|
|
28
|
+
ON tool_error_logs(session_id);
|
package/dist/db/providers.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface Provider {
|
|
|
13
13
|
queue_timeout_ms: number;
|
|
14
14
|
max_queue_size: number;
|
|
15
15
|
adaptive_enabled: number;
|
|
16
|
+
proxy_type: string | null;
|
|
17
|
+
proxy_url: string | null;
|
|
18
|
+
proxy_username: string | null;
|
|
19
|
+
proxy_password: string | null;
|
|
16
20
|
created_at: string;
|
|
17
21
|
updated_at: string;
|
|
18
22
|
}
|
|
@@ -41,8 +45,12 @@ export declare function createProvider(db: Database.Database, provider: {
|
|
|
41
45
|
queue_timeout_ms?: number;
|
|
42
46
|
max_queue_size?: number;
|
|
43
47
|
adaptive_enabled?: number;
|
|
48
|
+
proxy_type?: string | null;
|
|
49
|
+
proxy_url?: string | null;
|
|
50
|
+
proxy_username?: string | null;
|
|
51
|
+
proxy_password?: string | null;
|
|
44
52
|
}): string;
|
|
45
|
-
export declare function updateProvider(db: Database.Database, id: string, fields: Partial<Pick<Provider, "name" | "api_type" | "base_url" | "upstream_path" | "api_key" | "api_key_preview" | "models" | "is_active" | "max_concurrency" | "queue_timeout_ms" | "max_queue_size" | "adaptive_enabled">>): void;
|
|
53
|
+
export declare function updateProvider(db: Database.Database, id: string, fields: Partial<Pick<Provider, "name" | "api_type" | "base_url" | "upstream_path" | "api_key" | "api_key_preview" | "models" | "is_active" | "max_concurrency" | "queue_timeout_ms" | "max_queue_size" | "adaptive_enabled" | "proxy_type" | "proxy_url" | "proxy_username" | "proxy_password">>): void;
|
|
46
54
|
export declare function deleteProvider(db: Database.Database, id: string): void;
|
|
47
55
|
export declare function getActiveProviderByName(db: Database.Database, name: string): {
|
|
48
56
|
id: string;
|
package/dist/db/providers.js
CHANGED
|
@@ -37,7 +37,7 @@ export const PROVIDER_CONCURRENCY_DEFAULTS = {
|
|
|
37
37
|
max_queue_size: 100,
|
|
38
38
|
};
|
|
39
39
|
const PROVIDER_FIELDS = new Set([
|
|
40
|
-
"name", "api_type", "base_url", "upstream_path", "api_key", "api_key_preview", "models", "is_active", "max_concurrency", "queue_timeout_ms", "max_queue_size", "adaptive_enabled",
|
|
40
|
+
"name", "api_type", "base_url", "upstream_path", "api_key", "api_key_preview", "models", "is_active", "max_concurrency", "queue_timeout_ms", "max_queue_size", "adaptive_enabled", "proxy_type", "proxy_url", "proxy_username", "proxy_password",
|
|
41
41
|
]);
|
|
42
42
|
export function getActiveProviders(db, apiType) {
|
|
43
43
|
return db
|
|
@@ -53,8 +53,8 @@ export function getProviderById(db, id) {
|
|
|
53
53
|
export function createProvider(db, provider) {
|
|
54
54
|
const id = randomUUID();
|
|
55
55
|
const now = new Date().toISOString();
|
|
56
|
-
db.prepare(`INSERT INTO providers (id, name, api_type, base_url, upstream_path, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled, created_at, updated_at)
|
|
57
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, provider.name, provider.api_type, provider.base_url, provider.upstream_path ?? null, provider.api_key, provider.api_key_preview ?? null, provider.models ?? "[]", provider.is_active ?? 1, provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency, provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms, provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size, provider.adaptive_enabled ?? 0, now, now);
|
|
56
|
+
db.prepare(`INSERT INTO providers (id, name, api_type, base_url, upstream_path, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled, proxy_type, proxy_url, proxy_username, proxy_password, created_at, updated_at)
|
|
57
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, provider.name, provider.api_type, provider.base_url, provider.upstream_path ?? null, provider.api_key, provider.api_key_preview ?? null, provider.models ?? "[]", provider.is_active ?? 1, provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency, provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms, provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size, provider.adaptive_enabled ?? 0, provider.proxy_type ?? null, provider.proxy_url ?? null, provider.proxy_username ?? null, provider.proxy_password ?? null, now, now);
|
|
58
58
|
return id;
|
|
59
59
|
}
|
|
60
60
|
export function updateProvider(db, id, fields) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
/**
|
|
3
|
+
* 删除 created_at 早于 beforeDate 的 tool_error_logs 记录。
|
|
4
|
+
* 外键 ON DELETE SET NULL 确保 request_logs 被删后 tool_error_logs 仍保留。
|
|
5
|
+
*/
|
|
6
|
+
export declare function deleteToolErrorLogsBefore(db: Database.Database, beforeDate: string): number;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 删除 created_at 早于 beforeDate 的 tool_error_logs 记录。
|
|
3
|
+
* 外键 ON DELETE SET NULL 确保 request_logs 被删后 tool_error_logs 仍保留。
|
|
4
|
+
*/
|
|
5
|
+
export function deleteToolErrorLogsBefore(db, beforeDate) {
|
|
6
|
+
const changes = db.prepare("DELETE FROM tool_error_logs WHERE created_at < ?").run(beforeDate).changes;
|
|
7
|
+
return changes;
|
|
8
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import { startUpgradeChecker, stopUpgradeChecker } from "./admin/upgrade.js";
|
|
|
31
31
|
import fastifyStatic from "@fastify/static";
|
|
32
32
|
import { ServiceContainer, SERVICE_KEYS } from "./core/container.js";
|
|
33
33
|
import { LogFileWriter } from "./storage/log-file-writer.js";
|
|
34
|
+
import { ProxyAgentFactory } from "./proxy/transport/proxy-agent.js";
|
|
34
35
|
import { scheduleLogFileMaintenance } from "./storage/log-file-compressor.js";
|
|
35
36
|
import { getDetailLogEnabled, getLogFileRetentionDays } from "./db/settings.js";
|
|
36
37
|
import { dirname, join } from "node:path";
|
|
@@ -215,12 +216,15 @@ export async function buildApp(options) {
|
|
|
215
216
|
const pluginsDir = path.resolve(__dirname, "../plugins/transform");
|
|
216
217
|
pluginRegistry.scanPluginsDir(pluginsDir);
|
|
217
218
|
container.register(SERVICE_KEYS.pluginRegistry, () => pluginRegistry);
|
|
219
|
+
// 注册 ProxyAgentFactory
|
|
220
|
+
container.register(SERVICE_KEYS.proxyAgentFactory, () => new ProxyAgentFactory());
|
|
218
221
|
// 从容器解析所有服务
|
|
219
222
|
const matcher = container.resolve(SERVICE_KEYS.matcher);
|
|
220
223
|
const semaphoreManager = container.resolve(SERVICE_KEYS.semaphoreManager);
|
|
221
224
|
const tracker = container.resolve(SERVICE_KEYS.tracker);
|
|
222
225
|
const usageWindowTracker = container.resolve(SERVICE_KEYS.usageWindowTracker);
|
|
223
226
|
const adaptiveController = container.resolve(SERVICE_KEYS.adaptiveController);
|
|
227
|
+
const proxyAgentFactory = container.resolve(SERVICE_KEYS.proxyAgentFactory);
|
|
224
228
|
// Wire adaptive controller to tracker
|
|
225
229
|
tracker.setAdaptiveStatusProvider(adaptiveController);
|
|
226
230
|
// 从 DB 读取已有 provider 的并发配置,初始化信号量/adaptive/tracker(共享逻辑)
|
|
@@ -247,7 +251,7 @@ export async function buildApp(options) {
|
|
|
247
251
|
// Late-bound close ref — close 函数在 adminRoutes 注册之后才定义,
|
|
248
252
|
// 但 restart API 需要在运行时调用它
|
|
249
253
|
const closeRef = { fn: async () => { } };
|
|
250
|
-
app.register(adminRoutes, { db, stateRegistry, tracker, adaptiveController, logFileWriter, logsDir, closeFn: () => closeRef.fn(), pluginRegistry });
|
|
254
|
+
app.register(adminRoutes, { db, stateRegistry, tracker, adaptiveController, logFileWriter, logsDir, closeFn: () => closeRef.fn(), pluginRegistry, proxyAgentFactory });
|
|
251
255
|
// 前端静态文件服务(生产环境)
|
|
252
256
|
const frontendDist = path.resolve(process.env.FRONTEND_DIST || path.join(__dirname, "../frontend-dist"));
|
|
253
257
|
if (existsSync(frontendDist)) {
|
|
@@ -287,6 +291,7 @@ export async function buildApp(options) {
|
|
|
287
291
|
// 关闭所有 SSE 长连接,防止 app.close() 因 hijack 的连接无限等待
|
|
288
292
|
tracker.closeAllClients();
|
|
289
293
|
semaphoreManager.removeAll();
|
|
294
|
+
proxyAgentFactory.invalidateAll();
|
|
290
295
|
const sessionTracker = container.resolve(SERVICE_KEYS.sessionTracker);
|
|
291
296
|
sessionTracker.stop();
|
|
292
297
|
await app.close();
|
|
@@ -35,7 +35,7 @@ const anthropicProxyRaw = (app, opts, done) => {
|
|
|
35
35
|
const e = anthropicErrors.providerUnavailable();
|
|
36
36
|
return reply.code(e.statusCode).send(e.body);
|
|
37
37
|
}
|
|
38
|
-
const deps = { db, orchestrator, container };
|
|
38
|
+
const deps = { db, orchestrator, container, proxyAgentFactory: container.resolve(SERVICE_KEYS.proxyAgentFactory) };
|
|
39
39
|
return handleProxyRequest(request, reply, "anthropic", MESSAGES_PATH, anthropicErrors, deps);
|
|
40
40
|
});
|
|
41
41
|
done();
|
|
@@ -41,7 +41,7 @@ const openaiProxyRaw = (app, opts, done) => {
|
|
|
41
41
|
});
|
|
42
42
|
return sendError(reply, openaiErrors.providerUnavailable());
|
|
43
43
|
}
|
|
44
|
-
const deps = { db, orchestrator, container };
|
|
44
|
+
const deps = { db, orchestrator, container, proxyAgentFactory: container.resolve(SERVICE_KEYS.proxyAgentFactory) };
|
|
45
45
|
return handleProxyRequest(request, reply, "openai", CHAT_COMPLETIONS_PATH, openaiErrors, deps, {
|
|
46
46
|
beforeSendProxy: (body, isStream) => {
|
|
47
47
|
if (isStream && !body.stream_options) {
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import type { ContentBlock } from "@llm-router/core/monitor";
|
|
2
2
|
import type { ToolCallRecord } from "@llm-router/core/loop-prevention";
|
|
3
3
|
import type { TransportResult } from "../types.js";
|
|
4
|
+
import type { RawHeaders } from "../types.js";
|
|
5
|
+
export type ClientAgentType = "claude-code" | "pi" | "unknown";
|
|
6
|
+
export interface FailedToolResult {
|
|
7
|
+
toolName: string;
|
|
8
|
+
toolUseId: string | undefined;
|
|
9
|
+
toolInput: string | undefined;
|
|
10
|
+
errorContent: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 根据请求头识别客户端类型。
|
|
14
|
+
* - Claude Code 独有 x-claude-code-session-id 头
|
|
15
|
+
* - pi 的 User-Agent 包含 "pi-coding-agent"
|
|
16
|
+
*/
|
|
17
|
+
export declare function detectClientAgentType(headers: RawHeaders): ClientAgentType;
|
|
18
|
+
/**
|
|
19
|
+
* 从请求体 messages 中提取本条请求新产生的失败 tool_result 块。
|
|
20
|
+
*
|
|
21
|
+
* 只扫描最后一条 role = "user" 且有 tool_result 的消息,
|
|
22
|
+
* 避免重复记录前轮请求已记录的 tool 失败。
|
|
23
|
+
*
|
|
24
|
+
* 通过向前扫描 assistant 消息中的 tool_use 块
|
|
25
|
+
* 关联对应的 tool_name 和 tool_input。
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractFailedToolResults(body: Record<string, unknown>): FailedToolResult[];
|
|
4
28
|
/** 从 TransportResult 中提取最终 HTTP status code */
|
|
5
29
|
export declare function getTransportStatusCode(result: TransportResult): number | null;
|
|
6
30
|
/** 将 tracker blocks 序列化为前端 tryDirectParse 可解析的 JSON */
|
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
2
|
import { parseToolArguments } from "../transform/sanitize.js";
|
|
3
3
|
const HASH_DIGEST_LENGTH = 16;
|
|
4
|
+
/**
|
|
5
|
+
* 根据请求头识别客户端类型。
|
|
6
|
+
* - Claude Code 独有 x-claude-code-session-id 头
|
|
7
|
+
* - pi 的 User-Agent 包含 "pi-coding-agent"
|
|
8
|
+
*/
|
|
9
|
+
export function detectClientAgentType(headers) {
|
|
10
|
+
if (headers["x-claude-code-session-id"])
|
|
11
|
+
return "claude-code";
|
|
12
|
+
const ua = String(headers["user-agent"] ?? "").toLowerCase();
|
|
13
|
+
if (ua.includes("pi-coding-agent"))
|
|
14
|
+
return "pi";
|
|
15
|
+
return "unknown";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 从请求体 messages 中提取本条请求新产生的失败 tool_result 块。
|
|
19
|
+
*
|
|
20
|
+
* 只扫描最后一条 role = "user" 且有 tool_result 的消息,
|
|
21
|
+
* 避免重复记录前轮请求已记录的 tool 失败。
|
|
22
|
+
*
|
|
23
|
+
* 通过向前扫描 assistant 消息中的 tool_use 块
|
|
24
|
+
* 关联对应的 tool_name 和 tool_input。
|
|
25
|
+
*/
|
|
26
|
+
export function extractFailedToolResults(body) {
|
|
27
|
+
const messages = body.messages;
|
|
28
|
+
if (!messages || messages.length === 0)
|
|
29
|
+
return [];
|
|
30
|
+
// 第一步:向后往前找最后一个包含 tool_result 的 user 消息
|
|
31
|
+
let lastUserIndex = -1;
|
|
32
|
+
const resultBlocks = [];
|
|
33
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
34
|
+
const msg = messages[i];
|
|
35
|
+
if (msg.role !== "user")
|
|
36
|
+
continue;
|
|
37
|
+
const content = msg.content;
|
|
38
|
+
if (!Array.isArray(content))
|
|
39
|
+
continue;
|
|
40
|
+
for (const block of content) {
|
|
41
|
+
if (block.type === "tool_result") {
|
|
42
|
+
resultBlocks.push(block);
|
|
43
|
+
lastUserIndex = i;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (resultBlocks.length > 0)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
if (lastUserIndex < 0)
|
|
50
|
+
return [];
|
|
51
|
+
// 第二步:在整个 messages 中建立 tool_use_id → { name, input } 映射
|
|
52
|
+
const toolUseMap = new Map();
|
|
53
|
+
for (let i = 0; i < lastUserIndex; i++) {
|
|
54
|
+
const msg = messages[i];
|
|
55
|
+
if (msg.role !== "assistant")
|
|
56
|
+
continue;
|
|
57
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
58
|
+
for (const block of content) {
|
|
59
|
+
if (block.type === "tool_use" && block.id) {
|
|
60
|
+
const inputText = block.input ? JSON.stringify(block.input) : "";
|
|
61
|
+
toolUseMap.set(block.id, { name: block.name ?? "unknown", input: inputText });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 第三步:提取 is_error === true 的 tool_result
|
|
66
|
+
const failures = [];
|
|
67
|
+
for (const block of resultBlocks) {
|
|
68
|
+
if (block.is_error !== true)
|
|
69
|
+
continue;
|
|
70
|
+
const toolUse = block.tool_use_id && typeof block.tool_use_id === "string"
|
|
71
|
+
? toolUseMap.get(block.tool_use_id)
|
|
72
|
+
: undefined;
|
|
73
|
+
const errorContent = typeof block.content === "string"
|
|
74
|
+
? block.content
|
|
75
|
+
: JSON.stringify(block.content ?? "");
|
|
76
|
+
failures.push({
|
|
77
|
+
toolName: toolUse?.name ?? "unknown",
|
|
78
|
+
toolUseId: block.tool_use_id,
|
|
79
|
+
toolInput: toolUse?.input,
|
|
80
|
+
errorContent,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return failures;
|
|
84
|
+
}
|
|
4
85
|
/** 从 TransportResult 中提取最终 HTTP status code */
|
|
5
86
|
export function getTransportStatusCode(result) {
|
|
6
87
|
if (result.kind === "success" || result.kind === "error" || result.kind === "stream_error")
|
|
@@ -2,10 +2,12 @@ import type { FastifyReply, FastifyRequest } from "fastify";
|
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import type { ProxyOrchestrator } from "../orchestration/orchestrator.js";
|
|
4
4
|
import type { ProxyErrorFormatter } from "../proxy-core.js";
|
|
5
|
+
import type { ProxyAgentFactory } from "../transport/proxy-agent.js";
|
|
5
6
|
export interface RouteHandlerDeps {
|
|
6
7
|
db: Database.Database;
|
|
7
8
|
orchestrator: ProxyOrchestrator;
|
|
8
9
|
container: ServiceContainer;
|
|
10
|
+
proxyAgentFactory?: ProxyAgentFactory;
|
|
9
11
|
}
|
|
10
12
|
import type { ServiceContainer } from "../../core/container.js";
|
|
11
13
|
export declare function handleProxyRequest(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "openai-responses" | "anthropic", upstreamPath: string, errors: ProxyErrorFormatter, deps: RouteHandlerDeps, options?: {
|
|
@@ -18,7 +18,8 @@ import { applyProviderPatches } from "../patch/index.js";
|
|
|
18
18
|
import { PipelineSnapshot } from "../pipeline-snapshot.js";
|
|
19
19
|
import { applyToolRoundLimit } from "../patch/tool-round-limiter.js";
|
|
20
20
|
import { loadEnhancementConfig } from "../routing/enhancement-config.js";
|
|
21
|
-
import { getTransportStatusCode, serializeBlocksForStorage, extractLastToolUse } from "./proxy-handler-utils.js";
|
|
21
|
+
import { getTransportStatusCode, serializeBlocksForStorage, extractLastToolUse, extractFailedToolResults, detectClientAgentType } from "./proxy-handler-utils.js";
|
|
22
|
+
import { logToolErrors } from "../tool-error-logger.js";
|
|
22
23
|
const HTTP_ERROR_THRESHOLD = 400;
|
|
23
24
|
const MAX_LOG_FIELD_LENGTH = 80;
|
|
24
25
|
const UPSTREAM_ERROR_STATUS = 502;
|
|
@@ -125,8 +126,10 @@ async function executeFailoverLoop(ctx) {
|
|
|
125
126
|
const config = getConfig();
|
|
126
127
|
const excludeTargets = [];
|
|
127
128
|
let rootLogId = null;
|
|
129
|
+
let toolErrorsLogged = false;
|
|
128
130
|
// TransformCoordinator 无状态,只需创建一次
|
|
129
131
|
const coordinator = new TransformCoordinator();
|
|
132
|
+
const enhancementConfig = loadEnhancementConfig(deps.db);
|
|
130
133
|
while (true) {
|
|
131
134
|
const startTime = Date.now();
|
|
132
135
|
const logId = randomUUID();
|
|
@@ -177,6 +180,23 @@ async function executeFailoverLoop(ctx) {
|
|
|
177
180
|
if (!provider || !provider.is_active) {
|
|
178
181
|
return rejectAndReply(reply, rCtx, errors.providerUnavailable(), `Provider '${resolved.provider_id}' unavailable`, resolved.provider_id);
|
|
179
182
|
}
|
|
183
|
+
// 工具错误日志记录 — 首次迭代时执行,记录本轮请求中的 is_error tool_result
|
|
184
|
+
if (enhancementConfig.tool_error_logging_enabled && !toolErrorsLogged) {
|
|
185
|
+
toolErrorsLogged = true;
|
|
186
|
+
const failures = extractFailedToolResults(pipelineBody);
|
|
187
|
+
if (failures.length > 0) {
|
|
188
|
+
request.log.info({ failures: failures.length, sessionId }, "Tool error results detected");
|
|
189
|
+
logToolErrors(failures, {
|
|
190
|
+
db: deps.db,
|
|
191
|
+
providerId: resolved.provider_id,
|
|
192
|
+
backendModel: resolved.backend_model ?? effectiveModel,
|
|
193
|
+
clientAgentType: detectClientAgentType(cliHdrs),
|
|
194
|
+
requestLogId: logId,
|
|
195
|
+
routerKeyId,
|
|
196
|
+
sessionId,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
180
200
|
// --- 溢出重定向:上下文超出时切换到更大模型(必须在 transform 之前,确保使用正确的 api_type) ---
|
|
181
201
|
const overflowResult = applyOverflowRedirect(resolved, deps.db, currentBody);
|
|
182
202
|
if (overflowResult) {
|
|
@@ -283,6 +303,7 @@ async function executeFailoverLoop(ctx) {
|
|
|
283
303
|
streamTimeoutMs: getModelStreamTimeout(provider, resolved.backend_model), tracker, matcher, request,
|
|
284
304
|
streamLoopEnabled, formatTransform, responseTransform, injectedHeaders,
|
|
285
305
|
timeoutContext: { modelId: resolved.backend_model, providerId: provider.id },
|
|
306
|
+
proxyAgentFactory: deps.proxyAgentFactory,
|
|
286
307
|
});
|
|
287
308
|
const pipelineSnapshot = iterationSnapshot.toJSON();
|
|
288
309
|
try {
|
|
@@ -38,7 +38,7 @@ const responsesProxyRaw = (app, opts, done) => {
|
|
|
38
38
|
});
|
|
39
39
|
return sendError(reply, responsesErrors.providerUnavailable());
|
|
40
40
|
}
|
|
41
|
-
const deps = { db, orchestrator, container };
|
|
41
|
+
const deps = { db, orchestrator, container, proxyAgentFactory: container.resolve(SERVICE_KEYS.proxyAgentFactory) };
|
|
42
42
|
return handleProxyRequest(request, reply, "openai-responses", RESPONSES_PATH, responsesErrors, deps);
|
|
43
43
|
};
|
|
44
44
|
app.post(RESPONSES_PATH, handleResponses);
|
|
@@ -3,6 +3,7 @@ export interface EnhancementConfig {
|
|
|
3
3
|
tool_call_loop_enabled: boolean;
|
|
4
4
|
stream_loop_enabled: boolean;
|
|
5
5
|
tool_round_limit_enabled: boolean;
|
|
6
|
+
tool_error_logging_enabled: boolean;
|
|
6
7
|
}
|
|
7
8
|
/** 集中加载 proxy_enhancement 配置,避免多处重复 getSetting + JSON.parse */
|
|
8
9
|
export declare function loadEnhancementConfig(db: Database.Database): EnhancementConfig;
|
|
@@ -3,6 +3,7 @@ const DEFAULT_CONFIG = {
|
|
|
3
3
|
tool_call_loop_enabled: false,
|
|
4
4
|
stream_loop_enabled: false,
|
|
5
5
|
tool_round_limit_enabled: true,
|
|
6
|
+
tool_error_logging_enabled: false,
|
|
6
7
|
};
|
|
7
8
|
/** 集中加载 proxy_enhancement 配置,避免多处重复 getSetting + JSON.parse */
|
|
8
9
|
export function loadEnhancementConfig(db) {
|
|
@@ -15,6 +16,7 @@ export function loadEnhancementConfig(db) {
|
|
|
15
16
|
tool_call_loop_enabled: parsed.tool_call_loop_enabled ?? false,
|
|
16
17
|
stream_loop_enabled: parsed.stream_loop_enabled ?? false,
|
|
17
18
|
tool_round_limit_enabled: parsed.tool_round_limit_enabled ?? true,
|
|
19
|
+
tool_error_logging_enabled: parsed.tool_error_logging_enabled ?? false,
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
catch {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import type { FailedToolResult, ClientAgentType } from "./handler/proxy-handler-utils.js";
|
|
3
|
+
export interface ToolErrorLogContext {
|
|
4
|
+
db: Database.Database;
|
|
5
|
+
providerId: string;
|
|
6
|
+
backendModel: string;
|
|
7
|
+
clientAgentType: ClientAgentType;
|
|
8
|
+
requestLogId: string;
|
|
9
|
+
routerKeyId: string | null;
|
|
10
|
+
sessionId: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 将失败的 tool_result 批量写入 tool_error_logs 表。
|
|
14
|
+
* 每条失败记录独立一行。
|
|
15
|
+
*/
|
|
16
|
+
export declare function logToolErrors(failures: FailedToolResult[], ctx: ToolErrorLogContext): void;
|