llm-simple-router 0.6.5 → 0.6.7
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 +30 -1
- package/dist/admin/routes.d.ts +2 -0
- package/dist/admin/routes.js +1 -1
- package/dist/db/migrations/033_add_adaptive_concurrency.sql +3 -0
- package/dist/db/providers.d.ts +3 -1
- package/dist/db/providers.js +3 -3
- package/dist/index.js +13 -2
- package/dist/monitor/request-tracker.d.ts +3 -0
- package/dist/monitor/request-tracker.js +7 -0
- package/dist/monitor/types.d.ts +2 -0
- package/dist/proxy/adaptive-controller.d.ts +42 -0
- package/dist/proxy/adaptive-controller.js +139 -0
- package/dist/proxy/anthropic.d.ts +2 -0
- package/dist/proxy/anthropic.js +2 -2
- package/dist/proxy/openai.d.ts +2 -0
- package/dist/proxy/openai.js +2 -2
- package/dist/proxy/orchestrator.d.ts +3 -1
- package/dist/proxy/orchestrator.js +32 -18
- package/frontend-dist/assets/{CardContent-DG1NiXMU.js → CardContent-jQcfCC7J.js} +1 -1
- package/frontend-dist/assets/{CardTitle-6JvqGNd9.js → CardTitle-BrCTvULL.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-BzDHmRLv.js → CascadingModelSelect-BFh67j5d.js} +1 -1
- package/frontend-dist/assets/{Checkbox-D4TrUHCb.js → Checkbox-Bbt7JpdE.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-ZJCGAkxi.js → CollapsibleTrigger-DMnEA0qC.js} +1 -1
- package/frontend-dist/assets/{Collection-CRTZGViV.js → Collection-CVk3TPHc.js} +1 -1
- package/frontend-dist/assets/{Dashboard-C-B4p8HM.js → Dashboard-Coftbg4B.js} +1 -1
- package/frontend-dist/assets/{DialogTitle-CecSHUUq.js → DialogTitle-BbOAZzPQ.js} +1 -1
- package/frontend-dist/assets/{Input-DoCE9j9O.js → Input-DdHY9q0w.js} +1 -1
- package/frontend-dist/assets/{Label-6sa_UFaw.js → Label-DRQv_Dr_.js} +1 -1
- package/frontend-dist/assets/{Login-BkCJQFjz.js → Login-SV3ctFnJ.js} +1 -1
- package/frontend-dist/assets/{Logs-HG_BuJe5.js → Logs-BG45kX6E.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-CjI27TDc.js → ModelMappings-DEaBnRU3.js} +1 -1
- package/frontend-dist/assets/Monitor-ZHOt11n-.js +1 -0
- package/frontend-dist/assets/{PopoverTrigger-D0nT5UVQ.js → PopoverTrigger-z-Z3EjBk.js} +1 -1
- package/frontend-dist/assets/{PopperContent-BbE-uPX0.js → PopperContent-DPC-6a3n.js} +1 -1
- package/frontend-dist/assets/Providers-DpY6pAcg.js +1 -0
- package/frontend-dist/assets/{ProxyEnhancement-DQixi_0_.js → ProxyEnhancement-D6KBDXMp.js} +1 -1
- package/frontend-dist/assets/{RetryRules-B5r18TFL.js → RetryRules-DWI7_WLZ.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-BwdoieS_.js → RouterKeys-CZ1657eX.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-CpDEc1ox.js → RovingFocusItem-BREE2YEV.js} +1 -1
- package/frontend-dist/assets/{Schedules-C0q4rt97.js → Schedules-BVPsBRPi.js} +1 -1
- package/frontend-dist/assets/{SelectValue-CKJWYmgi.js → SelectValue-H8hwQwbk.js} +1 -1
- package/frontend-dist/assets/{Settings-DQN3_4Gx.js → Settings-DHYaYRgU.js} +1 -1
- package/frontend-dist/assets/{Setup-CEk8SRJu.js → Setup-yOYNKkOG.js} +1 -1
- package/frontend-dist/assets/{Switch-ThwlPMEz.js → Switch-CojD3rTH.js} +1 -1
- package/frontend-dist/assets/{TableHeader-UwRhaVOA.js → TableHeader-awoHTsWN.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-CNV5JhP6.js → TabsTrigger-DTKSFj85.js} +1 -1
- package/frontend-dist/assets/{Teleport-BHTgdtZR.js → Teleport-DehYAXud.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-DKa4gPvX.js → TooltipTrigger-C2dl_dml.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-CBbZxE1N.js → UnifiedRequestDialog-C8A-uSTR.js} +1 -1
- package/frontend-dist/assets/{VisuallyHidden-r2E3heZY.js → VisuallyHidden-C8oaGi2S.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-DSyTANlz.js → VisuallyHiddenInput-BMc813t2.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-CNAhVHUE.js → alert-dialog-C8TZQmU6.js} +1 -1
- package/frontend-dist/assets/arrow-down-D-cQXxau.js +1 -0
- package/frontend-dist/assets/{badge-RFq5LS37.js → badge-BVh2WpA5.js} +1 -1
- package/frontend-dist/assets/{button-D4qBQ0nA.js → button-N59D1BGa.js} +2 -2
- package/frontend-dist/assets/check-dDgrw3T3.js +1 -0
- package/frontend-dist/assets/{copy-BxjNh73N.js → copy-DTOecxa9.js} +1 -1
- package/frontend-dist/assets/{dialog-CLepZTFY.js → dialog-kA7AUNoc.js} +1 -1
- package/frontend-dist/assets/{file-text-DqxNey63.js → file-text-DzZCFO7y.js} +1 -1
- package/frontend-dist/assets/{index-C4H_2b0G.js → index-DVTeNVaa.js} +1 -1
- package/frontend-dist/assets/index-xjdbFKXJ.css +1 -0
- package/frontend-dist/assets/{lib-SdzBxIwM.js → lib-ClDokUbt.js} +1 -1
- package/frontend-dist/assets/loader-circle-DVHRL-38.js +1 -0
- package/frontend-dist/assets/{useClipboard-DBClUufY.js → useClipboard-DU1ne-Jw.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-B99Wx8XA.js → useFocusGuards-Btmdbg_F.js} +1 -1
- package/frontend-dist/assets/useFormControl-C5Kjziuj.js +1 -0
- package/frontend-dist/assets/{useLogRetention-CQ7Q54Tt.js → useLogRetention--EGNWXig.js} +1 -1
- package/frontend-dist/assets/useNonce-Cp31yRzV.js +1 -0
- package/frontend-dist/assets/x-DMktsI_w.js +1 -0
- package/frontend-dist/index.html +20 -20
- package/package.json +1 -1
- package/frontend-dist/assets/Monitor-CElP7aKi.js +0 -1
- package/frontend-dist/assets/Providers-DTqfl249.js +0 -1
- package/frontend-dist/assets/arrow-down-CFVTVH7t.js +0 -1
- package/frontend-dist/assets/check-CkLQpfOO.js +0 -1
- package/frontend-dist/assets/index-_Icfkt3I.css +0 -1
- package/frontend-dist/assets/loader-circle-Cffc9Uf0.js +0 -1
- package/frontend-dist/assets/useFormControl-DuWttDd8.js +0 -1
- package/frontend-dist/assets/useNonce-D1Mva4rM.js +0 -1
- package/frontend-dist/assets/x-BJjJvWU8.js +0 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { FastifyPluginCallback } from "fastify";
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
|
|
4
|
+
import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
|
|
4
5
|
import type { RequestTracker } from "../monitor/request-tracker.js";
|
|
5
6
|
interface ProviderRoutesOptions {
|
|
6
7
|
db: Database.Database;
|
|
7
8
|
semaphoreManager?: ProviderSemaphoreManager;
|
|
8
9
|
tracker?: RequestTracker;
|
|
10
|
+
adaptiveController?: AdaptiveConcurrencyController;
|
|
9
11
|
}
|
|
10
12
|
export declare const adminProviderRoutes: FastifyPluginCallback<ProviderRoutesOptions>;
|
|
11
13
|
export {};
|
package/dist/admin/providers.js
CHANGED
|
@@ -77,6 +77,7 @@ const CreateProviderSchema = Type.Object({
|
|
|
77
77
|
max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
78
78
|
queue_timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
79
79
|
max_queue_size: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
80
|
+
adaptive_enabled: Type.Optional(Type.Integer({ minimum: 0, maximum: 1 })),
|
|
80
81
|
});
|
|
81
82
|
const UpdateProviderSchema = Type.Object({
|
|
82
83
|
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
@@ -91,9 +92,10 @@ const UpdateProviderSchema = Type.Object({
|
|
|
91
92
|
max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
92
93
|
queue_timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
93
94
|
max_queue_size: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
95
|
+
adaptive_enabled: Type.Optional(Type.Integer({ minimum: 0, maximum: 1 })),
|
|
94
96
|
});
|
|
95
97
|
export const adminProviderRoutes = (app, options, done) => {
|
|
96
|
-
const { db, semaphoreManager, tracker } = options;
|
|
98
|
+
const { db, semaphoreManager, tracker, adaptiveController } = options;
|
|
97
99
|
app.get("/admin/api/providers", async (_request, reply) => {
|
|
98
100
|
const encryptionKey = getSetting(db, "encryption_key");
|
|
99
101
|
const providers = getAllProviders(db);
|
|
@@ -111,6 +113,7 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
111
113
|
max_concurrency: s.max_concurrency,
|
|
112
114
|
queue_timeout_ms: s.queue_timeout_ms,
|
|
113
115
|
max_queue_size: s.max_queue_size,
|
|
116
|
+
adaptive_enabled: s.adaptive_enabled,
|
|
114
117
|
concurrency_status: semaphoreManager?.getStatus(s.id) ?? { active: 0, queued: 0 },
|
|
115
118
|
created_at: s.created_at,
|
|
116
119
|
updated_at: s.updated_at,
|
|
@@ -128,6 +131,7 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
128
131
|
}
|
|
129
132
|
const encryptedKey = encrypt(body.api_key, getSetting(db, "encryption_key"));
|
|
130
133
|
const { names: normalizedModels, overrides: contextOverrides } = extractModelOverrides((body.models ?? []));
|
|
134
|
+
const isAdaptiveEnabled = body.adaptive_enabled ?? 0;
|
|
131
135
|
const id = createProvider(db, {
|
|
132
136
|
name: body.name,
|
|
133
137
|
api_type: body.api_type,
|
|
@@ -139,6 +143,7 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
139
143
|
max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
140
144
|
queue_timeout_ms: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
141
145
|
max_queue_size: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
146
|
+
adaptive_enabled: isAdaptiveEnabled,
|
|
142
147
|
});
|
|
143
148
|
if (contextOverrides.length > 0) {
|
|
144
149
|
setModelInfoForProvider(db, id, contextOverrides.map(o => ({ model_name: o.name, context_window: o.context_window })));
|
|
@@ -148,6 +153,12 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
148
153
|
queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
149
154
|
maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
150
155
|
});
|
|
156
|
+
adaptiveController?.syncProvider(id, {
|
|
157
|
+
adaptive_enabled: isAdaptiveEnabled,
|
|
158
|
+
max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
159
|
+
queue_timeout_ms: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
160
|
+
max_queue_size: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
161
|
+
});
|
|
151
162
|
tracker?.updateProviderConfig(id, {
|
|
152
163
|
name: body.name,
|
|
153
164
|
maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
@@ -191,6 +202,8 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
191
202
|
fields.queue_timeout_ms = body.queue_timeout_ms;
|
|
192
203
|
if (body.max_queue_size !== undefined)
|
|
193
204
|
fields.max_queue_size = body.max_queue_size;
|
|
205
|
+
if (body.adaptive_enabled !== undefined)
|
|
206
|
+
fields.adaptive_enabled = body.adaptive_enabled;
|
|
194
207
|
if (body.api_key) {
|
|
195
208
|
fields.api_key = encrypt(body.api_key, getSetting(db, "encryption_key"));
|
|
196
209
|
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)}` : "****";
|
|
@@ -208,6 +221,14 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
208
221
|
maxQueueSize: updated.max_queue_size,
|
|
209
222
|
});
|
|
210
223
|
}
|
|
224
|
+
if (body.adaptive_enabled !== undefined || body.max_concurrency !== undefined || body.queue_timeout_ms !== undefined || body.max_queue_size !== undefined) {
|
|
225
|
+
adaptiveController?.syncProvider(id, {
|
|
226
|
+
adaptive_enabled: updated.adaptive_enabled,
|
|
227
|
+
max_concurrency: updated.max_concurrency,
|
|
228
|
+
queue_timeout_ms: updated.queue_timeout_ms,
|
|
229
|
+
max_queue_size: updated.max_queue_size,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
211
232
|
tracker?.updateProviderConfig(id, {
|
|
212
233
|
name: body.name ?? existing.name,
|
|
213
234
|
maxConcurrency: updated.max_concurrency,
|
|
@@ -272,8 +293,16 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
272
293
|
}
|
|
273
294
|
deleteProvider(db, id);
|
|
274
295
|
semaphoreManager?.remove(id);
|
|
296
|
+
adaptiveController?.remove(id);
|
|
275
297
|
tracker?.removeProviderConfig(id);
|
|
276
298
|
return reply.send({ success: true });
|
|
277
299
|
});
|
|
300
|
+
app.get("/admin/api/providers/:id/adaptive-status", async (request, reply) => {
|
|
301
|
+
const { id } = request.params;
|
|
302
|
+
const status = adaptiveController?.getStatus(id);
|
|
303
|
+
if (!status)
|
|
304
|
+
return reply.code(HTTP_NOT_FOUND).send({ error: "Not found or adaptive not enabled" });
|
|
305
|
+
return status;
|
|
306
|
+
});
|
|
278
307
|
done();
|
|
279
308
|
};
|
package/dist/admin/routes.d.ts
CHANGED
|
@@ -3,11 +3,13 @@ import Database from "better-sqlite3";
|
|
|
3
3
|
import { RetryRuleMatcher } from "../proxy/retry-rules.js";
|
|
4
4
|
import type { RequestTracker } from "../monitor/request-tracker.js";
|
|
5
5
|
import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
|
|
6
|
+
import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
|
|
6
7
|
interface AdminRoutesOptions {
|
|
7
8
|
db: Database.Database;
|
|
8
9
|
matcher: RetryRuleMatcher | null;
|
|
9
10
|
tracker?: RequestTracker;
|
|
10
11
|
semaphoreManager?: ProviderSemaphoreManager;
|
|
12
|
+
adaptiveController?: AdaptiveConcurrencyController;
|
|
11
13
|
}
|
|
12
14
|
export declare const adminRoutes: FastifyPluginCallback<AdminRoutesOptions>;
|
|
13
15
|
export {};
|
package/dist/admin/routes.js
CHANGED
|
@@ -21,7 +21,7 @@ export const adminRoutes = (app, options, done) => {
|
|
|
21
21
|
app.register(adminSetupRoutes, { db: options.db });
|
|
22
22
|
app.register(adminAuthPlugin, { db: options.db });
|
|
23
23
|
app.register(adminLoginRoutes, { db: options.db });
|
|
24
|
-
app.register(adminProviderRoutes, { db: options.db, semaphoreManager: options.semaphoreManager, tracker: options.tracker });
|
|
24
|
+
app.register(adminProviderRoutes, { db: options.db, semaphoreManager: options.semaphoreManager, tracker: options.tracker, adaptiveController: options.adaptiveController });
|
|
25
25
|
app.register(adminMappingRoutes, { db: options.db });
|
|
26
26
|
app.register(adminGroupRoutes, { db: options.db });
|
|
27
27
|
app.register(adminScheduleRoutes, { db: options.db });
|
package/dist/db/providers.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface Provider {
|
|
|
11
11
|
max_concurrency: number;
|
|
12
12
|
queue_timeout_ms: number;
|
|
13
13
|
max_queue_size: number;
|
|
14
|
+
adaptive_enabled: number;
|
|
14
15
|
created_at: string;
|
|
15
16
|
updated_at: string;
|
|
16
17
|
}
|
|
@@ -33,8 +34,9 @@ export declare function createProvider(db: Database.Database, provider: {
|
|
|
33
34
|
max_concurrency?: number;
|
|
34
35
|
queue_timeout_ms?: number;
|
|
35
36
|
max_queue_size?: number;
|
|
37
|
+
adaptive_enabled?: number;
|
|
36
38
|
}): string;
|
|
37
|
-
export declare function updateProvider(db: Database.Database, id: string, fields: Partial<Pick<Provider, "name" | "api_type" | "base_url" | "api_key" | "api_key_preview" | "models" | "is_active" | "max_concurrency" | "queue_timeout_ms" | "max_queue_size">>): void;
|
|
39
|
+
export declare function updateProvider(db: Database.Database, id: string, fields: Partial<Pick<Provider, "name" | "api_type" | "base_url" | "api_key" | "api_key_preview" | "models" | "is_active" | "max_concurrency" | "queue_timeout_ms" | "max_queue_size" | "adaptive_enabled">>): void;
|
|
38
40
|
export declare function deleteProvider(db: Database.Database, id: string): void;
|
|
39
41
|
export declare function getActiveProviderByName(db: Database.Database, name: string): {
|
|
40
42
|
id: string;
|
package/dist/db/providers.js
CHANGED
|
@@ -6,7 +6,7 @@ export const PROVIDER_CONCURRENCY_DEFAULTS = {
|
|
|
6
6
|
max_queue_size: 100,
|
|
7
7
|
};
|
|
8
8
|
const PROVIDER_FIELDS = new Set([
|
|
9
|
-
"name", "api_type", "base_url", "api_key", "api_key_preview", "models", "is_active", "max_concurrency", "queue_timeout_ms", "max_queue_size",
|
|
9
|
+
"name", "api_type", "base_url", "api_key", "api_key_preview", "models", "is_active", "max_concurrency", "queue_timeout_ms", "max_queue_size", "adaptive_enabled",
|
|
10
10
|
]);
|
|
11
11
|
export function getActiveProviders(db, apiType) {
|
|
12
12
|
return db
|
|
@@ -22,8 +22,8 @@ export function getProviderById(db, id) {
|
|
|
22
22
|
export function createProvider(db, provider) {
|
|
23
23
|
const id = randomUUID();
|
|
24
24
|
const now = new Date().toISOString();
|
|
25
|
-
db.prepare(`INSERT INTO providers (id, name, api_type, base_url, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, created_at, updated_at)
|
|
26
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, provider.name, provider.api_type, provider.base_url, 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, now, now);
|
|
25
|
+
db.prepare(`INSERT INTO providers (id, name, api_type, base_url, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled, created_at, updated_at)
|
|
26
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, provider.name, provider.api_type, provider.base_url, 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);
|
|
27
27
|
return id;
|
|
28
28
|
}
|
|
29
29
|
export function updateProvider(db, id, fields) {
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { anthropicProxy } from "./proxy/anthropic.js";
|
|
|
20
20
|
import { adminRoutes } from "./admin/routes.js";
|
|
21
21
|
import { RetryRuleMatcher } from "./proxy/retry-rules.js";
|
|
22
22
|
import { ProviderSemaphoreManager } from "./proxy/semaphore.js";
|
|
23
|
+
import { AdaptiveConcurrencyController } from "./proxy/adaptive-controller.js";
|
|
23
24
|
import { RequestTracker } from "./monitor/request-tracker.js";
|
|
24
25
|
import { modelState } from "./proxy/model-state.js";
|
|
25
26
|
import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
|
|
@@ -155,6 +156,8 @@ export async function buildApp(options) {
|
|
|
155
156
|
const semaphoreManager = new ProviderSemaphoreManager();
|
|
156
157
|
const tracker = new RequestTracker({ semaphoreManager, logger: app.log });
|
|
157
158
|
tracker.startPushInterval();
|
|
159
|
+
const adaptiveController = new AdaptiveConcurrencyController(semaphoreManager, app.log);
|
|
160
|
+
tracker.setAdaptiveController(adaptiveController);
|
|
158
161
|
// 5h 用量窗口追踪器,启动时自动补齐缺失窗口
|
|
159
162
|
const usageWindowTracker = new UsageWindowTracker(db);
|
|
160
163
|
usageWindowTracker.reconcileOnStartup();
|
|
@@ -163,7 +166,13 @@ export async function buildApp(options) {
|
|
|
163
166
|
// 从 DB 读取已有 provider 的并发配置,初始化信号量管理器和 tracker
|
|
164
167
|
const allProviders = getAllProviders(db);
|
|
165
168
|
for (const p of allProviders) {
|
|
166
|
-
if (p.
|
|
169
|
+
if (p.adaptive_enabled) {
|
|
170
|
+
adaptiveController.init(p.id, { max: p.max_concurrency }, {
|
|
171
|
+
queueTimeoutMs: p.queue_timeout_ms,
|
|
172
|
+
maxQueueSize: p.max_queue_size,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else if (p.max_concurrency > 0) {
|
|
167
176
|
semaphoreManager.updateConfig(p.id, {
|
|
168
177
|
maxConcurrency: p.max_concurrency,
|
|
169
178
|
queueTimeoutMs: p.queue_timeout_ms,
|
|
@@ -187,6 +196,7 @@ export async function buildApp(options) {
|
|
|
187
196
|
tracker,
|
|
188
197
|
usageWindowTracker,
|
|
189
198
|
sessionTracker,
|
|
199
|
+
adaptiveController,
|
|
190
200
|
});
|
|
191
201
|
app.register(anthropicProxy, {
|
|
192
202
|
db,
|
|
@@ -197,8 +207,9 @@ export async function buildApp(options) {
|
|
|
197
207
|
tracker,
|
|
198
208
|
usageWindowTracker,
|
|
199
209
|
sessionTracker,
|
|
210
|
+
adaptiveController,
|
|
200
211
|
});
|
|
201
|
-
app.register(adminRoutes, { db, matcher, tracker, semaphoreManager });
|
|
212
|
+
app.register(adminRoutes, { db, matcher, tracker, semaphoreManager, adaptiveController });
|
|
202
213
|
// 前端静态文件服务(生产环境)
|
|
203
214
|
const frontendDist = path.resolve(process.env.FRONTEND_DIST || path.join(__dirname, "../frontend-dist"));
|
|
204
215
|
if (existsSync(frontendDist)) {
|
|
@@ -2,6 +2,7 @@ import type { ServerResponse } from "node:http";
|
|
|
2
2
|
import { StatsAggregator } from "./stats-aggregator.js";
|
|
3
3
|
import { RuntimeCollector } from "./runtime-collector.js";
|
|
4
4
|
import type { ProviderSemaphoreManager } from "../proxy/semaphore.js";
|
|
5
|
+
import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
|
|
5
6
|
import type { ActiveRequest, AttemptSnapshot, ProviderConcurrencySnapshot, RuntimeMetrics, StatsSnapshot } from "./types.js";
|
|
6
7
|
export interface TrackerLogger {
|
|
7
8
|
debug(obj: Record<string, unknown>, msg: string): void;
|
|
@@ -22,11 +23,13 @@ export declare class RequestTracker {
|
|
|
22
23
|
readonly statsAggregator: StatsAggregator;
|
|
23
24
|
readonly runtimeCollector: RuntimeCollector;
|
|
24
25
|
private readonly semaphoreManager?;
|
|
26
|
+
private adaptiveController?;
|
|
25
27
|
constructor(deps?: {
|
|
26
28
|
semaphoreManager?: ProviderSemaphoreManager;
|
|
27
29
|
runtimeCollector?: RuntimeCollector;
|
|
28
30
|
logger?: TrackerLogger;
|
|
29
31
|
});
|
|
32
|
+
setAdaptiveController(ctrl: AdaptiveConcurrencyController): void;
|
|
30
33
|
start(req: ActiveRequest): void;
|
|
31
34
|
/** 轻量级节流推送:流式内容变更后 500ms 内批量广播 */
|
|
32
35
|
private scheduleStreamContentPush;
|
|
@@ -22,12 +22,16 @@ export class RequestTracker {
|
|
|
22
22
|
statsAggregator;
|
|
23
23
|
runtimeCollector;
|
|
24
24
|
semaphoreManager;
|
|
25
|
+
adaptiveController;
|
|
25
26
|
constructor(deps) {
|
|
26
27
|
this.semaphoreManager = deps?.semaphoreManager;
|
|
27
28
|
this.runtimeCollector = deps?.runtimeCollector ?? new RuntimeCollector();
|
|
28
29
|
this.statsAggregator = new StatsAggregator();
|
|
29
30
|
this.logger = deps?.logger;
|
|
30
31
|
}
|
|
32
|
+
setAdaptiveController(ctrl) {
|
|
33
|
+
this.adaptiveController = ctrl;
|
|
34
|
+
}
|
|
31
35
|
// --- Core methods ---
|
|
32
36
|
start(req) {
|
|
33
37
|
this.activeMap.set(req.id, { ...req });
|
|
@@ -142,6 +146,7 @@ export class RequestTracker {
|
|
|
142
146
|
const result = [];
|
|
143
147
|
for (const [providerId, config] of this.providerConfigCache) {
|
|
144
148
|
const status = this.semaphoreManager.getStatus(providerId);
|
|
149
|
+
const adaptiveState = this.adaptiveController?.getStatus(providerId);
|
|
145
150
|
result.push({
|
|
146
151
|
providerId,
|
|
147
152
|
providerName: config.name,
|
|
@@ -150,6 +155,8 @@ export class RequestTracker {
|
|
|
150
155
|
queued: status.queued,
|
|
151
156
|
queueTimeoutMs: config.queueTimeoutMs,
|
|
152
157
|
maxQueueSize: config.maxQueueSize,
|
|
158
|
+
adaptiveEnabled: adaptiveState !== undefined,
|
|
159
|
+
adaptiveLimit: adaptiveState?.currentLimit,
|
|
153
160
|
});
|
|
154
161
|
}
|
|
155
162
|
return result;
|
package/dist/monitor/types.d.ts
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ProviderSemaphoreManager } from "./semaphore.js";
|
|
2
|
+
export interface AdaptiveState {
|
|
3
|
+
currentLimit: number;
|
|
4
|
+
probeActive: boolean;
|
|
5
|
+
consecutiveSuccesses: number;
|
|
6
|
+
consecutiveFailures: number;
|
|
7
|
+
cooldownUntil: number;
|
|
8
|
+
}
|
|
9
|
+
interface AdaptiveResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
statusCode?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ProviderAdaptiveConfig {
|
|
14
|
+
adaptive_enabled: number;
|
|
15
|
+
max_concurrency: number;
|
|
16
|
+
queue_timeout_ms: number;
|
|
17
|
+
max_queue_size: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ControllerLogger {
|
|
20
|
+
debug(obj: Record<string, unknown>, msg: string): void;
|
|
21
|
+
warn(obj: Record<string, unknown>, msg: string): void;
|
|
22
|
+
}
|
|
23
|
+
export declare class AdaptiveConcurrencyController {
|
|
24
|
+
private semaphoreManager;
|
|
25
|
+
private logger?;
|
|
26
|
+
private readonly entries;
|
|
27
|
+
constructor(semaphoreManager: ProviderSemaphoreManager, logger?: ControllerLogger | undefined);
|
|
28
|
+
init(providerId: string, config: {
|
|
29
|
+
max: number;
|
|
30
|
+
}, semParams: {
|
|
31
|
+
queueTimeoutMs: number;
|
|
32
|
+
maxQueueSize: number;
|
|
33
|
+
}): void;
|
|
34
|
+
remove(providerId: string): void;
|
|
35
|
+
onRequestComplete(providerId: string, result: AdaptiveResult): void;
|
|
36
|
+
getStatus(providerId: string): AdaptiveState | undefined;
|
|
37
|
+
syncProvider(providerId: string, p: ProviderAdaptiveConfig): void;
|
|
38
|
+
private transitionSuccess;
|
|
39
|
+
private transitionFailure;
|
|
40
|
+
private syncToSemaphore;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
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 ADAPTIVE_MIN = 1;
|
|
8
|
+
export class AdaptiveConcurrencyController {
|
|
9
|
+
semaphoreManager;
|
|
10
|
+
logger;
|
|
11
|
+
entries = new Map();
|
|
12
|
+
constructor(semaphoreManager, logger) {
|
|
13
|
+
this.semaphoreManager = semaphoreManager;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
}
|
|
16
|
+
init(providerId, config, semParams) {
|
|
17
|
+
const initialLimit = config.max;
|
|
18
|
+
this.entries.set(providerId, {
|
|
19
|
+
state: {
|
|
20
|
+
currentLimit: initialLimit,
|
|
21
|
+
probeActive: true,
|
|
22
|
+
consecutiveSuccesses: 0,
|
|
23
|
+
consecutiveFailures: 0,
|
|
24
|
+
cooldownUntil: 0,
|
|
25
|
+
},
|
|
26
|
+
max: config.max,
|
|
27
|
+
queueTimeoutMs: semParams.queueTimeoutMs,
|
|
28
|
+
maxQueueSize: semParams.maxQueueSize,
|
|
29
|
+
});
|
|
30
|
+
this.syncToSemaphore(providerId);
|
|
31
|
+
}
|
|
32
|
+
remove(providerId) {
|
|
33
|
+
this.entries.delete(providerId);
|
|
34
|
+
}
|
|
35
|
+
onRequestComplete(providerId, result) {
|
|
36
|
+
const entry = this.entries.get(providerId);
|
|
37
|
+
if (!entry)
|
|
38
|
+
return;
|
|
39
|
+
if (result.success) {
|
|
40
|
+
this.transitionSuccess(providerId, entry);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
this.transitionFailure(providerId, entry, result.statusCode);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
getStatus(providerId) {
|
|
47
|
+
return this.entries.get(providerId)?.state;
|
|
48
|
+
}
|
|
49
|
+
syncProvider(providerId, p) {
|
|
50
|
+
if (p.adaptive_enabled) {
|
|
51
|
+
const existing = this.entries.get(providerId);
|
|
52
|
+
if (existing) {
|
|
53
|
+
existing.max = p.max_concurrency;
|
|
54
|
+
existing.queueTimeoutMs = p.queue_timeout_ms;
|
|
55
|
+
existing.maxQueueSize = p.max_queue_size;
|
|
56
|
+
existing.state.currentLimit = Math.min(Math.max(existing.state.currentLimit, ADAPTIVE_MIN), existing.max);
|
|
57
|
+
this.syncToSemaphore(providerId);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.init(providerId, { max: p.max_concurrency }, {
|
|
61
|
+
queueTimeoutMs: p.queue_timeout_ms, maxQueueSize: p.max_queue_size,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.remove(providerId);
|
|
67
|
+
// 禁用自适应后恢复信号量到原始 max_concurrency
|
|
68
|
+
this.semaphoreManager.updateConfig(providerId, {
|
|
69
|
+
maxConcurrency: p.max_concurrency,
|
|
70
|
+
queueTimeoutMs: p.queue_timeout_ms,
|
|
71
|
+
maxQueueSize: p.max_queue_size,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
transitionSuccess(providerId, entry) {
|
|
76
|
+
const s = entry.state;
|
|
77
|
+
s.consecutiveSuccesses++;
|
|
78
|
+
s.consecutiveFailures = 0;
|
|
79
|
+
if (Date.now() < s.cooldownUntil)
|
|
80
|
+
return;
|
|
81
|
+
if (s.consecutiveSuccesses >= SUCCESS_THRESHOLD) {
|
|
82
|
+
if (!s.probeActive) {
|
|
83
|
+
s.probeActive = true;
|
|
84
|
+
s.consecutiveSuccesses = 0;
|
|
85
|
+
this.logger?.debug({ providerId, currentLimit: s.currentLimit, action: "probe_open" }, "Adaptive: probe window opened");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
s.currentLimit = Math.min(s.currentLimit + 1, entry.max);
|
|
89
|
+
s.consecutiveSuccesses = 0;
|
|
90
|
+
this.logger?.debug({ providerId, currentLimit: s.currentLimit, max: entry.max, action: "limit_increased" }, "Adaptive: limit increased by 1");
|
|
91
|
+
}
|
|
92
|
+
this.syncToSemaphore(providerId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
transitionFailure(providerId, entry, statusCode) {
|
|
96
|
+
// 只对明确的并发相关错误做退避:
|
|
97
|
+
// - 429: 限流
|
|
98
|
+
// - 5xx: 服务端错误(可能过载)
|
|
99
|
+
// - undefined: 网络异常
|
|
100
|
+
// 2xx(如 upstream 200 body 含 error)和 4xx(客户端错误)不是并发问题,不触发退避
|
|
101
|
+
if (statusCode !== undefined && statusCode !== RATE_LIMIT_STATUS && statusCode < 500) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const s = entry.state;
|
|
105
|
+
s.consecutiveFailures++;
|
|
106
|
+
s.consecutiveSuccesses = 0;
|
|
107
|
+
if (statusCode === RATE_LIMIT_STATUS) {
|
|
108
|
+
const prevLimit = s.currentLimit;
|
|
109
|
+
s.currentLimit = Math.max(Math.floor(s.currentLimit / HALF_DIVISOR), ADAPTIVE_MIN);
|
|
110
|
+
s.probeActive = false;
|
|
111
|
+
s.cooldownUntil = Date.now() + COOLDOWN_MS;
|
|
112
|
+
s.consecutiveFailures = 0;
|
|
113
|
+
this.syncToSemaphore(providerId);
|
|
114
|
+
this.logger?.warn({ providerId, prevLimit, newLimit: s.currentLimit, cooldownMs: COOLDOWN_MS, action: "rate_limit_backoff" }, "Adaptive: 429 rate limit, halved concurrency and entered cooldown");
|
|
115
|
+
}
|
|
116
|
+
else if (s.consecutiveFailures >= FAILURE_THRESHOLD) {
|
|
117
|
+
const prevLimit = s.currentLimit;
|
|
118
|
+
s.currentLimit = Math.max(s.currentLimit - DECREASE_STEP, ADAPTIVE_MIN);
|
|
119
|
+
s.probeActive = false;
|
|
120
|
+
s.consecutiveFailures = 0;
|
|
121
|
+
this.syncToSemaphore(providerId);
|
|
122
|
+
this.logger?.warn({ providerId, prevLimit, newLimit: s.currentLimit, action: "failure_backoff" }, "Adaptive: sustained failures, decreased concurrency");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
syncToSemaphore(providerId) {
|
|
126
|
+
const entry = this.entries.get(providerId);
|
|
127
|
+
if (!entry)
|
|
128
|
+
return;
|
|
129
|
+
// probeActive 时额外加 1 个探针槽位,但不超过 max
|
|
130
|
+
const effectiveLimit = entry.state.probeActive
|
|
131
|
+
? Math.min(Math.max(entry.state.currentLimit + 1, ADAPTIVE_MIN), entry.max)
|
|
132
|
+
: Math.max(entry.state.currentLimit, ADAPTIVE_MIN);
|
|
133
|
+
this.semaphoreManager.updateConfig(providerId, {
|
|
134
|
+
maxConcurrency: effectiveLimit,
|
|
135
|
+
queueTimeoutMs: entry.queueTimeoutMs,
|
|
136
|
+
maxQueueSize: entry.maxQueueSize,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -4,6 +4,7 @@ import { RetryRuleMatcher } from "./retry-rules.js";
|
|
|
4
4
|
import { ProviderSemaphoreManager } from "./semaphore.js";
|
|
5
5
|
import type { RequestTracker } from "../monitor/request-tracker.js";
|
|
6
6
|
import type { UsageWindowTracker } from "./usage-window-tracker.js";
|
|
7
|
+
import type { AdaptiveConcurrencyController } from "./adaptive-controller.js";
|
|
7
8
|
export interface AnthropicProxyOptions {
|
|
8
9
|
db: Database.Database;
|
|
9
10
|
streamTimeoutMs: number;
|
|
@@ -13,5 +14,6 @@ export interface AnthropicProxyOptions {
|
|
|
13
14
|
tracker?: RequestTracker;
|
|
14
15
|
usageWindowTracker?: UsageWindowTracker;
|
|
15
16
|
sessionTracker?: import("./loop-prevention/session-tracker.js").SessionTracker;
|
|
17
|
+
adaptiveController?: AdaptiveConcurrencyController;
|
|
16
18
|
}
|
|
17
19
|
export declare const anthropicProxy: FastifyPluginCallback<AnthropicProxyOptions>;
|
package/dist/proxy/anthropic.js
CHANGED
|
@@ -18,8 +18,8 @@ const ANTHROPIC_ERROR_TYPE = {
|
|
|
18
18
|
};
|
|
19
19
|
const anthropicErrors = createErrorFormatter((kind, message) => ({ type: "error", error: { type: ANTHROPIC_ERROR_TYPE[kind], message } }));
|
|
20
20
|
const anthropicProxyRaw = (app, opts, done) => {
|
|
21
|
-
const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker, sessionTracker } = opts;
|
|
22
|
-
const orchestrator = createOrchestrator(semaphoreManager, tracker);
|
|
21
|
+
const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker, sessionTracker, adaptiveController } = opts;
|
|
22
|
+
const orchestrator = createOrchestrator(semaphoreManager, tracker, adaptiveController);
|
|
23
23
|
app.post(MESSAGES_PATH, async (request, reply) => {
|
|
24
24
|
if (!orchestrator) {
|
|
25
25
|
const body = request.body;
|
package/dist/proxy/openai.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { RetryRuleMatcher } from "./retry-rules.js";
|
|
|
4
4
|
import { ProviderSemaphoreManager } from "./semaphore.js";
|
|
5
5
|
import type { RequestTracker } from "../monitor/request-tracker.js";
|
|
6
6
|
import type { UsageWindowTracker } from "./usage-window-tracker.js";
|
|
7
|
+
import type { AdaptiveConcurrencyController } from "./adaptive-controller.js";
|
|
7
8
|
export interface OpenaiProxyOptions {
|
|
8
9
|
db: Database.Database;
|
|
9
10
|
streamTimeoutMs: number;
|
|
@@ -13,5 +14,6 @@ export interface OpenaiProxyOptions {
|
|
|
13
14
|
tracker?: RequestTracker;
|
|
14
15
|
usageWindowTracker?: UsageWindowTracker;
|
|
15
16
|
sessionTracker?: import("./loop-prevention/session-tracker.js").SessionTracker;
|
|
17
|
+
adaptiveController?: AdaptiveConcurrencyController;
|
|
16
18
|
}
|
|
17
19
|
export declare const openaiProxy: FastifyPluginCallback<OpenaiProxyOptions>;
|
package/dist/proxy/openai.js
CHANGED
|
@@ -24,8 +24,8 @@ function sendError(reply, e) {
|
|
|
24
24
|
return reply.code(e.statusCode).send(e.body);
|
|
25
25
|
}
|
|
26
26
|
const openaiProxyRaw = (app, opts, done) => {
|
|
27
|
-
const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker, sessionTracker } = opts;
|
|
28
|
-
const orchestrator = createOrchestrator(semaphoreManager, tracker);
|
|
27
|
+
const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker, sessionTracker, adaptiveController } = opts;
|
|
28
|
+
const orchestrator = createOrchestrator(semaphoreManager, tracker, adaptiveController);
|
|
29
29
|
app.post(CHAT_COMPLETIONS_PATH, async (request, reply) => {
|
|
30
30
|
if (!orchestrator) {
|
|
31
31
|
const body = request.body;
|
|
@@ -7,6 +7,7 @@ import type { SemaphoreScope } from "./scope.js";
|
|
|
7
7
|
import type { TrackerScope } from "./scope.js";
|
|
8
8
|
import type { ProviderSemaphoreManager } from "./semaphore.js";
|
|
9
9
|
import type { RequestTracker } from "../monitor/request-tracker.js";
|
|
10
|
+
import type { AdaptiveConcurrencyController } from "./adaptive-controller.js";
|
|
10
11
|
export interface OrchestratorConfig {
|
|
11
12
|
resolved: Target;
|
|
12
13
|
provider: {
|
|
@@ -40,13 +41,14 @@ export interface HandleContext {
|
|
|
40
41
|
* 工厂函数,消除 openai/anthropic 创建 orchestrator 的重复代码。
|
|
41
42
|
* 两个 provider 的创建逻辑完全一致。
|
|
42
43
|
*/
|
|
43
|
-
export declare function createOrchestrator(semaphoreManager?: ProviderSemaphoreManager, tracker?: RequestTracker): ProxyOrchestrator | undefined;
|
|
44
|
+
export declare function createOrchestrator(semaphoreManager?: ProviderSemaphoreManager, tracker?: RequestTracker, adaptiveController?: AdaptiveConcurrencyController): ProxyOrchestrator | undefined;
|
|
44
45
|
export declare class ProxyOrchestrator {
|
|
45
46
|
private deps;
|
|
46
47
|
constructor(deps: {
|
|
47
48
|
semaphoreScope: SemaphoreScope;
|
|
48
49
|
trackerScope: TrackerScope;
|
|
49
50
|
resilience: ResilienceLayer;
|
|
51
|
+
adaptiveController?: AdaptiveConcurrencyController;
|
|
50
52
|
});
|
|
51
53
|
handle(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "anthropic", config: OrchestratorConfig, ctx?: HandleContext): Promise<ResilienceResult>;
|
|
52
54
|
private buildActiveRequest;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProviderSwitchNeeded } from "./types.js";
|
|
1
2
|
import { ResilienceLayer as ResilienceLayerClass } from "./resilience.js";
|
|
2
3
|
import { SemaphoreScope as SemaphoreScopeClass } from "./scope.js";
|
|
3
4
|
import { TrackerScope as TrackerScopeClass } from "./scope.js";
|
|
@@ -7,12 +8,12 @@ const DEFAULT_FAILOVER_THRESHOLD = 400;
|
|
|
7
8
|
* 工厂函数,消除 openai/anthropic 创建 orchestrator 的重复代码。
|
|
8
9
|
* 两个 provider 的创建逻辑完全一致。
|
|
9
10
|
*/
|
|
10
|
-
export function createOrchestrator(semaphoreManager, tracker) {
|
|
11
|
+
export function createOrchestrator(semaphoreManager, tracker, adaptiveController) {
|
|
11
12
|
const semaphoreScope = semaphoreManager ? new SemaphoreScopeClass(semaphoreManager) : undefined;
|
|
12
13
|
const trackerScope = tracker ? new TrackerScopeClass(tracker) : undefined;
|
|
13
14
|
if (!semaphoreScope || !trackerScope)
|
|
14
15
|
return undefined;
|
|
15
|
-
return new ProxyOrchestrator({ semaphoreScope, trackerScope, resilience: new ResilienceLayerClass() });
|
|
16
|
+
return new ProxyOrchestrator({ semaphoreScope, trackerScope, resilience: new ResilienceLayerClass(), adaptiveController });
|
|
16
17
|
}
|
|
17
18
|
export class ProxyOrchestrator {
|
|
18
19
|
deps;
|
|
@@ -20,24 +21,37 @@ export class ProxyOrchestrator {
|
|
|
20
21
|
this.deps = deps;
|
|
21
22
|
}
|
|
22
23
|
async handle(request, reply, apiType, config, ctx) {
|
|
24
|
+
const providerId = config.provider.id;
|
|
23
25
|
const trackerReq = this.buildActiveRequest(request, config, apiType);
|
|
24
|
-
|
|
25
|
-
trackerReq.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
trackerReq.queued
|
|
30
|
-
|
|
26
|
+
try {
|
|
27
|
+
const result = await this.deps.trackerScope.track(trackerReq, () => this.deps.semaphoreScope.withSlot(providerId, this.createAbortSignal(request), () => {
|
|
28
|
+
trackerReq.queued = true;
|
|
29
|
+
this.deps.trackerScope.markQueued(trackerReq.id, true);
|
|
30
|
+
}, () => {
|
|
31
|
+
if (trackerReq.queued) {
|
|
32
|
+
trackerReq.queued = false;
|
|
33
|
+
this.deps.trackerScope.markQueued(trackerReq.id, false);
|
|
34
|
+
}
|
|
35
|
+
return this.executeResilience(config, ctx);
|
|
36
|
+
}, config.concurrencyOverride), (result) => this.extractTrackStatus(result), (result) => result.attempts.map(a => ({
|
|
37
|
+
statusCode: a.statusCode,
|
|
38
|
+
error: a.error,
|
|
39
|
+
latencyMs: a.latencyMs,
|
|
40
|
+
providerId: a.target.provider_id,
|
|
41
|
+
})));
|
|
42
|
+
const { status, statusCode } = this.extractTrackStatus(result);
|
|
43
|
+
this.deps.adaptiveController?.onRequestComplete(providerId, { success: status === "completed", statusCode });
|
|
44
|
+
this.sendResponse(reply, result.result, ctx);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
if (e instanceof ProviderSwitchNeeded) {
|
|
49
|
+
const lastResult = e.lastResult;
|
|
50
|
+
const statusCode = lastResult && "statusCode" in lastResult ? lastResult.statusCode : undefined;
|
|
51
|
+
this.deps.adaptiveController?.onRequestComplete(providerId, { success: false, statusCode });
|
|
31
52
|
}
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
statusCode: a.statusCode,
|
|
35
|
-
error: a.error,
|
|
36
|
-
latencyMs: a.latencyMs,
|
|
37
|
-
providerId: a.target.provider_id,
|
|
38
|
-
})));
|
|
39
|
-
this.sendResponse(reply, result.result, ctx);
|
|
40
|
-
return result;
|
|
53
|
+
throw e;
|
|
54
|
+
}
|
|
41
55
|
}
|
|
42
56
|
buildActiveRequest(request, config, apiType) {
|
|
43
57
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-
|
|
1
|
+
import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-N59D1BGa.js";var s=[`data-size`],c=n({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(n){let c=n;return(l,u)=>(a(),e(`div`,{"data-slot":`card`,"data-size":n.size,class:t(r(o)(`ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-lg py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col`,c.class))},[i(l.$slots,`default`)],10,s))}}),l=n({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-content`,class:t(r(o)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[i(n.$slots,`default`)],2))}});export{c as n,l as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-
|
|
1
|
+
import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-N59D1BGa.js";var s=n({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-header`,class:t(r(o)(`gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]`,s.class))},[i(n.$slots,`default`)],2))}}),c=n({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-title`,class:t(r(o)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[i(n.$slots,`default`)],2))}});export{s as n,c as t};
|