llm-simple-router 0.8.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +319 -0
- package/README.md +2 -0
- package/config/recommended-providers.json +33 -9
- package/config/recommended-retry-rules.json +9 -8
- package/dist/admin/providers.js +11 -9
- package/dist/admin/quick-setup.d.ts +13 -0
- package/dist/admin/quick-setup.js +169 -0
- package/dist/admin/recommended.js +5 -1
- package/dist/admin/routes.js +2 -0
- package/dist/config/model-context.d.ts +8 -2
- package/dist/config/model-context.js +17 -5
- package/dist/config/recommended.d.ts +2 -1
- package/dist/config/recommended.js +5 -9
- package/dist/core/constants.js +2 -0
- package/dist/db/index.js +5 -0
- package/dist/db/migrations/033_add_adaptive_concurrency.sql +3 -0
- package/dist/db/migrations/036_add_openai_responses_api_type.sql +68 -0
- package/dist/db/migrations/037_fix_035_data_corruption.sql +54 -0
- package/dist/db/providers.d.ts +3 -3
- package/dist/index.js +7 -3
- package/dist/metrics/metrics-extractor.d.ts +3 -2
- package/dist/metrics/metrics-extractor.js +45 -0
- package/dist/metrics/sse-metrics-transform.d.ts +1 -1
- package/dist/metrics/sse-metrics-transform.js +10 -0
- package/dist/monitor/request-tracker.d.ts +1 -1
- package/dist/monitor/stream-content-accumulator.d.ts +1 -1
- package/dist/monitor/stream-extractor.d.ts +1 -1
- package/dist/monitor/stream-extractor.js +21 -0
- package/dist/monitor/types.d.ts +1 -1
- package/dist/proxy/handler/proxy-handler-utils.d.ts +1 -1
- package/dist/proxy/handler/proxy-handler.d.ts +1 -1
- package/dist/proxy/handler/proxy-handler.js +8 -2
- package/dist/proxy/handler/responses.d.ts +7 -0
- package/dist/proxy/handler/responses.js +48 -0
- package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +1 -1
- package/dist/proxy/loop-prevention/tool-loop-guard.js +10 -0
- package/dist/proxy/orchestration/orchestrator.d.ts +1 -1
- package/dist/proxy/orchestration/semaphore.js +6 -0
- package/dist/proxy/patch/deepseek/index.d.ts +1 -1
- package/dist/proxy/patch/deepseek/patch-thinking-param.d.ts +1 -1
- package/dist/proxy/patch/index.d.ts +3 -0
- package/dist/proxy/patch/index.js +28 -0
- package/dist/proxy/patch/tool-round-limiter.d.ts +1 -1
- package/dist/proxy/patch/tool-round-limiter.js +16 -0
- package/dist/proxy/proxy-core.d.ts +1 -1
- package/dist/proxy/proxy-logging.d.ts +3 -3
- package/dist/proxy/response-transform.js +13 -0
- package/dist/proxy/transform/id-utils.d.ts +1 -0
- package/dist/proxy/transform/id-utils.js +3 -0
- package/dist/proxy/transform/plugin-types.d.ts +5 -5
- package/dist/proxy/transform/request-bridge-responses.d.ts +19 -0
- package/dist/proxy/transform/request-bridge-responses.js +311 -0
- package/dist/proxy/transform/request-transform-responses.d.ts +2 -0
- package/dist/proxy/transform/request-transform-responses.js +350 -0
- package/dist/proxy/transform/response-bridge-responses.d.ts +23 -0
- package/dist/proxy/transform/response-bridge-responses.js +173 -0
- package/dist/proxy/transform/response-transform-responses.d.ts +2 -0
- package/dist/proxy/transform/response-transform-responses.js +137 -0
- package/dist/proxy/transform/stream-ant2resp.d.ts +26 -0
- package/dist/proxy/transform/stream-ant2resp.js +322 -0
- package/dist/proxy/transform/stream-bridge-chat2resp.d.ts +40 -0
- package/dist/proxy/transform/stream-bridge-chat2resp.js +382 -0
- package/dist/proxy/transform/stream-bridge-resp2chat.d.ts +24 -0
- package/dist/proxy/transform/stream-bridge-resp2chat.js +237 -0
- package/dist/proxy/transform/stream-resp2ant.d.ts +21 -0
- package/dist/proxy/transform/stream-resp2ant.js +238 -0
- package/dist/proxy/transform/stream-transform-base.d.ts +1 -0
- package/dist/proxy/transform/stream-transform-base.js +3 -0
- package/dist/proxy/transform/transform-coordinator.d.ts +1 -0
- package/dist/proxy/transform/transform-coordinator.js +127 -8
- package/dist/proxy/transform/types-responses.d.ts +177 -0
- package/dist/proxy/transform/types-responses.js +27 -0
- package/dist/proxy/transform/types.d.ts +3 -1
- package/dist/proxy/transport/transport-fn.d.ts +1 -1
- package/frontend-dist/assets/CardContent-BhMXx-JD.js +1 -0
- package/frontend-dist/assets/CardTitle-DQDjTee3.js +1 -0
- package/frontend-dist/assets/CascadingModelSelect-JBQq3JJt.js +1 -0
- package/frontend-dist/assets/Checkbox-ByxbKP_C.js +1 -0
- package/frontend-dist/assets/CollapsibleContent-GecW2Jk_.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-Cib3-OsK.js +1 -0
- package/frontend-dist/assets/Collection-Dbvdpa0m.js +1 -0
- package/frontend-dist/assets/Dashboard-3MJPLflT.js +3 -0
- package/frontend-dist/assets/DialogTitle-Ej_rtfV1.js +1 -0
- package/frontend-dist/assets/{Input-CAnKUBBK.js → Input-tcnrMp1v.js} +1 -1
- package/frontend-dist/assets/Label-BwzPFyL-.js +1 -0
- package/frontend-dist/assets/Login-Cdsw2pWC.js +1 -0
- package/frontend-dist/assets/Logs-5_CWiws5.js +1 -0
- package/frontend-dist/assets/MappingList-D8HRph05.js +1 -0
- package/frontend-dist/assets/ModelCard-CZbQcYNn.js +1 -0
- package/frontend-dist/assets/ModelMappings-CJqgl7O8.js +1 -0
- package/frontend-dist/assets/Monitor-B8v5a8fB.js +1 -0
- package/frontend-dist/assets/PopoverTrigger-C88SpJNZ.js +1 -0
- package/frontend-dist/assets/PopperContent-6BXua_FZ.js +1 -0
- package/frontend-dist/assets/Providers-DH0nvlGn.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-CAH-44W-.js +5 -0
- package/frontend-dist/assets/QuickSetup-CsDO-ZGP.js +1 -0
- package/frontend-dist/assets/RetryRules-8iT9fLsH.js +1 -0
- package/frontend-dist/assets/RouterKeys-BFoEmWgj.js +1 -0
- package/frontend-dist/assets/RovingFocusItem-DdPUFQHC.js +1 -0
- package/frontend-dist/assets/Schedules-B8Se31u4.js +1 -0
- package/frontend-dist/assets/SelectValue-CT2z_-6j.js +1 -0
- package/frontend-dist/assets/Settings-BHvtsJKD.js +6 -0
- package/frontend-dist/assets/Setup-k-l9KDC0.js +1 -0
- package/frontend-dist/assets/Switch-D1NdA4ax.js +1 -0
- package/frontend-dist/assets/TableHeader-CcMyOsUB.js +1 -0
- package/frontend-dist/assets/Teleport-Bmeh33lB.js +3 -0
- package/frontend-dist/assets/TooltipTrigger-LegC_Uvp.js +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-BVw6W2pk.js +3 -0
- package/frontend-dist/assets/UnifiedRequestDialog-C4MTxb25.css +1 -0
- package/frontend-dist/assets/VisuallyHidden-ogESfc9X.js +1 -0
- package/frontend-dist/assets/VisuallyHiddenInput-BQemVGau.js +1 -0
- package/frontend-dist/assets/alert-dialog-DzKCAoYJ.js +1 -0
- package/frontend-dist/assets/badge-C-9zPTgw.js +1 -0
- package/frontend-dist/assets/button-D27ClX8J.js +14 -0
- package/frontend-dist/assets/check-yTAivq1h.js +1 -0
- package/frontend-dist/assets/common-CWCbKHOK.js +1 -0
- package/frontend-dist/assets/common-D4xnnaqi.js +1 -0
- package/frontend-dist/assets/constants-B-VELBjk.js +1 -0
- package/frontend-dist/assets/copy-DWG9cQPR.js +1 -0
- package/frontend-dist/assets/dashboard-B8eI-t8c.js +1 -0
- package/frontend-dist/assets/dashboard-Dbe6A2lu.js +1 -0
- package/frontend-dist/assets/dialog-BnYR6_dh.js +1 -0
- package/frontend-dist/assets/file-text-D33FJAPX.js +1 -0
- package/frontend-dist/assets/format-BhxQSgt6.js +1 -0
- package/frontend-dist/assets/i18n-CwUfS0tE.js +1 -0
- package/frontend-dist/assets/index-B348nt-T.css +1 -0
- package/frontend-dist/assets/index-DPRxBo3N.js +1 -0
- package/frontend-dist/assets/lib-D0Ek2pPZ.js +1 -0
- package/frontend-dist/assets/loader-circle-EpKC006I.js +1 -0
- package/frontend-dist/assets/login-BTolYxVI.js +1 -0
- package/frontend-dist/assets/login-w_ICpiU5.js +1 -0
- package/frontend-dist/assets/logs-7dT2uyMa.js +1 -0
- package/frontend-dist/assets/logs-_3w8tDQa.js +1 -0
- package/frontend-dist/assets/mappings-Bbn3r2uJ.js +1 -0
- package/frontend-dist/assets/mappings-CTZ-zb1x.js +1 -0
- package/frontend-dist/assets/monitor-DN5m5n_x.js +1 -0
- package/frontend-dist/assets/monitor-DysWEOtt.js +1 -0
- package/frontend-dist/assets/providers-C1gQGzwa.js +1 -0
- package/frontend-dist/assets/providers-CCfko___.js +1 -0
- package/frontend-dist/assets/proxyEnhancement-BItabyLo.js +1 -0
- package/frontend-dist/assets/proxyEnhancement-DeMb7wIE.js +1 -0
- package/frontend-dist/assets/quickSetup-C75HMC_z.js +1 -0
- package/frontend-dist/assets/quickSetup-DStZWiuf.js +1 -0
- package/frontend-dist/assets/requestDetail-BoaPEQs-.js +1 -0
- package/frontend-dist/assets/requestDetail-CM5kFgy6.js +1 -0
- package/frontend-dist/assets/retryRules-CIF37gOl.js +1 -0
- package/frontend-dist/assets/retryRules-o_D8S5gy.js +1 -0
- package/frontend-dist/assets/routerKeys-BAvjW0V8.js +1 -0
- package/frontend-dist/assets/routerKeys-mQt2YPuE.js +1 -0
- package/frontend-dist/assets/schedules-BCV2rxK-.js +1 -0
- package/frontend-dist/assets/schedules-Qte9b7b_.js +1 -0
- package/frontend-dist/assets/settings-Bgu2lJfy.js +1 -0
- package/frontend-dist/assets/settings-UCmMSq_F.js +1 -0
- package/frontend-dist/assets/setup-B_fAfMoV.js +1 -0
- package/frontend-dist/assets/setup-Chc246Zi.js +1 -0
- package/frontend-dist/assets/sidebar-B7rejnZA.js +1 -0
- package/frontend-dist/assets/sidebar-CBMItLst.js +1 -0
- package/frontend-dist/assets/sun-BylRZIWt.js +1 -0
- package/frontend-dist/assets/trash-2-QNFff7V4.js +1 -0
- package/frontend-dist/assets/{useClipboard-BmmsNSGV.js → useClipboard-BFt5f-_-.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-A-9V2Y-b.js → useFocusGuards-DQBZKWnu.js} +1 -1
- package/frontend-dist/assets/useFormControl-T2RQNBqs.js +1 -0
- package/frontend-dist/assets/useLogRetention-NrrZrpPE.js +1 -0
- package/frontend-dist/assets/useNonce-DR38uny5.js +1 -0
- package/frontend-dist/assets/useTheme-CpTI547G.js +1 -0
- package/frontend-dist/assets/x-DSgLgKC_.js +1 -0
- package/frontend-dist/index.html +25 -22
- package/package.json +1 -1
- package/dist/db/migrations/033_add_pipeline_snapshot.sql +0 -1
- package/frontend-dist/assets/CardContent-BVMQ2_pg.js +0 -1
- package/frontend-dist/assets/CardTitle-GLv7QyIY.js +0 -1
- package/frontend-dist/assets/CascadingModelSelect-CBhqKFDX.js +0 -1
- package/frontend-dist/assets/Checkbox-HPVDmEdV.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-DhxD9tpM.js +0 -1
- package/frontend-dist/assets/Collection-BRt7YxN8.js +0 -1
- package/frontend-dist/assets/Dashboard-D1Ys8Zog.js +0 -3
- package/frontend-dist/assets/DialogTitle-23q73lwF.js +0 -1
- package/frontend-dist/assets/Label-DWdYtVMI.js +0 -1
- package/frontend-dist/assets/Login-w5WFOinP.js +0 -1
- package/frontend-dist/assets/Logs-C1F1ZmWF.js +0 -1
- package/frontend-dist/assets/ModelMappings-BzmecWEH.js +0 -1
- package/frontend-dist/assets/Monitor-DrAZFTKR.js +0 -1
- package/frontend-dist/assets/PopoverTrigger-Bj65uUbv.js +0 -1
- package/frontend-dist/assets/PopperContent-gzzf1XHe.js +0 -1
- package/frontend-dist/assets/Providers-DSgf4mb6.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-Bb1cCP6d.js +0 -5
- package/frontend-dist/assets/RetryRules-BwPfEZtm.js +0 -1
- package/frontend-dist/assets/RouterKeys-CzTSq1Mx.js +0 -1
- package/frontend-dist/assets/RovingFocusItem-CXM_Yfkm.js +0 -1
- package/frontend-dist/assets/Schedules-DVilCXrC.js +0 -1
- package/frontend-dist/assets/SelectValue-C0-LzGQY.js +0 -1
- package/frontend-dist/assets/Settings-Bpk53zVX.js +0 -6
- package/frontend-dist/assets/Setup-Dn7EgC49.js +0 -1
- package/frontend-dist/assets/Switch-BO8Ooae6.js +0 -1
- package/frontend-dist/assets/TableHeader-Bded9VTC.js +0 -1
- package/frontend-dist/assets/TabsTrigger-BzKMi9AF.js +0 -1
- package/frontend-dist/assets/Teleport-DizRK5O3.js +0 -3
- package/frontend-dist/assets/TooltipTrigger-EiIy2zn8.js +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-BABsTaGb.js +0 -3
- package/frontend-dist/assets/UnifiedRequestDialog-BjEigSaR.css +0 -1
- package/frontend-dist/assets/VisuallyHidden-5AozJQza.js +0 -1
- package/frontend-dist/assets/VisuallyHiddenInput-DdiZrV2i.js +0 -1
- package/frontend-dist/assets/alert-dialog-DlKUuTPe.js +0 -1
- package/frontend-dist/assets/arrow-down-CxWKmZ2I.js +0 -1
- package/frontend-dist/assets/badge-9KJEMa53.js +0 -1
- package/frontend-dist/assets/button-Ul8WlrM5.js +0 -12
- package/frontend-dist/assets/check-7ahK--N4.js +0 -1
- package/frontend-dist/assets/constants-D_0jiLjw.js +0 -1
- package/frontend-dist/assets/copy-DzU2pAMG.js +0 -1
- package/frontend-dist/assets/dialog-B9j-FMrd.js +0 -1
- package/frontend-dist/assets/file-text-Bj3ZIo-E.js +0 -1
- package/frontend-dist/assets/format-Dln15Luw.js +0 -1
- package/frontend-dist/assets/index-Bz_ZaXNn.css +0 -1
- package/frontend-dist/assets/index-MedWZMHB.js +0 -1
- package/frontend-dist/assets/lib-Hhs3NqfD.js +0 -1
- package/frontend-dist/assets/loader-circle-5TJUukEe.js +0 -1
- package/frontend-dist/assets/useFormControl-DEO19lRe.js +0 -1
- package/frontend-dist/assets/useLogRetention-BfnBFZ5K.js +0 -1
- package/frontend-dist/assets/useNonce-BfwUJ1Ci.js +0 -1
- package/frontend-dist/assets/x-Cfopt3QL.js +0 -1
- /package/dist/db/migrations/{034_drop_redundant_log_columns.sql → 035_drop_redundant_log_columns.sql} +0 -0
- /package/frontend-dist/assets/{ohash.D__AXeF1-D5e5Wyzx.js → ohash.D__AXeF1-CTo5WcIm.js} +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { createProvider } from "../db/providers.js";
|
|
3
|
+
import { createMappingGroup, updateMappingGroup } from "../db/mappings.js";
|
|
4
|
+
import { createRetryRule } from "../db/retry-rules.js";
|
|
5
|
+
import { upsertTransformRule } from "../db/transform-rules.js";
|
|
6
|
+
import { encrypt } from "../utils/crypto.js";
|
|
7
|
+
import { getSetting } from "../db/settings.js";
|
|
8
|
+
import { HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_CONFLICT } from "./constants.js";
|
|
9
|
+
import { API_CODE, apiError } from "./api-response.js";
|
|
10
|
+
import { PROVIDER_CONCURRENCY_DEFAULTS } from "../db/providers.js";
|
|
11
|
+
const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
12
|
+
const API_KEY_PREVIEW_MIN_LENGTH = 8;
|
|
13
|
+
const API_KEY_PREVIEW_PREFIX_LEN = 4;
|
|
14
|
+
const QuickSetupProviderSchema = Type.Object({
|
|
15
|
+
name: Type.String({ minLength: 1 }),
|
|
16
|
+
api_type: Type.Union([Type.Literal("openai"), Type.Literal("openai-responses"), Type.Literal("anthropic")]),
|
|
17
|
+
base_url: Type.String({ minLength: 1 }),
|
|
18
|
+
api_key: Type.String({ minLength: 1 }),
|
|
19
|
+
models: Type.Array(Type.Object({
|
|
20
|
+
name: Type.String(),
|
|
21
|
+
context_window: Type.Optional(Type.Number()),
|
|
22
|
+
patches: Type.Optional(Type.Array(Type.String())),
|
|
23
|
+
})),
|
|
24
|
+
concurrency_mode: Type.Optional(Type.Union([Type.Literal("auto"), Type.Literal("manual"), Type.Literal("none")])),
|
|
25
|
+
max_concurrency: Type.Optional(Type.Number()),
|
|
26
|
+
queue_timeout_ms: Type.Optional(Type.Number()),
|
|
27
|
+
max_queue_size: Type.Optional(Type.Number()),
|
|
28
|
+
});
|
|
29
|
+
const QuickSetupMappingSchema = Type.Object({
|
|
30
|
+
client_model: Type.String({ minLength: 1 }),
|
|
31
|
+
backend_model: Type.String({ minLength: 1 }),
|
|
32
|
+
});
|
|
33
|
+
const QuickSetupRetryRuleSchema = Type.Object({
|
|
34
|
+
name: Type.String({ minLength: 1 }),
|
|
35
|
+
status_code: Type.Number({ minimum: 100, maximum: 599 }),
|
|
36
|
+
body_pattern: Type.String({ minLength: 1 }),
|
|
37
|
+
retry_strategy: Type.Union([Type.Literal("fixed"), Type.Literal("exponential")]),
|
|
38
|
+
retry_delay_ms: Type.Number({ minimum: 100 }),
|
|
39
|
+
max_retries: Type.Number({ minimum: 0, maximum: 100 }),
|
|
40
|
+
max_delay_ms: Type.Number({ minimum: 100 }),
|
|
41
|
+
});
|
|
42
|
+
const QuickSetupTransformSchema = Type.Object({
|
|
43
|
+
inject_headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
44
|
+
request_defaults: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
45
|
+
drop_fields: Type.Optional(Type.Array(Type.String())),
|
|
46
|
+
});
|
|
47
|
+
const QuickSetupSchema = Type.Object({
|
|
48
|
+
provider: QuickSetupProviderSchema,
|
|
49
|
+
mappings: Type.Array(QuickSetupMappingSchema),
|
|
50
|
+
retry_rules: Type.Array(QuickSetupRetryRuleSchema),
|
|
51
|
+
transform_rules: Type.Optional(QuickSetupTransformSchema),
|
|
52
|
+
});
|
|
53
|
+
export const adminQuickSetupRoutes = (app, options, done) => {
|
|
54
|
+
const { db, stateRegistry, tracker, adaptiveController } = options;
|
|
55
|
+
app.post("/admin/api/quick-setup", { schema: { body: QuickSetupSchema } }, async (request, reply) => {
|
|
56
|
+
const body = request.body;
|
|
57
|
+
// 1. Validate provider name
|
|
58
|
+
if (!PROVIDER_NAME_RE.test(body.provider.name)) {
|
|
59
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "Provider 名称仅允许英文大小写字母、数字、横线和下划线"));
|
|
60
|
+
}
|
|
61
|
+
// 2. Check no duplicate provider name
|
|
62
|
+
const existing = db.prepare("SELECT id FROM providers WHERE name = ?").get(body.provider.name);
|
|
63
|
+
if (existing) {
|
|
64
|
+
return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, `Provider 名称 '${body.provider.name}' 已存在`));
|
|
65
|
+
}
|
|
66
|
+
// 3. Validate retry rule body_pattern regex
|
|
67
|
+
for (const rule of body.retry_rules) {
|
|
68
|
+
try {
|
|
69
|
+
new RegExp(rule.body_pattern);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.INVALID_REGEX, `重试规则「${rule.name}」的 body_pattern 不是有效的正则表达式`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// 4. Start transaction
|
|
76
|
+
const encryptionKey = getSetting(db, "encryption_key");
|
|
77
|
+
const createAll = db.transaction(() => {
|
|
78
|
+
// 5. Create provider with models JSON
|
|
79
|
+
const encryptedKey = encrypt(body.provider.api_key, encryptionKey);
|
|
80
|
+
const modelEntries = body.provider.models.map(m => ({
|
|
81
|
+
name: m.name,
|
|
82
|
+
...(m.context_window != null ? { context_window: m.context_window } : {}),
|
|
83
|
+
...(m.patches && m.patches.length > 0 ? { patches: m.patches } : {}),
|
|
84
|
+
}));
|
|
85
|
+
const adaptiveEnabled = body.provider.concurrency_mode === 'auto' ? 1 : 0;
|
|
86
|
+
const maxConcurrency = body.provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency;
|
|
87
|
+
const queueTimeoutMs = body.provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms;
|
|
88
|
+
const maxQueueSize = body.provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size;
|
|
89
|
+
const providerId = createProvider(db, {
|
|
90
|
+
name: body.provider.name,
|
|
91
|
+
api_type: body.provider.api_type,
|
|
92
|
+
base_url: body.provider.base_url,
|
|
93
|
+
api_key: encryptedKey,
|
|
94
|
+
api_key_preview: body.provider.api_key.length > API_KEY_PREVIEW_MIN_LENGTH
|
|
95
|
+
? `${body.provider.api_key.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${body.provider.api_key.slice(-API_KEY_PREVIEW_PREFIX_LEN)}`
|
|
96
|
+
: "****",
|
|
97
|
+
models: JSON.stringify(modelEntries),
|
|
98
|
+
is_active: 1,
|
|
99
|
+
max_concurrency: maxConcurrency,
|
|
100
|
+
queue_timeout_ms: queueTimeoutMs,
|
|
101
|
+
max_queue_size: maxQueueSize,
|
|
102
|
+
adaptive_enabled: adaptiveEnabled,
|
|
103
|
+
});
|
|
104
|
+
// 6. Upsert mapping groups
|
|
105
|
+
for (const m of body.mappings) {
|
|
106
|
+
const existing = db.prepare('SELECT id FROM mapping_groups WHERE client_model = ?').get(m.client_model);
|
|
107
|
+
const ruleJson = JSON.stringify({
|
|
108
|
+
targets: [{ backend_model: m.backend_model, provider_id: providerId }],
|
|
109
|
+
});
|
|
110
|
+
if (existing) {
|
|
111
|
+
updateMappingGroup(db, existing.id, {
|
|
112
|
+
client_model: m.client_model,
|
|
113
|
+
rule: ruleJson,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
createMappingGroup(db, {
|
|
118
|
+
client_model: m.client_model,
|
|
119
|
+
rule: ruleJson,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// 7. Create retry rules
|
|
124
|
+
for (const r of body.retry_rules) {
|
|
125
|
+
createRetryRule(db, {
|
|
126
|
+
name: r.name,
|
|
127
|
+
status_code: r.status_code,
|
|
128
|
+
body_pattern: r.body_pattern,
|
|
129
|
+
is_active: 1,
|
|
130
|
+
retry_strategy: r.retry_strategy,
|
|
131
|
+
retry_delay_ms: r.retry_delay_ms,
|
|
132
|
+
max_retries: r.max_retries,
|
|
133
|
+
max_delay_ms: r.max_delay_ms,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// 8. Create transform rules
|
|
137
|
+
if (body.transform_rules) {
|
|
138
|
+
upsertTransformRule(db, providerId, {
|
|
139
|
+
inject_headers: body.transform_rules.inject_headers ?? null,
|
|
140
|
+
request_defaults: body.transform_rules.request_defaults ?? null,
|
|
141
|
+
drop_fields: body.transform_rules.drop_fields ?? null,
|
|
142
|
+
is_active: 1,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return providerId;
|
|
146
|
+
});
|
|
147
|
+
// 8. Execute transaction
|
|
148
|
+
const providerId = createAll();
|
|
149
|
+
// 9. Sync concurrency state
|
|
150
|
+
const finalAdaptiveEnabled = body.provider.concurrency_mode === 'auto' ? 1 : 0;
|
|
151
|
+
const finalMaxConcurrency = body.provider.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency;
|
|
152
|
+
const finalQueueTimeoutMs = body.provider.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms;
|
|
153
|
+
const finalMaxQueueSize = body.provider.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size;
|
|
154
|
+
adaptiveController?.syncProvider(providerId, {
|
|
155
|
+
adaptive_enabled: finalAdaptiveEnabled,
|
|
156
|
+
max_concurrency: finalMaxConcurrency,
|
|
157
|
+
queue_timeout_ms: finalQueueTimeoutMs,
|
|
158
|
+
max_queue_size: finalMaxQueueSize,
|
|
159
|
+
});
|
|
160
|
+
tracker?.updateProviderConfig(providerId, {
|
|
161
|
+
name: body.provider.name,
|
|
162
|
+
maxConcurrency: finalMaxConcurrency,
|
|
163
|
+
queueTimeoutMs: finalQueueTimeoutMs,
|
|
164
|
+
maxQueueSize: finalMaxQueueSize,
|
|
165
|
+
});
|
|
166
|
+
return reply.code(HTTP_CREATED).send({ success: true, provider_id: providerId });
|
|
167
|
+
});
|
|
168
|
+
done();
|
|
169
|
+
};
|
|
@@ -7,7 +7,11 @@ export const adminRecommendedRoutes = (app, options, done) => {
|
|
|
7
7
|
app.get("/admin/api/recommended/retry-rules", async (_req, reply) => {
|
|
8
8
|
const rules = getRecommendedRetryRules();
|
|
9
9
|
const existing = new Set(db.prepare("SELECT name FROM retry_rules").all().map((r) => r.name));
|
|
10
|
-
|
|
10
|
+
// Return all rules with `exists` flag, so the frontend can show all and mark existing ones
|
|
11
|
+
return reply.send(rules.map(r => ({
|
|
12
|
+
...r,
|
|
13
|
+
exists: existing.has(r.name),
|
|
14
|
+
})));
|
|
11
15
|
});
|
|
12
16
|
app.post("/admin/api/recommended/reload", async (_req, reply) => {
|
|
13
17
|
reloadConfig();
|
package/dist/admin/routes.js
CHANGED
|
@@ -14,6 +14,7 @@ import { adminSettingsRoutes } from "./settings.js";
|
|
|
14
14
|
import { adminRecommendedRoutes } from "./recommended.js";
|
|
15
15
|
import { adminUsageRoutes } from "./usage.js";
|
|
16
16
|
import { adminUpgradeRoutes } from "./upgrade.js";
|
|
17
|
+
import { adminQuickSetupRoutes } from "./quick-setup.js";
|
|
17
18
|
import { adminImportExportRoutes } from "./settings-import-export.js";
|
|
18
19
|
import { adminTransformRuleRoutes } from "./transform-rules.js";
|
|
19
20
|
import { adminScheduleRoutes } from "./schedules.js";
|
|
@@ -37,6 +38,7 @@ export const adminRoutes = (app, options, done) => {
|
|
|
37
38
|
app.register(adminImportExportRoutes, { db: options.db, stateRegistry: options.stateRegistry, pluginRegistry: options.pluginRegistry });
|
|
38
39
|
app.register(adminRecommendedRoutes, { db: options.db });
|
|
39
40
|
app.register(adminUsageRoutes, { db: options.db });
|
|
41
|
+
app.register(adminQuickSetupRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController });
|
|
40
42
|
app.register(adminUpgradeRoutes, { db: options.db, closeFn: options.closeFn ?? (async () => { }) });
|
|
41
43
|
app.register(adminTransformRuleRoutes, { db: options.db, pluginRegistry: options.pluginRegistry });
|
|
42
44
|
done();
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
export interface ModelInfo {
|
|
2
2
|
name: string;
|
|
3
3
|
context_window: number | null;
|
|
4
|
+
patches: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface ModelEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
context_window?: number;
|
|
9
|
+
patches?: string[];
|
|
4
10
|
}
|
|
5
11
|
export declare const MODEL_CONTEXT_WINDOWS: Record<string, number>;
|
|
6
12
|
export declare const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
7
13
|
export declare const OVERFLOW_THRESHOLD = 1000000;
|
|
8
14
|
export declare function lookupContextWindow(modelName: string): number;
|
|
9
|
-
export declare function parseModels(raw: string):
|
|
10
|
-
export declare function buildModelInfoList(
|
|
15
|
+
export declare function parseModels(raw: string): ModelEntry[];
|
|
16
|
+
export declare function buildModelInfoList(modelEntries: ModelEntry[], overrides: Map<string, number>): ModelInfo[];
|
|
@@ -91,15 +91,27 @@ export function parseModels(raw) {
|
|
|
91
91
|
const parsed = JSON.parse(raw);
|
|
92
92
|
if (!Array.isArray(parsed))
|
|
93
93
|
return [];
|
|
94
|
-
return parsed.map((item) =>
|
|
94
|
+
return parsed.map((item) => {
|
|
95
|
+
if (typeof item === 'string') {
|
|
96
|
+
return item ? { name: item, patches: [] } : null;
|
|
97
|
+
}
|
|
98
|
+
const obj = item;
|
|
99
|
+
if (!obj || !obj.name)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
name: obj.name,
|
|
103
|
+
patches: obj.patches ?? [],
|
|
104
|
+
};
|
|
105
|
+
}).filter((e) => e !== null);
|
|
95
106
|
}
|
|
96
107
|
catch {
|
|
97
108
|
return [];
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
|
-
export function buildModelInfoList(
|
|
101
|
-
return
|
|
102
|
-
name,
|
|
103
|
-
context_window: overrides.get(name) ?? lookupContextWindow(name),
|
|
111
|
+
export function buildModelInfoList(modelEntries, overrides) {
|
|
112
|
+
return modelEntries.map(entry => ({
|
|
113
|
+
name: entry.name,
|
|
114
|
+
context_window: overrides.get(entry.name) ?? lookupContextWindow(entry.name),
|
|
115
|
+
patches: entry.patches ?? [],
|
|
104
116
|
}));
|
|
105
117
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface ProviderPreset {
|
|
2
2
|
plan: string;
|
|
3
3
|
presetName: string;
|
|
4
|
-
apiType: 'openai' | 'anthropic';
|
|
4
|
+
apiType: 'openai' | 'openai-responses' | 'anthropic';
|
|
5
5
|
baseUrl: string;
|
|
6
6
|
models: string[];
|
|
7
7
|
}
|
|
@@ -17,6 +17,7 @@ export interface RecommendedRetryRule {
|
|
|
17
17
|
retry_delay_ms: number;
|
|
18
18
|
max_retries: number;
|
|
19
19
|
max_delay_ms: number;
|
|
20
|
+
providers?: string[];
|
|
20
21
|
}
|
|
21
22
|
export declare function loadRecommendedConfig(dir?: string): void;
|
|
22
23
|
export declare function getRecommendedProviders(): ProviderGroup[];
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
let cachedProviders = [];
|
|
4
|
-
let cachedRetryRules = [];
|
|
5
3
|
let configDir = '';
|
|
6
4
|
export function loadRecommendedConfig(dir) {
|
|
7
5
|
configDir = dir ?? path.resolve(process.cwd(), 'config');
|
|
8
|
-
cachedProviders = loadJson('recommended-providers.json');
|
|
9
|
-
cachedRetryRules = loadJson('recommended-retry-rules.json');
|
|
10
6
|
}
|
|
11
7
|
export function getRecommendedProviders() {
|
|
12
|
-
return
|
|
8
|
+
return loadJson('recommended-providers.json');
|
|
13
9
|
}
|
|
14
10
|
export function getRecommendedRetryRules() {
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
export function reloadConfig() {
|
|
18
|
-
loadRecommendedConfig(configDir);
|
|
11
|
+
return loadJson('recommended-retry-rules.json');
|
|
19
12
|
}
|
|
13
|
+
// No-op: kept for backward compat (reload endpoint, upgrade flow)
|
|
14
|
+
// Config is now always read from disk, no caching.
|
|
15
|
+
export function reloadConfig() { }
|
|
20
16
|
function loadJson(filename) {
|
|
21
17
|
const filePath = path.join(configDir, filename);
|
|
22
18
|
try {
|
package/dist/core/constants.js
CHANGED
|
@@ -14,6 +14,8 @@ export const PROXY_API_TYPES = {
|
|
|
14
14
|
"/v1/chat/completions": "openai",
|
|
15
15
|
"/v1/models": "openai",
|
|
16
16
|
"/v1/messages": "anthropic",
|
|
17
|
+
"/v1/responses": "openai-responses",
|
|
18
|
+
"/responses": "openai-responses",
|
|
17
19
|
};
|
|
18
20
|
export function getProxyApiType(url) {
|
|
19
21
|
const path = url.split("?")[0];
|
package/dist/db/index.js
CHANGED
|
@@ -14,6 +14,11 @@ const MIGRATION_RENAMES = {
|
|
|
14
14
|
"028_convert_old_rule_format.sql": "029_convert_old_rule_format.sql",
|
|
15
15
|
"029_add_input_tokens_estimated.sql": "030_add_input_tokens_estimated.sql",
|
|
16
16
|
"030_add_tps_breakdown.sql": "031_add_tps_breakdown.sql",
|
|
17
|
+
// 消除双 033/034,重新编号 035→038
|
|
18
|
+
"033_add_pipeline_snapshot.sql": "033_add_adaptive_concurrency.sql",
|
|
19
|
+
"034_drop_redundant_log_columns.sql": "035_drop_redundant_log_columns.sql",
|
|
20
|
+
"035_add_openai_responses_api_type.sql": "036_add_openai_responses_api_type.sql",
|
|
21
|
+
"036_fix_035_data_corruption.sql": "037_fix_035_data_corruption.sql",
|
|
17
22
|
};
|
|
18
23
|
export function initDatabase(dbPath) {
|
|
19
24
|
if (dbPath !== ":memory:") {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
-- 033_add_adaptive_concurrency.sql
|
|
2
2
|
ALTER TABLE providers ADD COLUMN adaptive_enabled INTEGER NOT NULL DEFAULT 0;
|
|
3
3
|
ALTER TABLE providers ADD COLUMN adaptive_min INTEGER NOT NULL DEFAULT 1;
|
|
4
|
+
|
|
5
|
+
-- (merged from 033_add_pipeline_snapshot)
|
|
6
|
+
ALTER TABLE request_logs ADD COLUMN pipeline_snapshot TEXT;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
-- Expand api_type CHECK constraint to include 'openai-responses'
|
|
2
|
+
-- SQLite doesn't support ALTER TABLE ... ALTER CONSTRAINT, so we recreate the table.
|
|
3
|
+
-- We must temporarily drop referencing foreign key tables and recreate them after.
|
|
4
|
+
|
|
5
|
+
-- Note: This migration runs inside db.transaction() in the migration runner,
|
|
6
|
+
-- so we don't need our own BEGIN/COMMIT. PRAGMA foreign_keys doesn't work
|
|
7
|
+
-- inside transactions, so we handle FK tables explicitly instead.
|
|
8
|
+
|
|
9
|
+
-- Step 1: Save referencing table data as temp tables
|
|
10
|
+
CREATE TABLE IF NOT EXISTS _tmp_provider_model_info AS SELECT * FROM provider_model_info;
|
|
11
|
+
CREATE TABLE IF NOT EXISTS _tmp_provider_transform_rules AS SELECT * FROM provider_transform_rules;
|
|
12
|
+
|
|
13
|
+
-- Step 2: Drop referencing tables
|
|
14
|
+
DROP TABLE IF EXISTS provider_model_info;
|
|
15
|
+
DROP TABLE IF EXISTS provider_transform_rules;
|
|
16
|
+
|
|
17
|
+
-- Step 3: Recreate providers with expanded CHECK
|
|
18
|
+
CREATE TABLE providers_new (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
name TEXT NOT NULL UNIQUE,
|
|
21
|
+
api_type TEXT NOT NULL CHECK(api_type IN ('openai', 'openai-responses', 'anthropic')),
|
|
22
|
+
base_url TEXT NOT NULL,
|
|
23
|
+
api_key TEXT NOT NULL,
|
|
24
|
+
api_key_preview TEXT,
|
|
25
|
+
models TEXT NOT NULL DEFAULT '[]',
|
|
26
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
27
|
+
max_concurrency INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
queue_timeout_ms INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
max_queue_size INTEGER NOT NULL DEFAULT 100,
|
|
30
|
+
adaptive_enabled INTEGER NOT NULL DEFAULT 0,
|
|
31
|
+
adaptive_min INTEGER NOT NULL DEFAULT 1,
|
|
32
|
+
created_at TEXT NOT NULL,
|
|
33
|
+
updated_at TEXT NOT NULL
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
INSERT INTO providers_new (id, name, api_type, base_url, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled, adaptive_min, created_at, updated_at)
|
|
37
|
+
SELECT id, name, api_type, base_url, api_key, api_key_preview, models, is_active, max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled, adaptive_min, created_at, updated_at FROM providers;
|
|
38
|
+
DROP TABLE providers;
|
|
39
|
+
ALTER TABLE providers_new RENAME TO providers;
|
|
40
|
+
|
|
41
|
+
-- Step 4: Recreate referencing tables with their original schemas
|
|
42
|
+
CREATE TABLE provider_model_info (
|
|
43
|
+
provider_id TEXT NOT NULL,
|
|
44
|
+
model_name TEXT NOT NULL,
|
|
45
|
+
context_window INTEGER NOT NULL,
|
|
46
|
+
PRIMARY KEY (provider_id, model_name),
|
|
47
|
+
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS provider_transform_rules (
|
|
51
|
+
provider_id TEXT PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,
|
|
52
|
+
inject_headers TEXT,
|
|
53
|
+
request_defaults TEXT,
|
|
54
|
+
drop_fields TEXT,
|
|
55
|
+
field_overrides TEXT,
|
|
56
|
+
plugin_name TEXT,
|
|
57
|
+
is_active INTEGER DEFAULT 1,
|
|
58
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
59
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
-- Step 5: Restore data
|
|
63
|
+
INSERT INTO provider_model_info SELECT * FROM _tmp_provider_model_info;
|
|
64
|
+
INSERT OR IGNORE INTO provider_transform_rules SELECT * FROM _tmp_provider_transform_rules;
|
|
65
|
+
|
|
66
|
+
-- Step 6: Cleanup
|
|
67
|
+
DROP TABLE _tmp_provider_model_info;
|
|
68
|
+
DROP TABLE _tmp_provider_transform_rules;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- Fix data corruption caused by migration 036.
|
|
2
|
+
-- Migration 035 used `INSERT INTO providers_new SELECT * FROM providers`
|
|
3
|
+
-- which matches columns by position, not by name. The new table had a different
|
|
4
|
+
-- column order than the old table (where columns were added sequentially via
|
|
5
|
+
-- ALTER TABLE ADD COLUMN). This shifted every column from position 6 onward.
|
|
6
|
+
--
|
|
7
|
+
-- Old column order (via ALTER TABLE ADD COLUMN):
|
|
8
|
+
-- id, name, api_type, base_url, api_key, is_active, created_at, updated_at,
|
|
9
|
+
-- api_key_preview, models, max_concurrency, queue_timeout_ms, max_queue_size,
|
|
10
|
+
-- adaptive_enabled, adaptive_min
|
|
11
|
+
--
|
|
12
|
+
-- New column order (035):
|
|
13
|
+
-- id, name, api_type, base_url, api_key, api_key_preview, models, is_active,
|
|
14
|
+
-- max_concurrency, queue_timeout_ms, max_queue_size, adaptive_enabled,
|
|
15
|
+
-- adaptive_min, created_at, updated_at
|
|
16
|
+
--
|
|
17
|
+
-- Positional mapping of what actually went where:
|
|
18
|
+
-- old(6) is_active → new api_key_preview
|
|
19
|
+
-- old(7) created_at → new models
|
|
20
|
+
-- old(8) updated_at → new is_active
|
|
21
|
+
-- old(9) api_key_preview → new max_concurrency ← visible bug
|
|
22
|
+
-- old(10) models → new queue_timeout_ms
|
|
23
|
+
-- old(11) max_concurrency → new max_queue_size
|
|
24
|
+
-- old(12) queue_timeout_ms → new adaptive_enabled
|
|
25
|
+
-- old(13) max_queue_size → new adaptive_min
|
|
26
|
+
-- old(14) adaptive_enabled → new created_at
|
|
27
|
+
-- old(15) adaptive_min → new updated_at
|
|
28
|
+
--
|
|
29
|
+
-- Guard: only fixes rows where max_concurrency contains text data
|
|
30
|
+
-- (api_key_preview leaked into an INTEGER column). Providers created after
|
|
31
|
+
-- 035 have correct INTEGER values and are not affected.
|
|
32
|
+
|
|
33
|
+
-- Step 1: Snapshot current data before fixing
|
|
34
|
+
CREATE TABLE _m036_snapshot AS SELECT rowid, * FROM providers;
|
|
35
|
+
|
|
36
|
+
-- Step 2: Only fix rows where max_concurrency is text (corrupted by api_key_preview).
|
|
37
|
+
-- Each column reads from the snapshot position where the OLD value actually ended up.
|
|
38
|
+
UPDATE providers SET
|
|
39
|
+
api_key_preview = (SELECT max_concurrency FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
|
|
40
|
+
models = (SELECT queue_timeout_ms FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
|
|
41
|
+
is_active = (SELECT CAST(api_key_preview AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
|
|
42
|
+
max_concurrency = (SELECT CAST(max_queue_size AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
|
|
43
|
+
queue_timeout_ms = (SELECT CAST(adaptive_enabled AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
|
|
44
|
+
max_queue_size = (SELECT CAST(adaptive_min AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
|
|
45
|
+
adaptive_enabled = (SELECT CAST(created_at AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
|
|
46
|
+
adaptive_min = (SELECT CAST(updated_at AS INTEGER) FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
|
|
47
|
+
created_at = (SELECT models FROM _m036_snapshot s WHERE s.rowid = providers.rowid),
|
|
48
|
+
updated_at = (SELECT is_active FROM _m036_snapshot s WHERE s.rowid = providers.rowid)
|
|
49
|
+
WHERE typeof((
|
|
50
|
+
SELECT max_concurrency FROM _m036_snapshot s WHERE s.rowid = providers.rowid
|
|
51
|
+
)) = 'text';
|
|
52
|
+
|
|
53
|
+
-- Step 3: Cleanup
|
|
54
|
+
DROP TABLE _m036_snapshot;
|
package/dist/db/providers.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import Database from "better-sqlite3";
|
|
|
2
2
|
export interface Provider {
|
|
3
3
|
id: string;
|
|
4
4
|
name: string;
|
|
5
|
-
api_type: "openai" | "anthropic";
|
|
5
|
+
api_type: "openai" | "openai-responses" | "anthropic";
|
|
6
6
|
base_url: string;
|
|
7
7
|
api_key: string;
|
|
8
8
|
api_key_preview?: string;
|
|
@@ -20,12 +20,12 @@ export declare const PROVIDER_CONCURRENCY_DEFAULTS: {
|
|
|
20
20
|
readonly queue_timeout_ms: 0;
|
|
21
21
|
readonly max_queue_size: 100;
|
|
22
22
|
};
|
|
23
|
-
export declare function getActiveProviders(db: Database.Database, apiType: "openai" | "anthropic"): Provider[];
|
|
23
|
+
export declare function getActiveProviders(db: Database.Database, apiType: "openai" | "openai-responses" | "anthropic"): Provider[];
|
|
24
24
|
export declare function getAllProviders(db: Database.Database): Provider[];
|
|
25
25
|
export declare function getProviderById(db: Database.Database, id: string): Provider | undefined;
|
|
26
26
|
export declare function createProvider(db: Database.Database, provider: {
|
|
27
27
|
name: string;
|
|
28
|
-
api_type: "openai" | "anthropic";
|
|
28
|
+
api_type: "openai" | "openai-responses" | "anthropic";
|
|
29
29
|
base_url: string;
|
|
30
30
|
api_key: string;
|
|
31
31
|
api_key_preview?: string;
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { loadRecommendedConfig } from "./config/recommended.js";
|
|
|
17
17
|
import { authMiddleware } from "./middleware/auth.js";
|
|
18
18
|
import { openaiProxy } from "./proxy/handler/openai.js";
|
|
19
19
|
import { anthropicProxy } from "./proxy/handler/anthropic.js";
|
|
20
|
+
import { responsesProxy } from "./proxy/handler/responses.js";
|
|
20
21
|
import { adminRoutes } from "./admin/routes.js";
|
|
21
22
|
import { RetryRuleMatcher } from "./proxy/orchestration/retry-rules.js";
|
|
22
23
|
import { PluginRegistry } from "./proxy/transform/plugin-registry.js";
|
|
@@ -230,6 +231,7 @@ export async function buildApp(options) {
|
|
|
230
231
|
app.register(authMiddleware, { db });
|
|
231
232
|
app.register(openaiProxy, { db, container });
|
|
232
233
|
app.register(anthropicProxy, { db, container });
|
|
234
|
+
app.register(responsesProxy, { db, container });
|
|
233
235
|
// StateRegistry — Admin 层通过此接口触发 proxy 层状态刷新,消除 admin→proxy 依赖
|
|
234
236
|
const stateRegistry = {
|
|
235
237
|
refreshRetryRules: () => matcher.load(db),
|
|
@@ -344,13 +346,15 @@ export async function main() {
|
|
|
344
346
|
}
|
|
345
347
|
/* eslint-enable taste/no-silent-catch */
|
|
346
348
|
});
|
|
347
|
-
// 优雅关闭:SIGTERM
|
|
349
|
+
// 优雅关闭:SIGTERM 和 SIGINT(Ctrl+C)
|
|
350
|
+
// 首次 = 优雅关闭,再次 = 强制退出
|
|
348
351
|
let isShuttingDown = false;
|
|
349
352
|
const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
350
353
|
const shutdown = async (signal) => {
|
|
351
|
-
//
|
|
354
|
+
// 第二次收到信号 = 强制退出(Ctrl+C 卡住时用户可再按一次)
|
|
352
355
|
if (isShuttingDown) {
|
|
353
|
-
app.log.
|
|
356
|
+
app.log.warn(`Received ${signal} again, forcing exit`);
|
|
357
|
+
process.exit(1);
|
|
354
358
|
return;
|
|
355
359
|
}
|
|
356
360
|
isShuttingDown = true;
|
|
@@ -20,10 +20,11 @@ export declare class MetricsExtractor {
|
|
|
20
20
|
private textStreamStartTime;
|
|
21
21
|
private toolUseContentBuffer;
|
|
22
22
|
private toolUseStreamStartTime;
|
|
23
|
-
constructor(apiType: "openai" | "anthropic", requestStartTime: number);
|
|
23
|
+
constructor(apiType: "openai" | "openai-responses" | "anthropic", requestStartTime: number);
|
|
24
24
|
processEvent(event: SSEEvent): void;
|
|
25
25
|
getMetrics(): MetricsResult;
|
|
26
|
-
static fromNonStreamResponse(apiType: "openai" | "anthropic", responseBody: string): MetricsResult | null;
|
|
26
|
+
static fromNonStreamResponse(apiType: "openai" | "openai-responses" | "anthropic", responseBody: string): MetricsResult | null;
|
|
27
|
+
private processResponsesEvent;
|
|
27
28
|
private processAnthropicEvent;
|
|
28
29
|
private processOpenAIEvent;
|
|
29
30
|
}
|
|
@@ -32,6 +32,9 @@ export class MetricsExtractor {
|
|
|
32
32
|
if (this.apiType === "anthropic") {
|
|
33
33
|
this.processAnthropicEvent(event);
|
|
34
34
|
}
|
|
35
|
+
else if (this.apiType === "openai-responses") {
|
|
36
|
+
this.processResponsesEvent(event);
|
|
37
|
+
}
|
|
35
38
|
else {
|
|
36
39
|
this.processOpenAIEvent(event);
|
|
37
40
|
}
|
|
@@ -118,6 +121,48 @@ export class MetricsExtractor {
|
|
|
118
121
|
}
|
|
119
122
|
return extractAnthropicNonStream(parsed);
|
|
120
123
|
}
|
|
124
|
+
processResponsesEvent(event) {
|
|
125
|
+
let obj;
|
|
126
|
+
try {
|
|
127
|
+
obj = JSON.parse(event.data);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const type = obj.type;
|
|
133
|
+
// Track first content for TTFT
|
|
134
|
+
const isContentDelta = type === "response.output_text.delta"
|
|
135
|
+
|| type === "response.function_call_arguments.delta"
|
|
136
|
+
|| type === "response.reasoning_summary_text.delta";
|
|
137
|
+
if (isContentDelta) {
|
|
138
|
+
const delta = obj.delta ?? "";
|
|
139
|
+
if (delta && !this.firstContentReceived) {
|
|
140
|
+
this.firstContentReceived = true;
|
|
141
|
+
this.ttftMs = Date.now() - this.requestStartTime;
|
|
142
|
+
}
|
|
143
|
+
this.textContentBuffer += delta;
|
|
144
|
+
}
|
|
145
|
+
// Track completion
|
|
146
|
+
if (type === "response.completed" || type === "response.incomplete") {
|
|
147
|
+
this.streamEndTime = Date.now();
|
|
148
|
+
this.complete = true;
|
|
149
|
+
const resp = obj.response;
|
|
150
|
+
if (resp) {
|
|
151
|
+
const usage = resp.usage;
|
|
152
|
+
if (usage) {
|
|
153
|
+
this.inputTokens = usage.input_tokens ?? null;
|
|
154
|
+
this.outputTokens = usage.output_tokens ?? null;
|
|
155
|
+
}
|
|
156
|
+
const status = resp.status;
|
|
157
|
+
if (status === "completed")
|
|
158
|
+
this.stopReason = "end_turn";
|
|
159
|
+
else if (status === "incomplete")
|
|
160
|
+
this.stopReason = "max_tokens";
|
|
161
|
+
else
|
|
162
|
+
this.stopReason = "stop";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
121
166
|
processAnthropicEvent(event) {
|
|
122
167
|
let parsed;
|
|
123
168
|
try {
|
|
@@ -27,7 +27,7 @@ export declare class SSEMetricsTransform extends Transform {
|
|
|
27
27
|
private throttleMs;
|
|
28
28
|
private lastCallbackTime;
|
|
29
29
|
private flushed;
|
|
30
|
-
constructor(apiType: "openai" | "anthropic", requestStartTime: number, options?: MetricsTransformOptions);
|
|
30
|
+
constructor(apiType: "openai" | "openai-responses" | "anthropic", requestStartTime: number, options?: MetricsTransformOptions);
|
|
31
31
|
_transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void;
|
|
32
32
|
_flush(callback: TransformCallback): void;
|
|
33
33
|
getExtractor(): MetricsExtractor;
|
|
@@ -83,6 +83,16 @@ export class SSEMetricsTransform extends Transform {
|
|
|
83
83
|
if (delta.type === "input_json_delta" && typeof delta.partial_json === "string")
|
|
84
84
|
return delta.partial_json;
|
|
85
85
|
}
|
|
86
|
+
else if (this.apiType === "openai-responses") {
|
|
87
|
+
const type = parsed.type;
|
|
88
|
+
if (type === "response.output_text.delta")
|
|
89
|
+
return parsed.delta;
|
|
90
|
+
if (type === "response.reasoning_summary_text.delta")
|
|
91
|
+
return parsed.delta;
|
|
92
|
+
if (type === "response.function_call_arguments.delta")
|
|
93
|
+
return parsed.delta;
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
86
96
|
else {
|
|
87
97
|
const choices = parsed.choices;
|
|
88
98
|
if (!Array.isArray(choices) || choices.length === 0)
|
|
@@ -34,7 +34,7 @@ export declare class RequestTracker {
|
|
|
34
34
|
private scheduleStreamContentPush;
|
|
35
35
|
private flushStreamContentPush;
|
|
36
36
|
update(id: string, patch: Partial<ActiveRequest>): void;
|
|
37
|
-
appendStreamChunk(id: string, rawLine: string, apiType: "openai" | "anthropic", maxRaw: number, maxText: number): void;
|
|
37
|
+
appendStreamChunk(id: string, rawLine: string, apiType: "openai" | "openai-responses" | "anthropic", maxRaw: number, maxText: number): void;
|
|
38
38
|
complete(id: string, result: {
|
|
39
39
|
status: "completed" | "failed";
|
|
40
40
|
statusCode?: number;
|
|
@@ -9,6 +9,6 @@ export declare class StreamContentAccumulator {
|
|
|
9
9
|
private totalChars;
|
|
10
10
|
private blocks;
|
|
11
11
|
constructor(maxRaw?: number, maxText?: number);
|
|
12
|
-
append(rawLine: string, apiType: "openai" | "anthropic"): void;
|
|
12
|
+
append(rawLine: string, apiType: "openai" | "openai-responses" | "anthropic"): void;
|
|
13
13
|
getSnapshot(): StreamContentSnapshot;
|
|
14
14
|
}
|
|
@@ -8,4 +8,4 @@ export interface StreamExtraction {
|
|
|
8
8
|
name?: string;
|
|
9
9
|
} | null;
|
|
10
10
|
}
|
|
11
|
-
export declare function extractStreamText(line: string, apiType: "openai" | "anthropic"): StreamExtraction;
|
|
11
|
+
export declare function extractStreamText(line: string, apiType: "openai" | "openai-responses" | "anthropic"): StreamExtraction;
|