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
|
@@ -19,6 +19,27 @@ export function extractStreamText(line, apiType) {
|
|
|
19
19
|
const text = delta?.content ?? "";
|
|
20
20
|
return { text, block: text ? { index: 0, type: "text", content: text } : null };
|
|
21
21
|
}
|
|
22
|
+
if (apiType === "openai-responses") {
|
|
23
|
+
// Responses SSE uses named events, but line format is "data: {json}" (same as Anthropic)
|
|
24
|
+
// The event type is in the data JSON's "type" field
|
|
25
|
+
const type = obj.type;
|
|
26
|
+
if (type === "response.output_text.delta") {
|
|
27
|
+
const text = obj.delta ?? "";
|
|
28
|
+
const outputIndex = obj.output_index ?? 0;
|
|
29
|
+
return { text, block: text ? { index: outputIndex, type: "text", content: text } : empty.block };
|
|
30
|
+
}
|
|
31
|
+
if (type === "response.function_call_arguments.delta") {
|
|
32
|
+
const partialJson = obj.delta ?? "";
|
|
33
|
+
const outputIndex = obj.output_index ?? 0;
|
|
34
|
+
return { text: "", block: { index: outputIndex, type: "tool_use", content: partialJson } };
|
|
35
|
+
}
|
|
36
|
+
if (type === "response.reasoning_summary_text.delta") {
|
|
37
|
+
const thinking = obj.delta ?? "";
|
|
38
|
+
const outputIndex = obj.output_index ?? 0;
|
|
39
|
+
return { text: "", block: { index: outputIndex, type: "thinking", content: thinking } };
|
|
40
|
+
}
|
|
41
|
+
return empty;
|
|
42
|
+
}
|
|
22
43
|
// Anthropic
|
|
23
44
|
const type = obj.type;
|
|
24
45
|
const index = obj.index;
|
package/dist/monitor/types.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ import type { TransportResult } from "../types.js";
|
|
|
4
4
|
/** 从 TransportResult 中提取最终 HTTP status code */
|
|
5
5
|
export declare function getTransportStatusCode(result: TransportResult): number | null;
|
|
6
6
|
/** 将 tracker blocks 序列化为前端 tryDirectParse 可解析的 JSON */
|
|
7
|
-
export declare function serializeBlocksForStorage(blocks: ContentBlock[] | undefined, apiType: "openai" | "anthropic"): string;
|
|
7
|
+
export declare function serializeBlocksForStorage(blocks: ContentBlock[] | undefined, apiType: "openai" | "openai-responses" | "anthropic"): string;
|
|
8
8
|
/** 从请求体中提取最后一次工具调用记录 */
|
|
9
9
|
export declare function extractLastToolUse(body: Record<string, unknown>): ToolCallRecord | null;
|
|
@@ -8,6 +8,6 @@ export interface RouteHandlerDeps {
|
|
|
8
8
|
container: ServiceContainer;
|
|
9
9
|
}
|
|
10
10
|
import type { ServiceContainer } from "../../core/container.js";
|
|
11
|
-
export declare function handleProxyRequest(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "anthropic", upstreamPath: string, errors: ProxyErrorFormatter, deps: RouteHandlerDeps, options?: {
|
|
11
|
+
export declare function handleProxyRequest(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "openai-responses" | "anthropic", upstreamPath: string, errors: ProxyErrorFormatter, deps: RouteHandlerDeps, options?: {
|
|
12
12
|
beforeSendProxy?: (body: Record<string, unknown>, isStream: boolean) => void;
|
|
13
13
|
}): Promise<FastifyReply>;
|
|
@@ -13,6 +13,7 @@ import { insertRejectedLog } from "../log-helpers.js";
|
|
|
13
13
|
import { ToolLoopGuard } from "../loop-prevention/tool-loop-guard.js";
|
|
14
14
|
import { buildTransportFn } from "../transport/transport-fn.js";
|
|
15
15
|
import { applyOverflowRedirect } from "../routing/overflow.js";
|
|
16
|
+
import { parseModels } from "../../config/model-context.js";
|
|
16
17
|
import { applyProviderPatches } from "../patch/index.js";
|
|
17
18
|
import { PipelineSnapshot } from "../pipeline-snapshot.js";
|
|
18
19
|
import { maybeInjectModelInfoTag } from "../response-transform.js";
|
|
@@ -223,8 +224,13 @@ async function executeFailoverLoop(ctx) {
|
|
|
223
224
|
pluginRegistry.applyAfterRequest(pluginCtx);
|
|
224
225
|
injectedHeaders = pluginCtx.headers;
|
|
225
226
|
}
|
|
226
|
-
// provider patches —
|
|
227
|
-
const
|
|
227
|
+
// provider patches — 优先从 DB models JSON 读取 patch 配置,无配置时回退自动检测
|
|
228
|
+
const providerModels = parseModels(provider.models || "[]");
|
|
229
|
+
const { body: patchedBody, meta: patchMeta } = applyProviderPatches(currentBody, {
|
|
230
|
+
base_url: provider.base_url,
|
|
231
|
+
api_type: provider.api_type,
|
|
232
|
+
models: providerModels,
|
|
233
|
+
});
|
|
228
234
|
iterationSnapshot.add({ stage: "provider_patch", types: patchMeta.types });
|
|
229
235
|
const encryptionKey = getSetting(deps.db, "encryption_key");
|
|
230
236
|
if (!encryptionKey) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FastifyPluginCallback } from "fastify";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
export interface ResponsesProxyOptions {
|
|
4
|
+
db: Database.Database;
|
|
5
|
+
container: import("../../core/container.js").ServiceContainer;
|
|
6
|
+
}
|
|
7
|
+
export declare const responsesProxy: FastifyPluginCallback<ResponsesProxyOptions>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import fp from "fastify-plugin";
|
|
3
|
+
import { insertRequestLog } from "../../db/index.js";
|
|
4
|
+
import { createErrorFormatter } from "../proxy-core.js";
|
|
5
|
+
import { handleProxyRequest } from "./proxy-handler.js";
|
|
6
|
+
import { createOrchestrator } from "../orchestration/orchestrator.js";
|
|
7
|
+
import { HTTP_BAD_GATEWAY } from "../../core/constants.js";
|
|
8
|
+
import { SERVICE_KEYS } from "../../core/container.js";
|
|
9
|
+
const RESPONSES_PATH = "/v1/responses";
|
|
10
|
+
const RESPONSES_COMPAT_PATH = "/responses";
|
|
11
|
+
const RESPONSES_ERROR_META = {
|
|
12
|
+
modelNotFound: { type: "invalid_request_error", code: "model_not_found" },
|
|
13
|
+
modelNotAllowed: { type: "invalid_request_error", code: "model_not_allowed" },
|
|
14
|
+
providerUnavailable: { type: "server_error", code: "provider_unavailable" },
|
|
15
|
+
providerTypeMismatch: { type: "server_error", code: "provider_type_mismatch" },
|
|
16
|
+
upstreamConnectionFailed: { type: "upstream_error", code: "upstream_connection_failed" },
|
|
17
|
+
concurrencyQueueFull: { type: "server_error", code: "concurrency_queue_full" },
|
|
18
|
+
concurrencyTimeout: { type: "server_error", code: "concurrency_timeout" },
|
|
19
|
+
promptTooLong: { type: "invalid_request_error", code: "context_window_exceeded" },
|
|
20
|
+
};
|
|
21
|
+
const responsesErrors = createErrorFormatter((kind, message) => ({ error: { message, ...RESPONSES_ERROR_META[kind] } }));
|
|
22
|
+
function sendError(reply, e) {
|
|
23
|
+
return reply.code(e.statusCode).send(e.body);
|
|
24
|
+
}
|
|
25
|
+
const responsesProxyRaw = (app, opts, done) => {
|
|
26
|
+
const { db, container } = opts;
|
|
27
|
+
const orchestrator = createOrchestrator(container.resolve(SERVICE_KEYS.semaphoreManager), container.resolve(SERVICE_KEYS.tracker), container.resolve(SERVICE_KEYS.adaptiveController));
|
|
28
|
+
const handleResponses = async (request, reply) => {
|
|
29
|
+
if (!orchestrator) {
|
|
30
|
+
const body = request.body;
|
|
31
|
+
insertRequestLog(db, {
|
|
32
|
+
id: randomUUID(), api_type: "openai-responses", model: body?.model || null,
|
|
33
|
+
provider_id: null, status_code: HTTP_BAD_GATEWAY, latency_ms: 0, is_stream: 0,
|
|
34
|
+
error_message: "Orchestrator not available",
|
|
35
|
+
created_at: new Date().toISOString(),
|
|
36
|
+
client_request: JSON.stringify({ headers: request.headers }),
|
|
37
|
+
router_key_id: request.routerKey?.id ?? null,
|
|
38
|
+
});
|
|
39
|
+
return sendError(reply, responsesErrors.providerUnavailable());
|
|
40
|
+
}
|
|
41
|
+
const deps = { db, orchestrator, container };
|
|
42
|
+
return handleProxyRequest(request, reply, "openai-responses", RESPONSES_PATH, responsesErrors, deps);
|
|
43
|
+
};
|
|
44
|
+
app.post(RESPONSES_PATH, handleResponses);
|
|
45
|
+
app.post(RESPONSES_COMPAT_PATH, handleResponses);
|
|
46
|
+
done();
|
|
47
|
+
};
|
|
48
|
+
export const responsesProxy = fp(responsesProxyRaw, { name: "responses-proxy" });
|
|
@@ -9,5 +9,5 @@ export declare class ToolLoopGuard {
|
|
|
9
9
|
* 如果 sessionKey 不可用(无 sessionId),返回 detected: false。
|
|
10
10
|
*/
|
|
11
11
|
check(sessionKey: string | null, toolCall: ToolCallRecord | null): LoopCheckResult;
|
|
12
|
-
injectLoopBreakPrompt(body: Record<string, unknown>, apiType: "openai" | "anthropic", toolName: string): Record<string, unknown>;
|
|
12
|
+
injectLoopBreakPrompt(body: Record<string, unknown>, apiType: "openai" | "openai-responses" | "anthropic", toolName: string): Record<string, unknown>;
|
|
13
13
|
}
|
|
@@ -50,6 +50,16 @@ export class ToolLoopGuard {
|
|
|
50
50
|
cloned.system = [{ type: "text", text: prompt }];
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
else if (apiType === "openai-responses") {
|
|
54
|
+
// Append a user message to input items
|
|
55
|
+
const inputArr = Array.isArray(body.input) ? [...body.input] : [];
|
|
56
|
+
inputArr.push({
|
|
57
|
+
type: "message",
|
|
58
|
+
role: "user",
|
|
59
|
+
content: [{ type: "input_text", text: `[系统提醒] 检测到工具 "${toolName}" 可能陷入循环。请停止重复调用,总结当前进展。` }],
|
|
60
|
+
});
|
|
61
|
+
return { ...body, input: inputArr };
|
|
62
|
+
}
|
|
53
63
|
else {
|
|
54
64
|
const messages = cloned.messages ?? [];
|
|
55
65
|
messages.unshift({ role: "system", content: prompt });
|
|
@@ -52,7 +52,7 @@ export declare class ProxyOrchestrator {
|
|
|
52
52
|
resilience: ResilienceLayer;
|
|
53
53
|
adaptiveController?: AdaptiveConcurrencyController;
|
|
54
54
|
});
|
|
55
|
-
handle(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "anthropic", config: OrchestratorConfig, ctx?: HandleContext): Promise<ResilienceResult>;
|
|
55
|
+
handle(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "openai-responses" | "anthropic", config: OrchestratorConfig, ctx?: HandleContext): Promise<ResilienceResult>;
|
|
56
56
|
private buildActiveRequest;
|
|
57
57
|
private createAbortSignal;
|
|
58
58
|
private executeResilience;
|
|
@@ -34,6 +34,12 @@ export class ProviderSemaphoreManager {
|
|
|
34
34
|
}
|
|
35
35
|
if (entry.current < 0)
|
|
36
36
|
entry.current = 0;
|
|
37
|
+
// 当 maxConcurrency 降低时,将 current 限制在新的上限,
|
|
38
|
+
// 防止 current 停留在旧的高位,导致新请求始终进入队列、
|
|
39
|
+
// 且 release() 无法通过空队检查来递减 current(排队长驻)
|
|
40
|
+
if (entry.current > config.maxConcurrency) {
|
|
41
|
+
entry.current = config.maxConcurrency;
|
|
42
|
+
}
|
|
37
43
|
while (entry.current < config.maxConcurrency &&
|
|
38
44
|
entry.queue.length > 0) {
|
|
39
45
|
entry.current++;
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
* 1. patchNonDeepSeekToolMessages — 将非 DeepSeek 生成的 tool_calls 降级为 text
|
|
16
16
|
* 2. patchOrphanToolResultsOA — 处理孤儿 tool 消息
|
|
17
17
|
*/
|
|
18
|
-
export declare function applyDeepSeekPatches(body: Record<string, unknown>, apiType: "openai" | "anthropic"): void;
|
|
18
|
+
export declare function applyDeepSeekPatches(body: Record<string, unknown>, apiType: "openai" | "openai-responses" | "anthropic"): void;
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* 客户端(如 Claude Code)可能在后续轮次省略此参数。
|
|
4
4
|
* 检测历史中是否存在 thinking 内容,自动补上参数。
|
|
5
5
|
*/
|
|
6
|
-
export declare function patchThinkingParam(body: Record<string, unknown>, apiType: "openai" | "anthropic"): void;
|
|
6
|
+
export declare function patchThinkingParam(body: Record<string, unknown>, apiType: "openai" | "openai-responses" | "anthropic"): void;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import type { ModelEntry } from "../../config/model-context.js";
|
|
1
2
|
export interface ProviderInfo {
|
|
2
3
|
base_url: string;
|
|
3
4
|
api_type: string;
|
|
5
|
+
models?: ModelEntry[];
|
|
4
6
|
}
|
|
5
7
|
export interface ProviderPatchMeta {
|
|
6
8
|
types: string[];
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* 根据 provider 信息分发到对应的补丁逻辑。
|
|
12
|
+
* 优先使用 DB 配置的 patches 模式,无配置时回退到自动检测。
|
|
10
13
|
* 返回浅拷贝 body + 执行的补丁类型列表,不修改原始 body。
|
|
11
14
|
*/
|
|
12
15
|
export declare function applyProviderPatches(body: Record<string, unknown>, provider: ProviderInfo): {
|
|
@@ -2,6 +2,7 @@ import { applyDeepSeekPatches } from "./deepseek/index.js";
|
|
|
2
2
|
const OPENAI_ORIGIN_HOSTS = ["api.openai.com", "openai.com"];
|
|
3
3
|
/**
|
|
4
4
|
* 根据 provider 信息分发到对应的补丁逻辑。
|
|
5
|
+
* 优先使用 DB 配置的 patches 模式,无配置时回退到自动检测。
|
|
5
6
|
* 返回浅拷贝 body + 执行的补丁类型列表,不修改原始 body。
|
|
6
7
|
*/
|
|
7
8
|
export function applyProviderPatches(body, provider) {
|
|
@@ -15,6 +16,33 @@ export function applyProviderPatches(body, provider) {
|
|
|
15
16
|
}
|
|
16
17
|
return patched;
|
|
17
18
|
};
|
|
19
|
+
// ---- DB-driven mode:通过 provider.models 配置的 patches 驱动 ----
|
|
20
|
+
if (provider.models) {
|
|
21
|
+
const modelName = body.model ?? "";
|
|
22
|
+
const modelEntry = provider.models.find(m => m.name === modelName);
|
|
23
|
+
const modelPatches = modelEntry?.patches ?? [];
|
|
24
|
+
if (modelPatches.length > 0) {
|
|
25
|
+
// developer_role 补丁(仅 openai 格式需要)
|
|
26
|
+
if (modelPatches.includes("developer_role") && provider.api_type === "openai" && hasDeveloperRole(body)) {
|
|
27
|
+
patchDeveloperRole(ensureCloned());
|
|
28
|
+
patches.push("developer_role");
|
|
29
|
+
}
|
|
30
|
+
// DeepSeek Anthropic 补丁
|
|
31
|
+
const dsAnthropicPatches = ["thinking-param", "cache-control", "thinking-blocks", "orphan-tool-results"];
|
|
32
|
+
if (dsAnthropicPatches.some(p => modelPatches.includes(p)) && provider.api_type === "anthropic") {
|
|
33
|
+
applyDeepSeekPatches(ensureCloned(), "anthropic");
|
|
34
|
+
patches.push("deepseek");
|
|
35
|
+
}
|
|
36
|
+
// DeepSeek OpenAI 补丁
|
|
37
|
+
const dsOpenAIPatches = ["non-ds-tools", "orphan-tool-results-oa"];
|
|
38
|
+
if (dsOpenAIPatches.some(p => modelPatches.includes(p)) && provider.api_type === "openai") {
|
|
39
|
+
applyDeepSeekPatches(ensureCloned(), "openai");
|
|
40
|
+
patches.push("deepseek");
|
|
41
|
+
}
|
|
42
|
+
return { body: patched ?? body, meta: { types: patches } };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ---- 回退模式:自动检测(保持现有逻辑不变)----
|
|
18
46
|
// 通用补丁:OpenAI 兼容 provider(非 OpenAI 原生)不支持 developer role
|
|
19
47
|
if (provider.api_type === "openai" && !isOpenAIOrigin(provider.base_url)) {
|
|
20
48
|
if (hasDeveloperRole(body)) {
|
|
@@ -30,7 +30,7 @@ export declare function countConsecutiveToolRounds(messages: Message[]): number;
|
|
|
30
30
|
/**
|
|
31
31
|
* 检测并注入提示词。返回可能修改后的 body(浅拷贝),未超阈值时原样返回。
|
|
32
32
|
*/
|
|
33
|
-
export declare function applyToolRoundLimit(body: Record<string, unknown>, apiType: "openai" | "anthropic", maxRounds?: number): {
|
|
33
|
+
export declare function applyToolRoundLimit(body: Record<string, unknown>, apiType: "openai" | "openai-responses" | "anthropic", maxRounds?: number): {
|
|
34
34
|
body: Record<string, unknown>;
|
|
35
35
|
injected: boolean;
|
|
36
36
|
rounds: number;
|
|
@@ -50,6 +50,22 @@ export function countConsecutiveToolRounds(messages) {
|
|
|
50
50
|
* 检测并注入提示词。返回可能修改后的 body(浅拷贝),未超阈值时原样返回。
|
|
51
51
|
*/
|
|
52
52
|
export function applyToolRoundLimit(body, apiType, maxRounds = DEFAULT_MAX_ROUNDS) {
|
|
53
|
+
if (apiType === "openai-responses") {
|
|
54
|
+
const input = body.input;
|
|
55
|
+
if (!input || !Array.isArray(input))
|
|
56
|
+
return { body, injected: false, rounds: 0 };
|
|
57
|
+
const funcCalls = input.filter(i => i.type === "function_call").length;
|
|
58
|
+
if (funcCalls <= maxRounds)
|
|
59
|
+
return { body, injected: false, rounds: funcCalls };
|
|
60
|
+
// Inject warning: append a user message to input
|
|
61
|
+
const cloned = { ...body, input: [...input] };
|
|
62
|
+
cloned.input.push({
|
|
63
|
+
type: "message",
|
|
64
|
+
role: "user",
|
|
65
|
+
content: [{ type: "input_text", text: LOOP_WARNING_PROMPT }],
|
|
66
|
+
});
|
|
67
|
+
return { body: cloned, injected: true, rounds: funcCalls };
|
|
68
|
+
}
|
|
53
69
|
const messages = body.messages ?? [];
|
|
54
70
|
if (messages.length === 0)
|
|
55
71
|
return { body, injected: false, rounds: 0 };
|
|
@@ -30,5 +30,5 @@ export declare function createErrorFormatter(formatBody: (kind: ErrorKind, messa
|
|
|
30
30
|
export declare function buildUpstreamUrl(baseUrl: string, upstreamPath: string): string;
|
|
31
31
|
export declare const SKIP_UPSTREAM: Set<string>;
|
|
32
32
|
export declare function selectHeaders(raw: RawHeaders, skip: Set<string>): Record<string, string>;
|
|
33
|
-
export declare function buildUpstreamHeaders(clientHeaders: RawHeaders, apiKey: string, payloadBytes?: number, apiType?: "openai" | "anthropic"): Record<string, string>;
|
|
33
|
+
export declare function buildUpstreamHeaders(clientHeaders: RawHeaders, apiKey: string, payloadBytes?: number, apiType?: "openai" | "openai-responses" | "anthropic"): Record<string, string>;
|
|
34
34
|
export declare function proxyGetRequest(backend: Provider, apiKey: string, clientHeaders: RawHeaders, upstreamPath: string): Promise<GetTransportResult>;
|
|
@@ -6,7 +6,7 @@ import type { ResilienceAttempt } from "../core/types.js";
|
|
|
6
6
|
import type { TransportResult } from "./types.js";
|
|
7
7
|
/** 日志存储前脱敏 Authorization / x-api-key header,避免 API Key 被持久化 */
|
|
8
8
|
export declare function sanitizeHeadersForLog(headers: Record<string, string>): Record<string, string>;
|
|
9
|
-
export declare function handleIntercept(db: Database.Database, apiType: "openai" | "anthropic", request: FastifyRequest, reply: import("fastify").FastifyReply, interceptResponse: {
|
|
9
|
+
export declare function handleIntercept(db: Database.Database, apiType: "openai" | "openai-responses" | "anthropic", request: FastifyRequest, reply: import("fastify").FastifyReply, interceptResponse: {
|
|
10
10
|
statusCode: number;
|
|
11
11
|
body: unknown;
|
|
12
12
|
meta?: unknown;
|
|
@@ -14,7 +14,7 @@ export declare function handleIntercept(db: Database.Database, apiType: "openai"
|
|
|
14
14
|
test: (statusCode: number, body: string) => boolean;
|
|
15
15
|
} | null, logFileWriter?: LogFileWriter | null): import("fastify").FastifyReply;
|
|
16
16
|
export declare function logResilienceResult(db: Database.Database, params: {
|
|
17
|
-
apiType: "openai" | "anthropic";
|
|
17
|
+
apiType: "openai" | "openai-responses" | "anthropic";
|
|
18
18
|
model: string;
|
|
19
19
|
providerId: string;
|
|
20
20
|
isStream: boolean;
|
|
@@ -31,4 +31,4 @@ export declare function logResilienceResult(db: Database.Database, params: {
|
|
|
31
31
|
} | null;
|
|
32
32
|
logFileWriter?: LogFileWriter | null;
|
|
33
33
|
}, attempts: ResilienceAttempt[], result: TransportResult, startTime: number): string;
|
|
34
|
-
export declare function collectTransportMetrics(db: Database.Database, apiType: "openai" | "anthropic", result: TransportResult, isStream: boolean, lastSuccessLogId: string, providerId: string, backendModel: string, request: FastifyRequest, routerKeyId?: string | null, statusCode?: number | null): void;
|
|
34
|
+
export declare function collectTransportMetrics(db: Database.Database, apiType: "openai" | "openai-responses" | "anthropic", result: TransportResult, isStream: boolean, lastSuccessLogId: string, providerId: string, backendModel: string, request: FastifyRequest, routerKeyId?: string | null, statusCode?: number | null): void;
|
|
@@ -9,6 +9,19 @@ export function maybeInjectModelInfoTag(responseBody, originalModel, effectiveMo
|
|
|
9
9
|
bodyObj.content[0].text += `\n\n${buildModelInfoTag(effectiveModel)}`;
|
|
10
10
|
return { body: JSON.stringify(bodyObj), meta: { model_info_tag_injected: true } };
|
|
11
11
|
}
|
|
12
|
+
// Responses format: output[type=message].content[type=output_text].text
|
|
13
|
+
if (Array.isArray(bodyObj.output)) {
|
|
14
|
+
for (const item of bodyObj.output) {
|
|
15
|
+
if (item.type === "message" && Array.isArray(item.content)) {
|
|
16
|
+
for (const part of item.content) {
|
|
17
|
+
if (part.type === "output_text" && part.text) {
|
|
18
|
+
part.text = part.text + `\n\n${buildModelInfoTag(effectiveModel)}`;
|
|
19
|
+
return { body: JSON.stringify(bodyObj), meta: { model_info_tag_injected: true } };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
12
25
|
}
|
|
13
26
|
catch { /* non-JSON response, skip injection */ }
|
|
14
27
|
return { body: responseBody, meta: { model_info_tag_injected: false } };
|
|
@@ -6,4 +6,7 @@ export function generateMsgId() {
|
|
|
6
6
|
export function generateChatcmplId() {
|
|
7
7
|
return `chatcmpl-${randomUUID().slice(0, UUID_ID_LENGTH)}`;
|
|
8
8
|
}
|
|
9
|
+
export function generateRespId() {
|
|
10
|
+
return `resp_${randomUUID().slice(0, UUID_ID_LENGTH)}`;
|
|
11
|
+
}
|
|
9
12
|
export const MS_PER_SECOND = 1000;
|
|
@@ -5,13 +5,13 @@ export interface PluginMatch {
|
|
|
5
5
|
providerId?: string;
|
|
6
6
|
providerName?: string;
|
|
7
7
|
providerNamePattern?: string;
|
|
8
|
-
apiType?: "openai" | "anthropic";
|
|
8
|
+
apiType?: "openai" | "openai-responses" | "anthropic";
|
|
9
9
|
}
|
|
10
10
|
export interface RequestTransformContext {
|
|
11
11
|
body: Record<string, unknown>;
|
|
12
12
|
headers: Record<string, string>;
|
|
13
|
-
sourceApiType: "openai" | "anthropic";
|
|
14
|
-
targetApiType: "openai" | "anthropic";
|
|
13
|
+
sourceApiType: "openai" | "openai-responses" | "anthropic";
|
|
14
|
+
targetApiType: "openai" | "openai-responses" | "anthropic";
|
|
15
15
|
provider: {
|
|
16
16
|
id: string;
|
|
17
17
|
name: string;
|
|
@@ -21,8 +21,8 @@ export interface RequestTransformContext {
|
|
|
21
21
|
}
|
|
22
22
|
export interface ResponseTransformContext {
|
|
23
23
|
response: Record<string, unknown>;
|
|
24
|
-
sourceApiType: "openai" | "anthropic";
|
|
25
|
-
targetApiType: "openai" | "anthropic";
|
|
24
|
+
sourceApiType: "openai" | "openai-responses" | "anthropic";
|
|
25
|
+
targetApiType: "openai" | "openai-responses" | "anthropic";
|
|
26
26
|
provider: {
|
|
27
27
|
id: string;
|
|
28
28
|
name: string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge (lossy) request transformation between OpenAI Responses API
|
|
3
|
+
* and OpenAI Chat Completions API.
|
|
4
|
+
*
|
|
5
|
+
* This is the SECONDARY conversion path used when the upstream provider
|
|
6
|
+
* only supports the opposite API format. It is lossy because Chat Completions
|
|
7
|
+
* cannot represent `previous_response_id`, built-in tools, or structured
|
|
8
|
+
* reasoning items.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Convert an OpenAI Responses API request body to an OpenAI Chat Completions
|
|
12
|
+
* request body.
|
|
13
|
+
*/
|
|
14
|
+
export declare function responsesToChatRequest(body: Record<string, unknown>): Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Convert an OpenAI Chat Completions request body to an OpenAI Responses API
|
|
17
|
+
* request body.
|
|
18
|
+
*/
|
|
19
|
+
export declare function chatToResponsesRequest(body: Record<string, unknown>): Record<string, unknown>;
|