llm-simple-router 0.9.0 → 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/dist/admin/quick-setup.js +1 -1
- package/dist/config/recommended.d.ts +1 -1
- 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/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/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-RyuwzbNx.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-CpT5q-jI.js → badge-C-9zPTgw.js} +1 -1
- 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/{copy-CIHn6HDL.js → copy-DWG9cQPR.js} +1 -1
- 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-LfP0_JRK.js → file-text-D33FJAPX.js} +1 -1
- 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-n4cC12ho.js → sun-BylRZIWt.js} +1 -1
- package/frontend-dist/assets/{trash-2-oDWBOuqK.js → trash-2-QNFff7V4.js} +1 -1
- package/frontend-dist/assets/{useClipboard-C2i7YvJ-.js → useClipboard-BFt5f-_-.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-DORIgNd9.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-BFhy-DAX.js → useTheme-CpTI547G.js} +1 -1
- package/frontend-dist/assets/x-DSgLgKC_.js +1 -0
- package/frontend-dist/index.html +25 -24
- package/package.json +1 -1
- package/dist/db/migrations/033_add_pipeline_snapshot.sql +0 -1
- package/frontend-dist/assets/CardContent-F3K9pZNP.js +0 -1
- package/frontend-dist/assets/CardTitle-13anASyk.js +0 -1
- package/frontend-dist/assets/CascadingModelSelect-BmW89GUP.js +0 -1
- package/frontend-dist/assets/Checkbox-C2oSHNgP.js +0 -1
- package/frontend-dist/assets/CollapsibleContent-CdeCo0Ko.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-CMd4wTNY.js +0 -1
- package/frontend-dist/assets/Collection-BulopTxo.js +0 -1
- package/frontend-dist/assets/Dashboard-BahJSTKV.js +0 -3
- package/frontend-dist/assets/DialogTitle-CnqbO2hx.js +0 -1
- package/frontend-dist/assets/Label-73u_Os4X.js +0 -1
- package/frontend-dist/assets/Login-CoQSrVLo.js +0 -1
- package/frontend-dist/assets/Logs-C2b6MPXL.js +0 -1
- package/frontend-dist/assets/MappingList-m2ebUmJ9.js +0 -1
- package/frontend-dist/assets/ModelCard-B0pjEq6W.js +0 -1
- package/frontend-dist/assets/ModelMappings-BazKS9T4.js +0 -1
- package/frontend-dist/assets/Monitor-8B_tm1NO.js +0 -1
- package/frontend-dist/assets/PopoverTrigger-DSmA2dE4.js +0 -1
- package/frontend-dist/assets/PopperContent-Bd_mpt_D.js +0 -1
- package/frontend-dist/assets/Providers-TI83sF2T.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-CWLh-YlM.js +0 -5
- package/frontend-dist/assets/QuickSetup-DUZNdIvp.js +0 -1
- package/frontend-dist/assets/RetryRules-CzhCNQ0R.js +0 -1
- package/frontend-dist/assets/RouterKeys-B_C-Wp_I.js +0 -1
- package/frontend-dist/assets/RovingFocusItem-DwGTruuB.js +0 -1
- package/frontend-dist/assets/Schedules-BMB6RX9e.js +0 -1
- package/frontend-dist/assets/SelectValue-DRc1qira.js +0 -1
- package/frontend-dist/assets/Settings-Ck8CoUJC.js +0 -6
- package/frontend-dist/assets/Setup-dwKkHGrB.js +0 -1
- package/frontend-dist/assets/Switch-BE8DAylK.js +0 -1
- package/frontend-dist/assets/TableHeader-BqYT-eO-.js +0 -1
- package/frontend-dist/assets/Teleport-CLw1Jxrb.js +0 -3
- package/frontend-dist/assets/TooltipTrigger-bqCyq9MU.js +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-BDNR1wzi.js +0 -3
- package/frontend-dist/assets/UnifiedRequestDialog-DmpjVK9n.css +0 -1
- package/frontend-dist/assets/VisuallyHidden-BpDuyh8-.js +0 -1
- package/frontend-dist/assets/VisuallyHiddenInput-CCL5ykZW.js +0 -1
- package/frontend-dist/assets/alert-dialog-gprnWn1b.js +0 -1
- package/frontend-dist/assets/button-zud8Qspb.js +0 -12
- package/frontend-dist/assets/check-CRv7NpkT.js +0 -1
- package/frontend-dist/assets/dialog-Da8YFS7g.js +0 -1
- package/frontend-dist/assets/format-Dln15Luw.js +0 -1
- package/frontend-dist/assets/index-BfXK7SYr.js +0 -1
- package/frontend-dist/assets/index-CDtb1WVq.css +0 -1
- package/frontend-dist/assets/lib-xfvPneK8.js +0 -1
- package/frontend-dist/assets/loader-circle-D8BaqxEc.js +0 -1
- package/frontend-dist/assets/useFormControl-OyxyVR_M.js +0 -1
- package/frontend-dist/assets/useLogRetention-DE7zYGFK.js +0 -1
- package/frontend-dist/assets/useNonce-D_84NiFG.js +0 -1
- package/frontend-dist/assets/x-BN5AHIVk.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/{constants-ncbNnOLM.js → constants-B-VELBjk.js} +0 -0
- /package/frontend-dist/assets/{ohash.D__AXeF1-D5e5Wyzx.js → ohash.D__AXeF1-CTo5WcIm.js} +0 -0
|
@@ -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;
|
|
@@ -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>;
|
|
@@ -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;
|
|
@@ -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>;
|