llm-simple-router 0.10.3 → 0.10.5
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/metrics.js +10 -3
- package/dist/admin/monitor.d.ts +1 -1
- package/dist/admin/providers.d.ts +2 -2
- package/dist/admin/quick-setup.d.ts +2 -2
- package/dist/admin/routes.d.ts +2 -2
- package/dist/admin/settings.js +36 -1
- package/dist/core/concurrency/adaptive-controller.d.ts +24 -0
- package/dist/core/concurrency/adaptive-controller.js +151 -0
- package/dist/core/concurrency/index.d.ts +4 -0
- package/dist/core/concurrency/index.js +2 -0
- package/dist/core/concurrency/semaphore.d.ts +29 -0
- package/dist/core/concurrency/semaphore.js +161 -0
- package/dist/core/concurrency/types.d.ts +34 -0
- package/dist/core/concurrency/types.js +1 -0
- package/dist/core/errors.d.ts +15 -1
- package/dist/core/errors.js +24 -2
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +12 -0
- package/dist/core/loop-prevention/detector.d.ts +10 -0
- package/dist/core/loop-prevention/detector.js +1 -0
- package/dist/core/loop-prevention/index.d.ts +7 -0
- package/dist/core/loop-prevention/index.js +5 -0
- package/dist/core/loop-prevention/ngram-detector.d.ts +15 -0
- package/dist/core/loop-prevention/ngram-detector.js +65 -0
- package/dist/core/loop-prevention/session-tracker.d.ts +14 -0
- package/dist/core/loop-prevention/session-tracker.js +67 -0
- package/dist/core/loop-prevention/stream-loop-guard.d.ts +12 -0
- package/dist/core/loop-prevention/stream-loop-guard.js +28 -0
- package/dist/core/loop-prevention/tool-loop-guard.d.ts +13 -0
- package/dist/core/loop-prevention/tool-loop-guard.js +70 -0
- package/dist/core/loop-prevention/types.d.ts +38 -0
- package/dist/core/loop-prevention/types.js +18 -0
- package/dist/core/monitor/index.d.ts +7 -0
- package/dist/core/monitor/index.js +5 -0
- package/dist/core/monitor/request-tracker.d.ts +87 -0
- package/dist/core/monitor/request-tracker.js +379 -0
- package/dist/core/monitor/runtime-collector.d.ts +11 -0
- package/dist/core/monitor/runtime-collector.js +41 -0
- package/dist/core/monitor/stats-aggregator.d.ts +22 -0
- package/dist/core/monitor/stats-aggregator.js +167 -0
- package/dist/core/monitor/stream-content-accumulator.d.ts +14 -0
- package/dist/core/monitor/stream-content-accumulator.js +58 -0
- package/dist/core/monitor/stream-extractor.d.ts +11 -0
- package/dist/core/monitor/stream-extractor.js +72 -0
- package/dist/core/monitor/types.d.ts +106 -0
- package/dist/core/monitor/types.js +1 -0
- package/dist/core/pino-logger.d.ts +1 -1
- package/dist/core/registry.d.ts +1 -1
- package/dist/core/sse-client-adapter.d.ts +1 -1
- package/dist/core/types.d.ts +8 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +1 -1
- package/dist/db/logs.js +2 -2
- package/dist/db/metrics.d.ts +10 -1
- package/dist/db/metrics.js +38 -5
- package/dist/db/migrations/043_add_client_type_and_cache_estimation.sql +6 -0
- package/dist/db/settings.d.ts +8 -0
- package/dist/db/settings.js +31 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -3
- package/dist/proxy/handler/create-proxy-handler.js +11 -3
- package/dist/proxy/handler/failover-loop.js +11 -11
- package/dist/proxy/handler/proxy-handler-utils.d.ts +18 -7
- package/dist/proxy/handler/proxy-handler-utils.js +23 -10
- package/dist/proxy/hooks/builtin/cache-estimation.d.ts +2 -0
- package/dist/proxy/hooks/builtin/cache-estimation.js +49 -0
- package/dist/proxy/hooks/builtin/client-detection.d.ts +2 -0
- package/dist/proxy/hooks/builtin/client-detection.js +31 -0
- package/dist/proxy/hooks/builtin/enhancement-preprocess.js +3 -2
- package/dist/proxy/hooks/builtin/error-logging.js +5 -5
- package/dist/proxy/hooks/builtin/request-logging.js +7 -5
- package/dist/proxy/orchestration/orchestrator.d.ts +4 -4
- package/dist/proxy/orchestration/orchestrator.js +1 -1
- package/dist/proxy/orchestration/scope.d.ts +3 -3
- package/dist/proxy/pipeline/context.js +0 -3
- package/dist/proxy/pipeline/pipeline.d.ts +2 -0
- package/dist/proxy/pipeline/pipeline.js +2 -0
- package/dist/proxy/pipeline/register-hooks.js +21 -10
- package/dist/proxy/pipeline/types.d.ts +0 -1
- package/dist/proxy/proxy-logging.d.ts +2 -1
- package/dist/proxy/proxy-logging.js +47 -14
- package/dist/proxy/tool-error-logger.d.ts +2 -2
- package/dist/proxy/transform/message-mapper.d.ts +5 -5
- package/dist/proxy/transform/message-mapper.js +36 -44
- package/dist/proxy/transform/request-bridge-responses.js +102 -111
- package/dist/proxy/transform/request-transform-responses.js +83 -97
- package/dist/proxy/transform/request-transform.js +49 -49
- package/dist/proxy/transform/response-bridge-responses.js +24 -27
- package/dist/proxy/transform/response-transform-responses.js +24 -23
- package/dist/proxy/transform/response-transform.js +4 -4
- package/dist/proxy/transform/thinking-mapper.js +7 -5
- package/dist/proxy/transform/tool-mapper.d.ts +2 -1
- package/dist/proxy/transform/tool-mapper.js +6 -9
- package/dist/proxy/transform/types-responses.d.ts +1 -1
- package/dist/proxy/transform/types.d.ts +59 -0
- package/dist/proxy/transport/stream.d.ts +1 -1
- package/dist/proxy/transport/transport-fn.d.ts +1 -1
- package/dist/proxy/transport/transport-fn.js +3 -3
- package/dist/routing/cache-estimator.d.ts +38 -0
- package/dist/routing/cache-estimator.js +90 -0
- package/dist/utils/token-counter.d.ts +2 -0
- package/dist/utils/token-counter.js +1 -1
- package/frontend-dist/assets/CardContent-CkdwrZW4.js +1 -0
- package/frontend-dist/assets/CardTitle-DSy7RCiB.js +1 -0
- package/frontend-dist/assets/Checkbox-C3NmNtqa.js +1 -0
- package/frontend-dist/assets/CollapsibleContent-DBzDxLSb.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-B2f-xQJ0.js +1 -0
- package/frontend-dist/assets/Dashboard-D8wX4CUe.js +3 -0
- package/frontend-dist/assets/Input-CWz3gSq7.js +1 -0
- package/frontend-dist/assets/Label-DtNVUGfD.js +1 -0
- package/frontend-dist/assets/Login-CWveR_5r.js +1 -0
- package/frontend-dist/assets/Logs-Bi1whdhz.js +1 -0
- package/frontend-dist/assets/MappingEntryEditor-BRm2vENX.js +1 -0
- package/frontend-dist/assets/ModelCard-7k6e0d6o.js +1 -0
- package/frontend-dist/assets/ModelMappings-BA2biFmT.js +1 -0
- package/frontend-dist/assets/Monitor-B0ZTNvv5.js +1 -0
- package/frontend-dist/assets/Providers-62LJNLRi.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-dcYVsc3f.js +1 -0
- package/frontend-dist/assets/QuickSetup-CRcUhnmK.js +1 -0
- package/frontend-dist/assets/RetryRules-B-Yaery1.js +1 -0
- package/frontend-dist/assets/RouterKeys-HMyzbiSY.js +1 -0
- package/frontend-dist/assets/RovingFocusItem-BLxXLvHz.js +1 -0
- package/frontend-dist/assets/Schedules-DuXBLzKL.js +1 -0
- package/frontend-dist/assets/Settings--oVZQg3A.js +6 -0
- package/frontend-dist/assets/Setup-DCtJiJxI.js +1 -0
- package/frontend-dist/assets/Switch-Cvlk-GzL.js +1 -0
- package/frontend-dist/assets/TooltipTrigger-Caej0jjH.js +1 -0
- package/frontend-dist/assets/TransformRulesForm-kWP-wmEh.js +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-Vwxh-lNJ.js +3 -0
- package/frontend-dist/assets/VisuallyHiddenInput-DVhdgqSs.js +1 -0
- package/frontend-dist/assets/button--Qf6nmZk.js +14 -0
- package/frontend-dist/assets/{copy-D4VVSDtz.js → copy-DBByuQcn.js} +1 -1
- package/frontend-dist/assets/dashboard-B1pq4be7.js +1 -0
- package/frontend-dist/assets/dashboard-BVRlMB_W.js +1 -0
- package/frontend-dist/assets/dialog-MkZTr6jd.js +1 -0
- package/frontend-dist/assets/index-Bg5CP0c1.js +3 -0
- package/frontend-dist/assets/index-C6lfMcX8.css +1 -0
- package/frontend-dist/assets/proxyEnhancement-DpIVSv-g.js +3 -0
- package/frontend-dist/assets/{proxyEnhancement-Cx7MC-ly.js → proxyEnhancement-rSM6KhbN.js} +3 -1
- package/frontend-dist/assets/requestDetail-DZltcrAt.js +1 -0
- package/frontend-dist/assets/requestDetail-NrvqHtpI.js +1 -0
- package/frontend-dist/assets/{trash-2-DRNvvnll.js → trash-2-C1sEBLn-.js} +1 -1
- package/frontend-dist/assets/{useClipboard-DI8woKxZ.js → useClipboard-BQ-_hkN0.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-DT3HBeXR.js → useLogRetention-PhhUFWsW.js} +1 -1
- package/frontend-dist/index.html +3 -3
- package/package.json +1 -2
- package/frontend-dist/assets/CardContent-D3qqQOzw.js +0 -1
- package/frontend-dist/assets/CardTitle-DZmbJGNi.js +0 -1
- package/frontend-dist/assets/Checkbox-D0kd5NM5.js +0 -1
- package/frontend-dist/assets/CollapsibleContent-D7KpsB50.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-dErHwaLp.js +0 -1
- package/frontend-dist/assets/Dashboard-C1l8In2m.js +0 -3
- package/frontend-dist/assets/Input-DSL9i6-h.js +0 -1
- package/frontend-dist/assets/Label-DZV7A4As.js +0 -1
- package/frontend-dist/assets/Login-CGooPG3v.js +0 -1
- package/frontend-dist/assets/Logs-C6Yq0PD5.js +0 -1
- package/frontend-dist/assets/MappingEntryEditor-BCX9sqLY.js +0 -1
- package/frontend-dist/assets/ModelCard-VhRGa9dN.js +0 -1
- package/frontend-dist/assets/ModelMappings-qXsh3Yhb.js +0 -1
- package/frontend-dist/assets/Monitor-CI4Xf34K.js +0 -1
- package/frontend-dist/assets/Providers-CH9v1ctT.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-Fk_bLjAK.js +0 -1
- package/frontend-dist/assets/QuickSetup-CcuF2ifO.js +0 -1
- package/frontend-dist/assets/RetryRules-D6cIJh3h.js +0 -1
- package/frontend-dist/assets/RouterKeys-BgkmcxxN.js +0 -1
- package/frontend-dist/assets/RovingFocusItem-CJnGRDn0.js +0 -1
- package/frontend-dist/assets/Schedules-BdnUtBRd.js +0 -1
- package/frontend-dist/assets/Settings-DwnPkKQC.js +0 -6
- package/frontend-dist/assets/Setup-DTeb40gc.js +0 -1
- package/frontend-dist/assets/Switch-DypIONwh.js +0 -1
- package/frontend-dist/assets/TooltipTrigger-PjT4_t5o.js +0 -1
- package/frontend-dist/assets/TransformRulesForm-B1JEj03a.js +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-D4MlcpiA.js +0 -3
- package/frontend-dist/assets/VisuallyHiddenInput-DfcRACwu.js +0 -1
- package/frontend-dist/assets/button-DOpof38-.js +0 -14
- package/frontend-dist/assets/dashboard-DVDFmK36.js +0 -1
- package/frontend-dist/assets/dashboard-DzlE5uZS.js +0 -1
- package/frontend-dist/assets/dialog-BfODCy5Y.js +0 -1
- package/frontend-dist/assets/index-BEzoY3wI.js +0 -3
- package/frontend-dist/assets/index-iEMoIOdZ.css +0 -1
- package/frontend-dist/assets/proxyEnhancement-BlhJq5sA.js +0 -1
- package/frontend-dist/assets/requestDetail-OsCIcS79.js +0 -1
- package/frontend-dist/assets/requestDetail-bH5SerEV.js +0 -1
package/dist/admin/metrics.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import { getMetricsSummary, getMetricsTimeseries } from "../db/index.js";
|
|
2
|
+
import { getMetricsSummary, getMetricsTimeseries, getClientTypeBreakdown } from "../db/index.js";
|
|
3
3
|
import { resolveTimeRange } from "../utils/time-range.js";
|
|
4
4
|
const LegacyPeriodEnum = Type.Union([
|
|
5
5
|
Type.Literal("1h"), Type.Literal("5h"), Type.Literal("6h"), Type.Literal("24h"),
|
|
@@ -21,6 +21,7 @@ const SummaryQuerySchema = Type.Object({
|
|
|
21
21
|
provider_id: Type.Optional(Type.String()),
|
|
22
22
|
backend_model: Type.Optional(Type.String()),
|
|
23
23
|
router_key_id: Type.Optional(Type.String()),
|
|
24
|
+
client_type: Type.Optional(Type.String()),
|
|
24
25
|
start_time: Type.Optional(Type.String()),
|
|
25
26
|
end_time: Type.Optional(Type.String()),
|
|
26
27
|
});
|
|
@@ -34,6 +35,8 @@ const TimeseriesQuerySchema = Type.Object({
|
|
|
34
35
|
end_time: Type.Optional(Type.String()),
|
|
35
36
|
});
|
|
36
37
|
const DASHBOARD_PERIODS = new Set(["window", "weekly", "monthly"]);
|
|
38
|
+
const PCT_FACTOR = 100;
|
|
39
|
+
const PCT_ROUND_DIGITS = 10;
|
|
37
40
|
function resolveMetricsTime(query, db, routerKeyId, providerId) {
|
|
38
41
|
if (query.start_time && query.end_time) {
|
|
39
42
|
return { startTime: query.start_time, endTime: query.end_time, legacyPeriod: "30d" };
|
|
@@ -50,8 +53,12 @@ export const adminMetricsRoutes = (app, options, done) => {
|
|
|
50
53
|
app.get("/admin/api/metrics/summary", { schema: { querystring: SummaryQuerySchema } }, async (request, reply) => {
|
|
51
54
|
const query = request.query;
|
|
52
55
|
const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id, query.provider_id);
|
|
53
|
-
const summary = getMetricsSummary(db, legacyPeriod, query.provider_id, query.backend_model, query.router_key_id, startTime, endTime);
|
|
54
|
-
|
|
56
|
+
const summary = getMetricsSummary(db, legacyPeriod, query.provider_id, query.backend_model, query.router_key_id, startTime, endTime, query.client_type);
|
|
57
|
+
const breakdown = getClientTypeBreakdown(db, legacyPeriod, query.provider_id, query.backend_model, query.router_key_id, startTime, endTime);
|
|
58
|
+
const totalInputTokens = summary.reduce((sum, r) => sum + r.total_input_tokens, 0);
|
|
59
|
+
const totalCacheHitTokens = summary.reduce((sum, r) => sum + r.total_cache_hit_tokens, 0);
|
|
60
|
+
const cacheHitRate = totalInputTokens > 0 ? totalCacheHitTokens * PCT_FACTOR / totalInputTokens : 0;
|
|
61
|
+
return reply.send({ rows: summary, client_type_breakdown: breakdown, cache_hit_rate: Math.round(cacheHitRate * PCT_ROUND_DIGITS) / PCT_ROUND_DIGITS });
|
|
55
62
|
});
|
|
56
63
|
app.get("/admin/api/metrics/timeseries", { schema: { querystring: TimeseriesQuerySchema } }, async (request, reply) => {
|
|
57
64
|
const query = request.query;
|
package/dist/admin/monitor.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { FastifyPluginCallback } from "fastify";
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import type { StateRegistry } from "../core/registry.js";
|
|
4
|
-
import type { AdaptiveController } from "
|
|
5
|
-
import type { RequestTracker } from "
|
|
4
|
+
import type { AdaptiveController } from "../core/concurrency/index.js";
|
|
5
|
+
import type { RequestTracker } from "../core/monitor/index.js";
|
|
6
6
|
import type { ProxyAgentFactory } from "../proxy/transport/proxy-agent.js";
|
|
7
7
|
interface ProviderRoutesOptions {
|
|
8
8
|
db: Database.Database;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { FastifyPluginCallback } from "fastify";
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import type { StateRegistry } from "../core/registry.js";
|
|
4
|
-
import type { RequestTracker } from "
|
|
5
|
-
import type { AdaptiveController } from "
|
|
4
|
+
import type { RequestTracker } from "../core/monitor/index.js";
|
|
5
|
+
import type { AdaptiveController } from "../core/concurrency/index.js";
|
|
6
6
|
interface QuickSetupRoutesOptions {
|
|
7
7
|
db: Database.Database;
|
|
8
8
|
stateRegistry?: StateRegistry;
|
package/dist/admin/routes.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { FastifyPluginCallback } from "fastify";
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import type { StateRegistry } from "../core/registry.js";
|
|
4
|
-
import type { RequestTracker } from "
|
|
5
|
-
import type { AdaptiveController } from "
|
|
4
|
+
import type { RequestTracker } from "../core/monitor/index.js";
|
|
5
|
+
import type { AdaptiveController } from "../core/concurrency/index.js";
|
|
6
6
|
import type { ProxyAgentFactory } from "../proxy/transport/proxy-agent.js";
|
|
7
7
|
interface AdminRoutesOptions {
|
|
8
8
|
db: Database.Database;
|
package/dist/admin/settings.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { getLogRetentionDays, setLogRetentionDays, getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, getSetting, } from "../db/settings.js";
|
|
3
|
+
import { getLogRetentionDays, setLogRetentionDays, getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, getSetting, getTokenEstimationEnabled, setTokenEstimationEnabled, getClientSessionHeaders, setClientSessionHeaders, } from "../db/settings.js";
|
|
4
4
|
import { HTTP_BAD_REQUEST } from "./constants.js";
|
|
5
5
|
import { API_CODE, apiError } from "./api-response.js";
|
|
6
6
|
export const adminSettingsRoutes = (app, options, done) => {
|
|
@@ -63,6 +63,41 @@ export const adminSettingsRoutes = (app, options, done) => {
|
|
|
63
63
|
logTableMaxSizeMb: getLogTableMaxSizeMb(db),
|
|
64
64
|
};
|
|
65
65
|
});
|
|
66
|
+
app.get("/admin/api/settings/token-estimation", async () => {
|
|
67
|
+
return { enabled: getTokenEstimationEnabled(db) };
|
|
68
|
+
});
|
|
69
|
+
app.put("/admin/api/settings/token-estimation", async (request, reply) => {
|
|
70
|
+
const { enabled } = request.body;
|
|
71
|
+
if (typeof enabled !== "boolean") {
|
|
72
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "enabled must be a boolean"));
|
|
73
|
+
}
|
|
74
|
+
setTokenEstimationEnabled(db, enabled);
|
|
75
|
+
return { success: true };
|
|
76
|
+
});
|
|
77
|
+
app.get("/admin/api/settings/client-session-headers", async () => {
|
|
78
|
+
const entries = getClientSessionHeaders(db);
|
|
79
|
+
return { entries };
|
|
80
|
+
});
|
|
81
|
+
app.put("/admin/api/settings/client-session-headers", async (request, reply) => {
|
|
82
|
+
const body = request.body;
|
|
83
|
+
if (!Array.isArray(body.entries)) {
|
|
84
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "entries must be a non-empty array"));
|
|
85
|
+
}
|
|
86
|
+
const entries = body.entries;
|
|
87
|
+
if (entries.length === 0) {
|
|
88
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "entries must be a non-empty array"));
|
|
89
|
+
}
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (!entry.client_type || typeof entry.client_type !== "string" || entry.client_type.trim() === "") {
|
|
92
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "each entry must have a non-empty client_type"));
|
|
93
|
+
}
|
|
94
|
+
if (!entry.session_header_key || typeof entry.session_header_key !== "string" || entry.session_header_key.trim() === "") {
|
|
95
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "each entry must have a non-empty session_header_key"));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
setClientSessionHeaders(db, entries);
|
|
99
|
+
return { success: true };
|
|
100
|
+
});
|
|
66
101
|
done();
|
|
67
102
|
};
|
|
68
103
|
/** 递归计算目录下所有文件的总大小(字节) */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ISemaphoreControl, AdaptiveState, AdaptiveResult, ProviderConcurrencyParams } from "./types.js";
|
|
2
|
+
import type { Logger } from "../types.js";
|
|
3
|
+
export declare class AdaptiveController {
|
|
4
|
+
private semaphoreControl;
|
|
5
|
+
private logger?;
|
|
6
|
+
private readonly entries;
|
|
7
|
+
constructor(semaphoreControl: ISemaphoreControl, logger?: Logger | undefined);
|
|
8
|
+
init(providerId: string, config: {
|
|
9
|
+
max: number;
|
|
10
|
+
}, semParams: {
|
|
11
|
+
queueTimeoutMs: number;
|
|
12
|
+
maxQueueSize: number;
|
|
13
|
+
}): void;
|
|
14
|
+
/** 移除 provider 的自适应并发状态。调用方还需调用 semaphoreManager.remove() 或 updateConfig() 清理信号量配置。 */
|
|
15
|
+
remove(providerId: string): void;
|
|
16
|
+
/** 清除所有 provider 的自适应并发状态(导入配置后重建前调用) */
|
|
17
|
+
removeAll(): void;
|
|
18
|
+
onRequestComplete(providerId: string, result: AdaptiveResult): void;
|
|
19
|
+
getStatus(providerId: string): AdaptiveState | undefined;
|
|
20
|
+
syncProvider(providerId: string, p: ProviderConcurrencyParams): void;
|
|
21
|
+
private transitionSuccess;
|
|
22
|
+
private transitionFailure;
|
|
23
|
+
private syncToSemaphore;
|
|
24
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const SUCCESS_THRESHOLD = 3;
|
|
2
|
+
const FAILURE_THRESHOLD = 3;
|
|
3
|
+
const DECREASE_STEP = 2;
|
|
4
|
+
const COOLDOWN_MS = 30_000;
|
|
5
|
+
const RATE_LIMIT_STATUS = 429;
|
|
6
|
+
const HALF_DIVISOR = 2;
|
|
7
|
+
const HTTP_SERVER_ERROR_MIN = 500;
|
|
8
|
+
const ADAPTIVE_MIN = 1;
|
|
9
|
+
export class AdaptiveController {
|
|
10
|
+
semaphoreControl;
|
|
11
|
+
logger;
|
|
12
|
+
entries = new Map();
|
|
13
|
+
constructor(semaphoreControl, logger) {
|
|
14
|
+
this.semaphoreControl = semaphoreControl;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
}
|
|
17
|
+
init(providerId, config, semParams) {
|
|
18
|
+
const initialLimit = config.max;
|
|
19
|
+
this.entries.set(providerId, {
|
|
20
|
+
state: {
|
|
21
|
+
currentLimit: initialLimit,
|
|
22
|
+
probeActive: true,
|
|
23
|
+
consecutiveSuccesses: 0,
|
|
24
|
+
consecutiveFailures: 0,
|
|
25
|
+
cooldownUntil: 0,
|
|
26
|
+
},
|
|
27
|
+
max: config.max,
|
|
28
|
+
queueTimeoutMs: semParams.queueTimeoutMs,
|
|
29
|
+
maxQueueSize: semParams.maxQueueSize,
|
|
30
|
+
});
|
|
31
|
+
this.syncToSemaphore(providerId);
|
|
32
|
+
}
|
|
33
|
+
/** 移除 provider 的自适应并发状态。调用方还需调用 semaphoreManager.remove() 或 updateConfig() 清理信号量配置。 */
|
|
34
|
+
remove(providerId) {
|
|
35
|
+
this.entries.delete(providerId);
|
|
36
|
+
}
|
|
37
|
+
/** 清除所有 provider 的自适应并发状态(导入配置后重建前调用) */
|
|
38
|
+
removeAll() {
|
|
39
|
+
this.entries.clear();
|
|
40
|
+
}
|
|
41
|
+
onRequestComplete(providerId, result) {
|
|
42
|
+
const entry = this.entries.get(providerId);
|
|
43
|
+
if (!entry)
|
|
44
|
+
return;
|
|
45
|
+
if (result.success) {
|
|
46
|
+
this.transitionSuccess(providerId, entry, result);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.transitionFailure(providerId, entry, result);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
getStatus(providerId) {
|
|
53
|
+
return this.entries.get(providerId)?.state;
|
|
54
|
+
}
|
|
55
|
+
syncProvider(providerId, p) {
|
|
56
|
+
if (p.adaptive_enabled) {
|
|
57
|
+
const existing = this.entries.get(providerId);
|
|
58
|
+
if (existing) {
|
|
59
|
+
existing.max = p.max_concurrency;
|
|
60
|
+
existing.queueTimeoutMs = p.queue_timeout_ms;
|
|
61
|
+
existing.maxQueueSize = p.max_queue_size;
|
|
62
|
+
existing.state.currentLimit = Math.min(Math.max(existing.state.currentLimit, ADAPTIVE_MIN), existing.max);
|
|
63
|
+
this.syncToSemaphore(providerId);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.init(providerId, { max: p.max_concurrency }, {
|
|
67
|
+
queueTimeoutMs: p.queue_timeout_ms, maxQueueSize: p.max_queue_size,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.remove(providerId);
|
|
73
|
+
// 禁用自适应后恢复信号量到原始 max_concurrency
|
|
74
|
+
this.semaphoreControl.updateConfig(providerId, {
|
|
75
|
+
maxConcurrency: p.max_concurrency,
|
|
76
|
+
queueTimeoutMs: p.queue_timeout_ms,
|
|
77
|
+
maxQueueSize: p.max_queue_size,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
transitionSuccess(providerId, entry, result) {
|
|
82
|
+
const s = entry.state;
|
|
83
|
+
s.consecutiveSuccesses++;
|
|
84
|
+
s.consecutiveFailures = 0;
|
|
85
|
+
if (Date.now() < s.cooldownUntil)
|
|
86
|
+
return;
|
|
87
|
+
if (s.consecutiveSuccesses >= SUCCESS_THRESHOLD) {
|
|
88
|
+
if (!s.probeActive) {
|
|
89
|
+
s.probeActive = true;
|
|
90
|
+
s.consecutiveSuccesses = 0;
|
|
91
|
+
const effective = Math.min(Math.max(s.currentLimit + 1, ADAPTIVE_MIN), entry.max);
|
|
92
|
+
this.logger?.info?.({ providerId, requestId: result.requestId, prevLimit: s.currentLimit, newLimit: s.currentLimit, effectiveLimit: effective, action: "probe_open" }, "Adaptive: probe window opened");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const prevLimit = s.currentLimit;
|
|
96
|
+
s.currentLimit = Math.min(s.currentLimit + 1, entry.max);
|
|
97
|
+
s.consecutiveSuccesses = 0;
|
|
98
|
+
const effective = Math.min(Math.max(s.currentLimit + 1, ADAPTIVE_MIN), entry.max);
|
|
99
|
+
this.logger?.info?.({ providerId, requestId: result.requestId, prevLimit, newLimit: s.currentLimit, effectiveLimit: effective, max: entry.max, action: "limit_increased" }, "Adaptive: limit increased by 1");
|
|
100
|
+
}
|
|
101
|
+
this.syncToSemaphore(providerId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
transitionFailure(providerId, entry, result) {
|
|
105
|
+
const statusCode = result.statusCode;
|
|
106
|
+
// 过滤非并发相关的错误:
|
|
107
|
+
// - retryRuleMatched=true → resilience 层根据重试规则判断为可重试的失败,计入退避
|
|
108
|
+
// - 429: 限流,计入退避
|
|
109
|
+
// - 5xx: 服务端错误(可能过载),计入退避
|
|
110
|
+
// - undefined: 网络异常,计入退避
|
|
111
|
+
// - 2xx/4xx 且 retryRuleMatched!=true: 非并发问题(如 upstream 200 body error 但未命中重试规则),不触发退避
|
|
112
|
+
if (!result.retryRuleMatched && statusCode !== undefined && statusCode !== RATE_LIMIT_STATUS && statusCode < HTTP_SERVER_ERROR_MIN) {
|
|
113
|
+
this.logger?.debug?.({ providerId, statusCode, action: "failure_ignored" }, "Adaptive: non-concurrency failure ignored");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const s = entry.state;
|
|
117
|
+
s.consecutiveFailures++;
|
|
118
|
+
s.consecutiveSuccesses = 0;
|
|
119
|
+
if (statusCode === RATE_LIMIT_STATUS) {
|
|
120
|
+
const prevLimit = s.currentLimit;
|
|
121
|
+
s.currentLimit = Math.max(Math.floor(s.currentLimit / HALF_DIVISOR), ADAPTIVE_MIN);
|
|
122
|
+
s.probeActive = false;
|
|
123
|
+
s.cooldownUntil = Date.now() + COOLDOWN_MS;
|
|
124
|
+
s.consecutiveFailures = 0;
|
|
125
|
+
this.syncToSemaphore(providerId);
|
|
126
|
+
this.logger?.warn?.({ providerId, requestId: result.requestId, prevLimit, newLimit: s.currentLimit, cooldownMs: COOLDOWN_MS, statusCode, action: "rate_limit_backoff" }, "Adaptive: 429 rate limit, halved concurrency and entered cooldown");
|
|
127
|
+
}
|
|
128
|
+
else if (s.consecutiveFailures >= FAILURE_THRESHOLD) {
|
|
129
|
+
const prevLimit = s.currentLimit;
|
|
130
|
+
s.currentLimit = Math.max(s.currentLimit - DECREASE_STEP, ADAPTIVE_MIN);
|
|
131
|
+
s.probeActive = false;
|
|
132
|
+
s.consecutiveFailures = 0;
|
|
133
|
+
this.syncToSemaphore(providerId);
|
|
134
|
+
this.logger?.warn?.({ providerId, requestId: result.requestId, prevLimit, newLimit: s.currentLimit, statusCode, retryRuleMatched: result.retryRuleMatched ?? false, action: "failure_backoff" }, "Adaptive: sustained failures, decreased concurrency");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
syncToSemaphore(providerId) {
|
|
138
|
+
const entry = this.entries.get(providerId);
|
|
139
|
+
if (!entry)
|
|
140
|
+
return;
|
|
141
|
+
// probeActive 时额外加 1 个探针槽位,但不超过 max
|
|
142
|
+
const effectiveLimit = entry.state.probeActive
|
|
143
|
+
? Math.min(Math.max(entry.state.currentLimit + 1, ADAPTIVE_MIN), entry.max)
|
|
144
|
+
: Math.max(entry.state.currentLimit, ADAPTIVE_MIN);
|
|
145
|
+
this.semaphoreControl.updateConfig(providerId, {
|
|
146
|
+
maxConcurrency: effectiveLimit,
|
|
147
|
+
queueTimeoutMs: entry.queueTimeoutMs,
|
|
148
|
+
maxQueueSize: entry.maxQueueSize,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { SemaphoreManager } from "./semaphore.js";
|
|
2
|
+
export { AdaptiveController } from "./adaptive-controller.js";
|
|
3
|
+
export type { AcquireToken } from "./semaphore.js";
|
|
4
|
+
export type { ConcurrencyConfig, AdaptiveState, AdaptiveResult, ISemaphoreControl, ProviderConcurrencyParams, } from "./types.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SemaphoreQueueFullError, SemaphoreTimeoutError } from "../errors.js";
|
|
2
|
+
export { SemaphoreQueueFullError, SemaphoreTimeoutError };
|
|
3
|
+
import type { ConcurrencyConfig } from "./types.js";
|
|
4
|
+
import type { Logger } from "../types.js";
|
|
5
|
+
export interface AcquireToken {
|
|
6
|
+
readonly generation: number;
|
|
7
|
+
/** acquire 时 maxConcurrency=0(不计数),release 时跳过递减 */
|
|
8
|
+
readonly bypassed: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare class SemaphoreManager {
|
|
11
|
+
private readonly entries;
|
|
12
|
+
/** 全局 generation 计数器 — 每次 getOrCreate 分配唯一值,避免 disable+re-enable 后旧 token 匹配新条目 */
|
|
13
|
+
private nextGeneration;
|
|
14
|
+
private getOrCreate;
|
|
15
|
+
updateConfig(providerId: string, config: ConcurrencyConfig): void;
|
|
16
|
+
acquire(providerId: string, signal?: AbortSignal, onQueued?: () => void, logger?: Logger, override?: {
|
|
17
|
+
max_concurrency?: number;
|
|
18
|
+
queue_timeout_ms?: number;
|
|
19
|
+
max_queue_size?: number;
|
|
20
|
+
}): Promise<AcquireToken>;
|
|
21
|
+
release(providerId: string, token: AcquireToken, logger?: Logger): void;
|
|
22
|
+
getStatus(providerId: string): {
|
|
23
|
+
active: number;
|
|
24
|
+
queued: number;
|
|
25
|
+
};
|
|
26
|
+
remove(providerId: string): void;
|
|
27
|
+
/** 清除所有 provider 的信号量配置(导入配置后调用) */
|
|
28
|
+
removeAll(): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { SemaphoreQueueFullError, SemaphoreTimeoutError } from "../errors.js";
|
|
2
|
+
export { SemaphoreQueueFullError, SemaphoreTimeoutError };
|
|
3
|
+
export class SemaphoreManager {
|
|
4
|
+
entries = new Map();
|
|
5
|
+
/** 全局 generation 计数器 — 每次 getOrCreate 分配唯一值,避免 disable+re-enable 后旧 token 匹配新条目 */
|
|
6
|
+
nextGeneration = 0;
|
|
7
|
+
getOrCreate(providerId) {
|
|
8
|
+
let entry = this.entries.get(providerId);
|
|
9
|
+
if (!entry) {
|
|
10
|
+
entry = {
|
|
11
|
+
config: { maxConcurrency: 0, queueTimeoutMs: 0, maxQueueSize: 0 },
|
|
12
|
+
current: 0,
|
|
13
|
+
queue: [],
|
|
14
|
+
generation: ++this.nextGeneration,
|
|
15
|
+
};
|
|
16
|
+
this.entries.set(providerId, entry);
|
|
17
|
+
}
|
|
18
|
+
return entry;
|
|
19
|
+
}
|
|
20
|
+
updateConfig(providerId, config) {
|
|
21
|
+
const entry = this.getOrCreate(providerId);
|
|
22
|
+
entry.config = config;
|
|
23
|
+
if (config.maxConcurrency === 0) {
|
|
24
|
+
while (entry.queue.length > 0) {
|
|
25
|
+
const e = entry.queue.shift();
|
|
26
|
+
if (e.timer)
|
|
27
|
+
clearTimeout(e.timer);
|
|
28
|
+
e.resolve();
|
|
29
|
+
}
|
|
30
|
+
// 递增 generation(全局唯一),使当前所有持有旧 token 的 release() 调用失效
|
|
31
|
+
entry.generation = ++this.nextGeneration;
|
|
32
|
+
entry.current = 0;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (entry.current < 0)
|
|
36
|
+
entry.current = 0;
|
|
37
|
+
// maxConcurrency 降低时**不截断 current**、**不递增 generation**。
|
|
38
|
+
//
|
|
39
|
+
// 原因:截断 current + 递增 generation 会导致所有旧请求的 release() 失效,
|
|
40
|
+
// current 永远停留在截断值(即使旧请求全部完成也无法回落),信号量卡死。
|
|
41
|
+
//
|
|
42
|
+
// 不截断的代价:current 可能暂时超过 maxConcurrency,但旧请求完成后
|
|
43
|
+
// release() 会正常递减 current,自然回落到 maxConcurrency 以下。
|
|
44
|
+
// 新请求在 current >= maxConcurrency 时仍会排队,不会被超限放行。
|
|
45
|
+
//
|
|
46
|
+
// 唯一的例外是 maxConcurrency=0(关闭信号量),在上面单独处理。
|
|
47
|
+
while (entry.current < config.maxConcurrency &&
|
|
48
|
+
entry.queue.length > 0) {
|
|
49
|
+
entry.current++;
|
|
50
|
+
const e = entry.queue.shift();
|
|
51
|
+
if (e.timer)
|
|
52
|
+
clearTimeout(e.timer);
|
|
53
|
+
e.resolve();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async acquire(providerId, signal, onQueued, logger, override) {
|
|
57
|
+
const entry = this.getOrCreate(providerId);
|
|
58
|
+
const maxConcurrency = override?.max_concurrency ?? entry.config.maxConcurrency;
|
|
59
|
+
const queueTimeoutMs = Math.max(0, override?.queue_timeout_ms ?? entry.config.queueTimeoutMs);
|
|
60
|
+
const maxQueueSize = Math.max(0, override?.max_queue_size ?? entry.config.maxQueueSize);
|
|
61
|
+
if (maxConcurrency === 0)
|
|
62
|
+
return { generation: entry.generation, bypassed: true };
|
|
63
|
+
if (entry.current < maxConcurrency) {
|
|
64
|
+
entry.current++;
|
|
65
|
+
logger?.debug?.({ providerId, current: entry.current, maxConcurrency, action: "acquire_direct" }, "Semaphore: acquired directly");
|
|
66
|
+
return { generation: entry.generation, bypassed: false };
|
|
67
|
+
}
|
|
68
|
+
if (entry.queue.length >= maxQueueSize) {
|
|
69
|
+
logger?.debug?.({ providerId, queueLength: entry.queue.length, maxQueueSize, action: "acquire_rejected" }, "Semaphore: queue full, rejecting");
|
|
70
|
+
throw new SemaphoreQueueFullError(providerId);
|
|
71
|
+
}
|
|
72
|
+
logger?.debug?.({ providerId, current: entry.current, maxConcurrency, queueLength: entry.queue.length, action: "acquire_queued" }, "Semaphore: entering wait queue");
|
|
73
|
+
onQueued?.();
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const token = { generation: entry.generation, bypassed: false };
|
|
76
|
+
const qe = {
|
|
77
|
+
resolve: () => {
|
|
78
|
+
logger?.debug?.({ providerId, current: entry.current, maxConcurrency, queueLength: entry.queue.length, action: "acquire_resolved" }, "Semaphore: left wait queue, acquired");
|
|
79
|
+
resolve(token);
|
|
80
|
+
},
|
|
81
|
+
reject: (err) => {
|
|
82
|
+
logger?.debug?.({ providerId, action: "acquire_rejected_internal", error: err.message }, "Semaphore: wait queue entry rejected");
|
|
83
|
+
reject(err);
|
|
84
|
+
},
|
|
85
|
+
timer: null,
|
|
86
|
+
};
|
|
87
|
+
if (queueTimeoutMs > 0) {
|
|
88
|
+
qe.timer = setTimeout(() => {
|
|
89
|
+
const idx = entry.queue.indexOf(qe);
|
|
90
|
+
if (idx !== -1)
|
|
91
|
+
entry.queue.splice(idx, 1);
|
|
92
|
+
reject(new SemaphoreTimeoutError(providerId, queueTimeoutMs));
|
|
93
|
+
}, queueTimeoutMs);
|
|
94
|
+
}
|
|
95
|
+
if (signal) {
|
|
96
|
+
const onAbort = () => {
|
|
97
|
+
const idx = entry.queue.indexOf(qe);
|
|
98
|
+
if (idx !== -1)
|
|
99
|
+
entry.queue.splice(idx, 1);
|
|
100
|
+
if (qe.timer)
|
|
101
|
+
clearTimeout(qe.timer);
|
|
102
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
103
|
+
};
|
|
104
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
105
|
+
}
|
|
106
|
+
entry.queue.push(qe);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
release(providerId, token, logger) {
|
|
110
|
+
const entry = this.entries.get(providerId);
|
|
111
|
+
if (!entry)
|
|
112
|
+
return;
|
|
113
|
+
// bypassed: acquire 时 maxConcurrency=0(不计数),release 跳过递减
|
|
114
|
+
if (token.bypassed)
|
|
115
|
+
return;
|
|
116
|
+
// generation 不匹配说明此请求在 updateConfig 重置前 acquire,其槽位已被回收
|
|
117
|
+
if (token.generation !== entry.generation) {
|
|
118
|
+
logger?.debug?.({ providerId, tokenGen: token.generation, currentGen: entry.generation, action: "release_stale" }, "Semaphore: stale token, skipping release");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (entry.queue.length > 0) {
|
|
122
|
+
const e = entry.queue.shift();
|
|
123
|
+
logger?.debug?.({ providerId, current: entry.current, maxConcurrency: entry.config.maxConcurrency, queueRemaining: entry.queue.length, action: "release_dequeue" }, "Semaphore: released, dequeued next waiter");
|
|
124
|
+
if (e.timer)
|
|
125
|
+
clearTimeout(e.timer);
|
|
126
|
+
e.resolve();
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
entry.current--;
|
|
130
|
+
logger?.debug?.({ providerId, current: entry.current, maxConcurrency: entry.config.maxConcurrency, action: "release_decrement" }, "Semaphore: released slot");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
getStatus(providerId) {
|
|
134
|
+
const entry = this.entries.get(providerId);
|
|
135
|
+
if (!entry)
|
|
136
|
+
return { active: 0, queued: 0 };
|
|
137
|
+
return { active: entry.current, queued: entry.queue.length };
|
|
138
|
+
}
|
|
139
|
+
remove(providerId) {
|
|
140
|
+
const entry = this.entries.get(providerId);
|
|
141
|
+
if (!entry)
|
|
142
|
+
return;
|
|
143
|
+
for (const e of entry.queue) {
|
|
144
|
+
if (e.timer)
|
|
145
|
+
clearTimeout(e.timer);
|
|
146
|
+
e.reject(new Error("Provider removed"));
|
|
147
|
+
}
|
|
148
|
+
this.entries.delete(providerId);
|
|
149
|
+
}
|
|
150
|
+
/** 清除所有 provider 的信号量配置(导入配置后调用) */
|
|
151
|
+
removeAll() {
|
|
152
|
+
for (const [, entry] of this.entries) {
|
|
153
|
+
for (const e of entry.queue) {
|
|
154
|
+
if (e.timer)
|
|
155
|
+
clearTimeout(e.timer);
|
|
156
|
+
e.reject(new Error("Provider removed"));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
this.entries.clear();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Provider-level concurrency control configuration. */
|
|
2
|
+
export interface ConcurrencyConfig {
|
|
3
|
+
maxConcurrency: number;
|
|
4
|
+
queueTimeoutMs: number;
|
|
5
|
+
maxQueueSize: number;
|
|
6
|
+
}
|
|
7
|
+
/** Internal state of adaptive concurrency for a provider. */
|
|
8
|
+
export interface AdaptiveState {
|
|
9
|
+
currentLimit: number;
|
|
10
|
+
probeActive: boolean;
|
|
11
|
+
consecutiveSuccesses: number;
|
|
12
|
+
consecutiveFailures: number;
|
|
13
|
+
cooldownUntil: number;
|
|
14
|
+
}
|
|
15
|
+
/** Result of a request for adaptive concurrency feedback. */
|
|
16
|
+
export interface AdaptiveResult {
|
|
17
|
+
success: boolean;
|
|
18
|
+
statusCode?: number;
|
|
19
|
+
/** 重试规则是否匹配(resilience 层判断为可重试的失败),为 true 时忽略 statusCode 过滤 */
|
|
20
|
+
retryRuleMatched?: boolean;
|
|
21
|
+
/** 触发此反馈的请求日志 ID,用于日志关联 */
|
|
22
|
+
requestId?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Abstraction for semaphore operations (decouples AdaptiveController). */
|
|
25
|
+
export interface ISemaphoreControl {
|
|
26
|
+
updateConfig(providerId: string, config: ConcurrencyConfig): void;
|
|
27
|
+
}
|
|
28
|
+
/** Provider DB fields for adaptive/manual concurrency. */
|
|
29
|
+
export interface ProviderConcurrencyParams {
|
|
30
|
+
adaptive_enabled: number;
|
|
31
|
+
max_concurrency: number;
|
|
32
|
+
queue_timeout_ms: number;
|
|
33
|
+
max_queue_size: number;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a provider's concurrency queue is full.
|
|
3
|
+
*/
|
|
4
|
+
export declare class SemaphoreQueueFullError extends Error {
|
|
5
|
+
readonly providerId: string;
|
|
6
|
+
constructor(providerId: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Thrown when a provider's concurrency wait times out.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SemaphoreTimeoutError extends Error {
|
|
12
|
+
readonly providerId: string;
|
|
13
|
+
readonly timeoutMs: number;
|
|
14
|
+
constructor(providerId: string, timeoutMs: number);
|
|
15
|
+
}
|
|
2
16
|
import type { TransportResult, ResilienceAttempt } from "./types.js";
|
|
3
17
|
/**
|
|
4
18
|
* 跨 provider failover 时由 ResilienceLayer 抛出,
|
package/dist/core/errors.js
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
// src/core/errors.ts
|
|
2
2
|
// Re-export core errors + router-specific errors
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when a provider's concurrency queue is full.
|
|
5
|
+
*/
|
|
6
|
+
export class SemaphoreQueueFullError extends Error {
|
|
7
|
+
providerId;
|
|
8
|
+
constructor(providerId) {
|
|
9
|
+
super(`Provider '${providerId}' concurrency queue is full`);
|
|
10
|
+
this.providerId = providerId;
|
|
11
|
+
this.name = "SemaphoreQueueFullError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Thrown when a provider's concurrency wait times out.
|
|
16
|
+
*/
|
|
17
|
+
export class SemaphoreTimeoutError extends Error {
|
|
18
|
+
providerId;
|
|
19
|
+
timeoutMs;
|
|
20
|
+
constructor(providerId, timeoutMs) {
|
|
21
|
+
super(`Provider '${providerId}' concurrency wait timeout (${timeoutMs}ms)`);
|
|
22
|
+
this.providerId = providerId;
|
|
23
|
+
this.timeoutMs = timeoutMs;
|
|
24
|
+
this.name = "SemaphoreTimeoutError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
5
27
|
/**
|
|
6
28
|
* 跨 provider failover 时由 ResilienceLayer 抛出,
|
|
7
29
|
* orchestrator 捕获后释放当前信号量并获取新 provider 的信号量。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { SemaphoreQueueFullError, SemaphoreTimeoutError } from "./errors.js";
|
|
2
|
+
export type { Logger } from "./types.js";
|
|
3
|
+
export { SemaphoreManager, AdaptiveController } from "./concurrency/index.js";
|
|
4
|
+
export type { AcquireToken, ConcurrencyConfig, AdaptiveState, AdaptiveResult, ISemaphoreControl, ProviderConcurrencyParams } from "./concurrency/index.js";
|
|
5
|
+
export { SessionTracker, StreamLoopGuard, ToolLoopGuard, NGramLoopDetector, DEFAULT_LOOP_PREVENTION_CONFIG, } from "./loop-prevention/index.js";
|
|
6
|
+
export type { LoopPreventionConfig, StreamLoopGuardConfig, ToolLoopGuardConfig, SessionTrackerConfig, NGramDetectorConfig, ToolCallRecord, LoopCheckResult, LoopDetector, LoopDetectorStatus, } from "./loop-prevention/index.js";
|
|
7
|
+
export { RequestTracker, StatsAggregator, RuntimeCollector } from "./monitor/index.js";
|
|
8
|
+
export type { ISemaphoreStatus, IAdaptiveStatus } from "./monitor/index.js";
|
|
9
|
+
export type { ActiveRequest, AttemptSnapshot, ContentBlock, ProviderConcurrencySnapshot, ProviderStats, RuntimeMetrics, SSEClient, StatsSnapshot, StreamContentSnapshot, StreamMetricsSnapshot, } from "./monitor/index.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/core/index.ts — unified re-export
|
|
2
|
+
// Individual sub-path imports also available:
|
|
3
|
+
// ./concurrency
|
|
4
|
+
// ./loop-prevention
|
|
5
|
+
// ./monitor
|
|
6
|
+
export { SemaphoreQueueFullError, SemaphoreTimeoutError } from "./errors.js";
|
|
7
|
+
// Concurrency
|
|
8
|
+
export { SemaphoreManager, AdaptiveController } from "./concurrency/index.js";
|
|
9
|
+
// Loop prevention
|
|
10
|
+
export { SessionTracker, StreamLoopGuard, ToolLoopGuard, NGramLoopDetector, DEFAULT_LOOP_PREVENTION_CONFIG, } from "./loop-prevention/index.js";
|
|
11
|
+
// Monitor
|
|
12
|
+
export { RequestTracker, StatsAggregator, RuntimeCollector } from "./monitor/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { SessionTracker } from "./session-tracker.js";
|
|
2
|
+
export { StreamLoopGuard } from "./stream-loop-guard.js";
|
|
3
|
+
export { ToolLoopGuard } from "./tool-loop-guard.js";
|
|
4
|
+
export { NGramLoopDetector } from "./ngram-detector.js";
|
|
5
|
+
export { DEFAULT_LOOP_PREVENTION_CONFIG, } from "./types.js";
|
|
6
|
+
export type { LoopPreventionConfig, StreamLoopGuardConfig, ToolLoopGuardConfig, SessionTrackerConfig, NGramDetectorConfig, ToolCallRecord, LoopCheckResult, } from "./types.js";
|
|
7
|
+
export type { LoopDetector, LoopDetectorStatus } from "./detector.js";
|