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,311 @@
|
|
|
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
|
+
// ---------- Responses → Chat Completions ----------
|
|
11
|
+
/**
|
|
12
|
+
* Convert an OpenAI Responses API request body to an OpenAI Chat Completions
|
|
13
|
+
* request body.
|
|
14
|
+
*/
|
|
15
|
+
export function responsesToChatRequest(body) {
|
|
16
|
+
const result = {};
|
|
17
|
+
result.model = body.model;
|
|
18
|
+
// instructions → system message
|
|
19
|
+
const messages = [];
|
|
20
|
+
if (body.instructions != null && body.instructions !== "") {
|
|
21
|
+
messages.push({ role: "system", content: String(body.instructions) });
|
|
22
|
+
}
|
|
23
|
+
// input → messages
|
|
24
|
+
convertResponsesInputToChatMessages(body.input, messages);
|
|
25
|
+
result.messages = messages;
|
|
26
|
+
// max_output_tokens → max_completion_tokens
|
|
27
|
+
if (body.max_output_tokens != null) {
|
|
28
|
+
result.max_completion_tokens = body.max_output_tokens;
|
|
29
|
+
}
|
|
30
|
+
// Pass-through fields
|
|
31
|
+
if (body.temperature != null)
|
|
32
|
+
result.temperature = body.temperature;
|
|
33
|
+
if (body.top_p != null)
|
|
34
|
+
result.top_p = body.top_p;
|
|
35
|
+
if (body.stream != null)
|
|
36
|
+
result.stream = body.stream;
|
|
37
|
+
// tools: Responses format → Chat Completions format
|
|
38
|
+
const tools = body.tools;
|
|
39
|
+
if (tools) {
|
|
40
|
+
const chatTools = [];
|
|
41
|
+
for (const t of tools) {
|
|
42
|
+
if (t.type === "function") {
|
|
43
|
+
// Responses tools are flat: {type:"function", name, parameters, description}
|
|
44
|
+
// Chat tools need function wrapper: {type:"function", function:{name, parameters}}
|
|
45
|
+
const fn = { name: String(t.name) };
|
|
46
|
+
if (t.description != null)
|
|
47
|
+
fn.description = String(t.description);
|
|
48
|
+
if (t.parameters != null)
|
|
49
|
+
fn.parameters = t.parameters;
|
|
50
|
+
chatTools.push({ type: "function", function: fn });
|
|
51
|
+
}
|
|
52
|
+
// Non-function tools (web_search_preview, file_search, etc.) → skip
|
|
53
|
+
}
|
|
54
|
+
if (chatTools.length > 0) {
|
|
55
|
+
result.tools = chatTools;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// tool_choice — compatible between Chat and Responses
|
|
59
|
+
if (body.tool_choice != null) {
|
|
60
|
+
result.tool_choice = body.tool_choice;
|
|
61
|
+
}
|
|
62
|
+
// reasoning — pass through (both use {effort?, max_tokens?})
|
|
63
|
+
if (body.reasoning != null) {
|
|
64
|
+
result.reasoning = body.reasoning;
|
|
65
|
+
}
|
|
66
|
+
// text.format → response_format
|
|
67
|
+
const text = body.text;
|
|
68
|
+
if (text?.format != null) {
|
|
69
|
+
result.response_format = text.format;
|
|
70
|
+
}
|
|
71
|
+
// stream_options
|
|
72
|
+
if (body.stream_options != null) {
|
|
73
|
+
result.stream_options = body.stream_options;
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Convert Responses `input` (string | ResponseInputItem[]) into Chat
|
|
79
|
+
* Completions `messages[]`, appending to the provided array.
|
|
80
|
+
*/
|
|
81
|
+
function convertResponsesInputToChatMessages(input, messages) {
|
|
82
|
+
if (input == null)
|
|
83
|
+
return;
|
|
84
|
+
// String shorthand → single user message
|
|
85
|
+
if (typeof input === "string") {
|
|
86
|
+
messages.push({ role: "user", content: input });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(input))
|
|
90
|
+
return;
|
|
91
|
+
// Track pending function_calls to merge into a single assistant message
|
|
92
|
+
const pendingFnCalls = [];
|
|
93
|
+
for (const item of input) {
|
|
94
|
+
const type = item.type;
|
|
95
|
+
// Flush any pending function_calls before processing non-function_call items
|
|
96
|
+
if (type !== "function_call" && pendingFnCalls.length > 0) {
|
|
97
|
+
flushFunctionCalls(messages, pendingFnCalls);
|
|
98
|
+
}
|
|
99
|
+
if (type === "message") {
|
|
100
|
+
// ResponseInputMessage → Chat message
|
|
101
|
+
const role = item.role;
|
|
102
|
+
const content = extractMessageTextContent(item);
|
|
103
|
+
messages.push({ role, content });
|
|
104
|
+
}
|
|
105
|
+
else if (type === "input_text") {
|
|
106
|
+
messages.push({ role: "user", content: String(item.text ?? "") });
|
|
107
|
+
}
|
|
108
|
+
else if (type === "function_call") {
|
|
109
|
+
// Collect; will be flushed when next non-function_call item appears
|
|
110
|
+
// or at end of loop
|
|
111
|
+
const fn = {
|
|
112
|
+
name: String(item.name ?? ""),
|
|
113
|
+
arguments: String(item.arguments ?? "{}"),
|
|
114
|
+
};
|
|
115
|
+
pendingFnCalls.push({
|
|
116
|
+
id: String(item.id ?? ""),
|
|
117
|
+
type: "function",
|
|
118
|
+
function: fn,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else if (type === "function_call_output") {
|
|
122
|
+
messages.push({
|
|
123
|
+
role: "tool",
|
|
124
|
+
tool_call_id: String(item.call_id ?? ""),
|
|
125
|
+
content: String(item.output ?? ""),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else if (type === "reasoning") {
|
|
129
|
+
// No Chat Completions equivalent — skip
|
|
130
|
+
}
|
|
131
|
+
// Unknown item types → skip
|
|
132
|
+
}
|
|
133
|
+
// Flush any remaining pending function_calls
|
|
134
|
+
if (pendingFnCalls.length > 0) {
|
|
135
|
+
flushFunctionCalls(messages, pendingFnCalls);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Flush accumulated function_call tool_calls into a single assistant message.
|
|
140
|
+
*/
|
|
141
|
+
function flushFunctionCalls(messages, pending) {
|
|
142
|
+
messages.push({
|
|
143
|
+
role: "assistant",
|
|
144
|
+
content: null,
|
|
145
|
+
tool_calls: [...pending],
|
|
146
|
+
});
|
|
147
|
+
pending.length = 0;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Extract text content from a ResponseInputMessage.
|
|
151
|
+
*/
|
|
152
|
+
function extractMessageTextContent(msg) {
|
|
153
|
+
const content = msg.content;
|
|
154
|
+
if (content == null)
|
|
155
|
+
return "";
|
|
156
|
+
if (typeof content === "string")
|
|
157
|
+
return content;
|
|
158
|
+
if (Array.isArray(content)) {
|
|
159
|
+
return content
|
|
160
|
+
.filter((p) => p.type === "input_text" && p.text != null)
|
|
161
|
+
.map((p) => String(p.text))
|
|
162
|
+
.join("");
|
|
163
|
+
}
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
// ---------- Chat Completions → Responses ----------
|
|
167
|
+
/**
|
|
168
|
+
* Convert an OpenAI Chat Completions request body to an OpenAI Responses API
|
|
169
|
+
* request body.
|
|
170
|
+
*/
|
|
171
|
+
export function chatToResponsesRequest(body) {
|
|
172
|
+
const result = {};
|
|
173
|
+
result.model = body.model;
|
|
174
|
+
// Extract instructions from system/developer messages
|
|
175
|
+
const messages = body.messages;
|
|
176
|
+
const { instructions, nonSystemMsgs } = extractChatInstructions(messages ?? []);
|
|
177
|
+
if (instructions) {
|
|
178
|
+
result.instructions = instructions;
|
|
179
|
+
}
|
|
180
|
+
// Convert non-system messages → input items
|
|
181
|
+
result.input = convertChatMessagesToResponsesInput(nonSystemMsgs);
|
|
182
|
+
// max_completion_tokens / max_tokens → max_output_tokens
|
|
183
|
+
if (body.max_completion_tokens != null) {
|
|
184
|
+
result.max_output_tokens = body.max_completion_tokens;
|
|
185
|
+
}
|
|
186
|
+
else if (body.max_tokens != null) {
|
|
187
|
+
result.max_output_tokens = body.max_tokens;
|
|
188
|
+
}
|
|
189
|
+
// Pass-through fields
|
|
190
|
+
if (body.temperature != null)
|
|
191
|
+
result.temperature = body.temperature;
|
|
192
|
+
if (body.top_p != null)
|
|
193
|
+
result.top_p = body.top_p;
|
|
194
|
+
if (body.stream != null)
|
|
195
|
+
result.stream = body.stream;
|
|
196
|
+
// tools: Chat format → Responses format
|
|
197
|
+
const tools = body.tools;
|
|
198
|
+
if (tools) {
|
|
199
|
+
const respTools = [];
|
|
200
|
+
for (const t of tools) {
|
|
201
|
+
if (t.type === "function" && t.function) {
|
|
202
|
+
// Chat: {type:"function", function:{name, parameters, description}}
|
|
203
|
+
// Responses: {type:"function", name, parameters, description}
|
|
204
|
+
const fn = t.function;
|
|
205
|
+
const mapped = {
|
|
206
|
+
type: "function",
|
|
207
|
+
name: String(fn.name),
|
|
208
|
+
};
|
|
209
|
+
if (fn.description != null)
|
|
210
|
+
mapped.description = String(fn.description);
|
|
211
|
+
if (fn.parameters != null)
|
|
212
|
+
mapped.parameters = fn.parameters;
|
|
213
|
+
respTools.push(mapped);
|
|
214
|
+
}
|
|
215
|
+
// Non-function tools → skip
|
|
216
|
+
}
|
|
217
|
+
if (respTools.length > 0) {
|
|
218
|
+
result.tools = respTools;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// tool_choice — compatible
|
|
222
|
+
if (body.tool_choice != null) {
|
|
223
|
+
result.tool_choice = body.tool_choice;
|
|
224
|
+
}
|
|
225
|
+
// reasoning — pass through
|
|
226
|
+
if (body.reasoning != null) {
|
|
227
|
+
result.reasoning = body.reasoning;
|
|
228
|
+
}
|
|
229
|
+
// response_format → text.format
|
|
230
|
+
if (body.response_format != null) {
|
|
231
|
+
result.text = { format: body.response_format };
|
|
232
|
+
}
|
|
233
|
+
// stream_options
|
|
234
|
+
if (body.stream_options != null) {
|
|
235
|
+
result.stream_options = body.stream_options;
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Extract system/developer messages from Chat messages as instructions.
|
|
241
|
+
*/
|
|
242
|
+
function extractChatInstructions(messages) {
|
|
243
|
+
const parts = [];
|
|
244
|
+
const nonSystemMsgs = [];
|
|
245
|
+
for (const msg of messages) {
|
|
246
|
+
const role = msg.role;
|
|
247
|
+
if (role === "system" || role === "developer") {
|
|
248
|
+
parts.push(String(msg.content ?? ""));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
nonSystemMsgs.push(msg);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
instructions: parts.length > 0 ? parts.join("\n") : "",
|
|
256
|
+
nonSystemMsgs,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Convert Chat Completions non-system messages → Responses input items.
|
|
261
|
+
*/
|
|
262
|
+
function convertChatMessagesToResponsesInput(messages) {
|
|
263
|
+
const items = [];
|
|
264
|
+
for (const msg of messages) {
|
|
265
|
+
const role = msg.role;
|
|
266
|
+
if (role === "user") {
|
|
267
|
+
const content = msg.content;
|
|
268
|
+
const text = typeof content === "string" ? content : String(content ?? "");
|
|
269
|
+
items.push({
|
|
270
|
+
type: "message",
|
|
271
|
+
role: "user",
|
|
272
|
+
content: [{ type: "input_text", text }],
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
else if (role === "assistant") {
|
|
276
|
+
// Text content → assistant message with output_text
|
|
277
|
+
const content = msg.content;
|
|
278
|
+
if (content != null && content !== "" && content !== null) {
|
|
279
|
+
const text = typeof content === "string" ? content : String(content);
|
|
280
|
+
items.push({
|
|
281
|
+
type: "message",
|
|
282
|
+
role: "assistant",
|
|
283
|
+
content: [{ type: "output_text", text }],
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// tool_calls → function_call items
|
|
287
|
+
const toolCalls = msg.tool_calls;
|
|
288
|
+
if (toolCalls) {
|
|
289
|
+
for (const tc of toolCalls) {
|
|
290
|
+
const fn = tc.function;
|
|
291
|
+
items.push({
|
|
292
|
+
type: "function_call",
|
|
293
|
+
id: String(tc.id ?? ""),
|
|
294
|
+
call_id: String(tc.id ?? ""),
|
|
295
|
+
name: String(fn?.name ?? ""),
|
|
296
|
+
arguments: String(fn?.arguments ?? "{}"),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else if (role === "tool") {
|
|
302
|
+
items.push({
|
|
303
|
+
type: "function_call_output",
|
|
304
|
+
call_id: String(msg.tool_call_id ?? ""),
|
|
305
|
+
output: String(msg.content ?? ""),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
// reasoning_content in messages → skip (can't create reasoning items)
|
|
309
|
+
}
|
|
310
|
+
return items;
|
|
311
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { sanitizeToolUseId, parseToolArguments } from "./sanitize.js";
|
|
2
|
+
// ---------- Effort → budget mapping (shared with thinking-mapper) ----------
|
|
3
|
+
const EFFORT_BUDGET = { low: 1024, medium: 8192, high: 32768 };
|
|
4
|
+
const DEFAULT_BUDGET = 8192;
|
|
5
|
+
// ---------- Helpers ----------
|
|
6
|
+
/** Strip "toolu_" prefix from a tool_use_id to recover the original call_id. */
|
|
7
|
+
function stripTooluPrefix(id) {
|
|
8
|
+
return id.startsWith("toolu_") ? id.slice(6) : id;
|
|
9
|
+
}
|
|
10
|
+
/** Merge consecutive same-role messages to satisfy Anthropic strict alternation. */
|
|
11
|
+
function mergeConsecutiveMessages(msgs) {
|
|
12
|
+
const merged = [];
|
|
13
|
+
for (const msg of msgs) {
|
|
14
|
+
const prev = merged[merged.length - 1];
|
|
15
|
+
if (prev && prev.role === msg.role) {
|
|
16
|
+
prev.content = [...prev.content, ...msg.content];
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
merged.push({ ...msg, content: [...msg.content] });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return merged;
|
|
23
|
+
}
|
|
24
|
+
/** Ensure first message has role "user" (prepend empty user if needed). */
|
|
25
|
+
function ensureFirstIsUser(msgs) {
|
|
26
|
+
if (msgs.length > 0 && msgs[0].role !== "user") {
|
|
27
|
+
msgs.unshift({ role: "user", content: [{ type: "text", text: "" }] });
|
|
28
|
+
}
|
|
29
|
+
return msgs;
|
|
30
|
+
}
|
|
31
|
+
// ---------- Responses → Anthropic ----------
|
|
32
|
+
export function responsesToAnthropicRequest(body) {
|
|
33
|
+
const result = {};
|
|
34
|
+
result.model = body.model;
|
|
35
|
+
// instructions → system
|
|
36
|
+
if (body.instructions != null) {
|
|
37
|
+
result.system = String(body.instructions);
|
|
38
|
+
}
|
|
39
|
+
// input → messages
|
|
40
|
+
result.messages = convertResponsesInputToAntMessages(body.input);
|
|
41
|
+
// max_output_tokens → max_tokens
|
|
42
|
+
if (body.max_output_tokens != null) {
|
|
43
|
+
result.max_tokens = body.max_output_tokens;
|
|
44
|
+
}
|
|
45
|
+
// temperature, top_p, stream — pass through
|
|
46
|
+
if (body.temperature != null)
|
|
47
|
+
result.temperature = body.temperature;
|
|
48
|
+
if (body.top_p != null)
|
|
49
|
+
result.top_p = body.top_p;
|
|
50
|
+
if (body.stream != null)
|
|
51
|
+
result.stream = body.stream;
|
|
52
|
+
// tools: only function-type tools are forwarded
|
|
53
|
+
const tools = body.tools;
|
|
54
|
+
if (tools) {
|
|
55
|
+
const fnTools = tools.filter(t => t.type === "function");
|
|
56
|
+
if (fnTools.length > 0 && body.tool_choice !== "none") {
|
|
57
|
+
result.tools = fnTools.map(t => {
|
|
58
|
+
const mapped = { name: String(t.name) };
|
|
59
|
+
if (t.description != null)
|
|
60
|
+
mapped.description = String(t.description);
|
|
61
|
+
if (t.parameters != null)
|
|
62
|
+
mapped.input_schema = t.parameters;
|
|
63
|
+
return mapped;
|
|
64
|
+
});
|
|
65
|
+
// tool_choice mapping
|
|
66
|
+
if (body.tool_choice != null && body.tool_choice !== "none") {
|
|
67
|
+
const tc = mapToolChoiceResponses2Ant(body.tool_choice);
|
|
68
|
+
if (tc != null) {
|
|
69
|
+
result.tool_choice = body.parallel_tool_calls === false
|
|
70
|
+
? { ...tc, disable_parallel_tool_use: true }
|
|
71
|
+
: tc;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (body.parallel_tool_calls === false) {
|
|
75
|
+
result.tool_choice = { type: "auto", disable_parallel_tool_use: true };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// reasoning → thinking
|
|
80
|
+
if (body.reasoning) {
|
|
81
|
+
const reasoning = body.reasoning;
|
|
82
|
+
const effort = reasoning.effort;
|
|
83
|
+
const maxTokens = reasoning.max_tokens;
|
|
84
|
+
const budget = maxTokens ?? EFFORT_BUDGET[effort ?? ""] ?? DEFAULT_BUDGET;
|
|
85
|
+
result.thinking = { type: "enabled", budget_tokens: budget };
|
|
86
|
+
// Ensure max_tokens >= budget_tokens
|
|
87
|
+
if (result.max_tokens != null && result.max_tokens < budget) {
|
|
88
|
+
result.max_tokens = budget;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// metadata.user_id
|
|
92
|
+
const meta = body.metadata;
|
|
93
|
+
if (meta?.user_id) {
|
|
94
|
+
result.metadata = { user_id: meta.user_id };
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
/** Convert Responses input (string | ResponseInputItem[]) → Anthropic messages. */
|
|
99
|
+
function convertResponsesInputToAntMessages(input) {
|
|
100
|
+
if (input == null)
|
|
101
|
+
return [];
|
|
102
|
+
// String shorthand → single user message
|
|
103
|
+
if (typeof input === "string") {
|
|
104
|
+
return [{ role: "user", content: [{ type: "text", text: input }] }];
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(input))
|
|
107
|
+
return [];
|
|
108
|
+
const raw = [];
|
|
109
|
+
for (const item of input) {
|
|
110
|
+
const type = item.type;
|
|
111
|
+
if (type === "message") {
|
|
112
|
+
// ResponseInputMessage: extract content as AnthropicContentBlock[]
|
|
113
|
+
const role = item.role;
|
|
114
|
+
const content = extractMessageContent(item);
|
|
115
|
+
raw.push({ role, content });
|
|
116
|
+
}
|
|
117
|
+
else if (type === "function_call") {
|
|
118
|
+
// → assistant tool_use (Anthropic requires "toolu_" prefix)
|
|
119
|
+
const rawId = String(item.call_id ?? item.id ?? "");
|
|
120
|
+
const antId = rawId.startsWith("toolu_") ? rawId : `toolu_${rawId}`;
|
|
121
|
+
raw.push({
|
|
122
|
+
role: "assistant",
|
|
123
|
+
content: [{
|
|
124
|
+
type: "tool_use",
|
|
125
|
+
id: sanitizeToolUseId(antId),
|
|
126
|
+
name: String(item.name ?? ""),
|
|
127
|
+
input: parseToolArguments(item.arguments),
|
|
128
|
+
}],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else if (type === "function_call_output") {
|
|
132
|
+
// → user tool_result (Anthropic requires "toolu_" prefix)
|
|
133
|
+
const rawCallId = String(item.call_id ?? "");
|
|
134
|
+
const antCallId = rawCallId.startsWith("toolu_") ? rawCallId : `toolu_${rawCallId}`;
|
|
135
|
+
raw.push({
|
|
136
|
+
role: "user",
|
|
137
|
+
content: [{
|
|
138
|
+
type: "tool_result",
|
|
139
|
+
tool_use_id: sanitizeToolUseId(antCallId),
|
|
140
|
+
content: String(item.output ?? ""),
|
|
141
|
+
}],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else if (type === "reasoning") {
|
|
145
|
+
// → assistant thinking
|
|
146
|
+
const summary = item.summary;
|
|
147
|
+
const thinkingText = summary
|
|
148
|
+
? summary.map(s => String(s.text ?? "")).join("\n")
|
|
149
|
+
: "";
|
|
150
|
+
raw.push({
|
|
151
|
+
role: "assistant",
|
|
152
|
+
content: [{ type: "thinking", thinking: thinkingText }],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else if (type === "input_text") {
|
|
156
|
+
// → user text
|
|
157
|
+
raw.push({
|
|
158
|
+
role: "user",
|
|
159
|
+
content: [{ type: "text", text: String(item.text ?? "") }],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const merged = mergeConsecutiveMessages(raw);
|
|
164
|
+
ensureFirstIsUser(merged);
|
|
165
|
+
return merged;
|
|
166
|
+
}
|
|
167
|
+
/** Extract content blocks from a ResponseInputMessage. */
|
|
168
|
+
function extractMessageContent(msg) {
|
|
169
|
+
const content = msg.content;
|
|
170
|
+
if (content == null)
|
|
171
|
+
return [];
|
|
172
|
+
if (typeof content === "string") {
|
|
173
|
+
return [{ type: "text", text: content }];
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(content)) {
|
|
176
|
+
return content.flatMap((part) => {
|
|
177
|
+
if (part.type === "input_text" && part.text != null) {
|
|
178
|
+
return [{ type: "text", text: String(part.text) }];
|
|
179
|
+
}
|
|
180
|
+
return [];
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
/** Map Responses tool_choice → Anthropic tool_choice. */
|
|
186
|
+
function mapToolChoiceResponses2Ant(tc) {
|
|
187
|
+
if (tc === "auto")
|
|
188
|
+
return { type: "auto" };
|
|
189
|
+
if (tc === "required")
|
|
190
|
+
return { type: "any" };
|
|
191
|
+
if (tc === "none")
|
|
192
|
+
return undefined;
|
|
193
|
+
if (typeof tc === "object" && tc !== null) {
|
|
194
|
+
const obj = tc;
|
|
195
|
+
if (obj.type === "function" && obj.name) {
|
|
196
|
+
return { type: "tool", name: String(obj.name) };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { type: "auto" };
|
|
200
|
+
}
|
|
201
|
+
// ---------- Anthropic → Responses ----------
|
|
202
|
+
export function anthropicToResponsesRequest(body) {
|
|
203
|
+
const result = {};
|
|
204
|
+
result.model = body.model;
|
|
205
|
+
// system → instructions
|
|
206
|
+
if (body.system != null) {
|
|
207
|
+
if (typeof body.system === "string") {
|
|
208
|
+
result.instructions = body.system;
|
|
209
|
+
}
|
|
210
|
+
else if (Array.isArray(body.system)) {
|
|
211
|
+
result.instructions = body.system
|
|
212
|
+
.map(b => String(b.text ?? ""))
|
|
213
|
+
.join("\n");
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
result.instructions = String(body.system);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// messages → input items
|
|
220
|
+
const antMessages = body.messages;
|
|
221
|
+
result.input = antMessages ? convertAntMessagesToResponsesInput(antMessages) : [];
|
|
222
|
+
// max_tokens → max_output_tokens
|
|
223
|
+
if (body.max_tokens != null)
|
|
224
|
+
result.max_output_tokens = body.max_tokens;
|
|
225
|
+
// temperature, top_p, stream — pass through
|
|
226
|
+
if (body.temperature != null)
|
|
227
|
+
result.temperature = body.temperature;
|
|
228
|
+
if (body.top_p != null)
|
|
229
|
+
result.top_p = body.top_p;
|
|
230
|
+
if (body.stream != null)
|
|
231
|
+
result.stream = body.stream;
|
|
232
|
+
// tools
|
|
233
|
+
const tools = body.tools;
|
|
234
|
+
if (tools && tools.length > 0) {
|
|
235
|
+
result.tools = tools.map(t => {
|
|
236
|
+
const mapped = {
|
|
237
|
+
type: "function",
|
|
238
|
+
name: String(t.name),
|
|
239
|
+
};
|
|
240
|
+
if (t.description != null)
|
|
241
|
+
mapped.description = String(t.description);
|
|
242
|
+
if (t.input_schema != null)
|
|
243
|
+
mapped.parameters = t.input_schema;
|
|
244
|
+
return mapped;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// tool_choice
|
|
248
|
+
if (body.tool_choice != null) {
|
|
249
|
+
const tc = mapToolChoiceAnt2Responses(body.tool_choice);
|
|
250
|
+
if (tc != null)
|
|
251
|
+
result.tool_choice = tc;
|
|
252
|
+
}
|
|
253
|
+
// thinking → reasoning
|
|
254
|
+
if (body.thinking) {
|
|
255
|
+
const thinking = body.thinking;
|
|
256
|
+
if (thinking.type === "enabled" && thinking.budget_tokens != null) {
|
|
257
|
+
result.reasoning = { max_tokens: thinking.budget_tokens };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// metadata.user_id
|
|
261
|
+
const meta = body.metadata;
|
|
262
|
+
if (meta?.user_id) {
|
|
263
|
+
result.metadata = { user_id: meta.user_id };
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
/** Convert Anthropic messages → Responses input items. */
|
|
268
|
+
function convertAntMessagesToResponsesInput(messages) {
|
|
269
|
+
const items = [];
|
|
270
|
+
for (const msg of messages) {
|
|
271
|
+
const role = msg.role;
|
|
272
|
+
const content = msg.content;
|
|
273
|
+
if (!content || !Array.isArray(content))
|
|
274
|
+
continue;
|
|
275
|
+
if (role === "user") {
|
|
276
|
+
// Separate text blocks and tool_result blocks
|
|
277
|
+
const textBlocks = content.filter(b => b.type === "text");
|
|
278
|
+
const toolResultBlocks = content.filter(b => b.type === "tool_result");
|
|
279
|
+
if (textBlocks.length > 0) {
|
|
280
|
+
const text = textBlocks.map(b => String(b.text ?? "")).join("");
|
|
281
|
+
items.push({
|
|
282
|
+
type: "message",
|
|
283
|
+
role: "user",
|
|
284
|
+
content: [{ type: "input_text", text }],
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
for (const tr of toolResultBlocks) {
|
|
288
|
+
items.push({
|
|
289
|
+
type: "function_call_output",
|
|
290
|
+
call_id: stripTooluPrefix(String(tr.tool_use_id ?? "")),
|
|
291
|
+
output: String(tr.content ?? ""),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (role === "assistant") {
|
|
296
|
+
const textBlocks = content.filter(b => b.type === "text");
|
|
297
|
+
const toolUseBlocks = content.filter(b => b.type === "tool_use");
|
|
298
|
+
const thinkingBlocks = content.filter(b => b.type === "thinking");
|
|
299
|
+
// thinking → reasoning items
|
|
300
|
+
for (const tb of thinkingBlocks) {
|
|
301
|
+
items.push({
|
|
302
|
+
type: "reasoning",
|
|
303
|
+
id: `rs_${Date.now()}_${items.length}`,
|
|
304
|
+
summary: [{ type: "summary_text", text: String(tb.thinking ?? "") }],
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
// text → assistant message
|
|
308
|
+
if (textBlocks.length > 0) {
|
|
309
|
+
const text = textBlocks.map(b => String(b.text ?? "")).join("");
|
|
310
|
+
items.push({
|
|
311
|
+
type: "message",
|
|
312
|
+
role: "assistant",
|
|
313
|
+
content: [{ type: "output_text", text }],
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// tool_use → function_call
|
|
317
|
+
for (const tu of toolUseBlocks) {
|
|
318
|
+
items.push({
|
|
319
|
+
type: "function_call",
|
|
320
|
+
id: String(tu.id ?? ""),
|
|
321
|
+
call_id: stripTooluPrefix(String(tu.id ?? "")),
|
|
322
|
+
name: String(tu.name ?? ""),
|
|
323
|
+
arguments: JSON.stringify(tu.input ?? {}),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return items;
|
|
329
|
+
}
|
|
330
|
+
/** Map Anthropic tool_choice → Responses tool_choice. */
|
|
331
|
+
function mapToolChoiceAnt2Responses(tc) {
|
|
332
|
+
if (typeof tc === "string") {
|
|
333
|
+
if (tc === "auto")
|
|
334
|
+
return "auto";
|
|
335
|
+
if (tc === "any")
|
|
336
|
+
return "required";
|
|
337
|
+
return "auto";
|
|
338
|
+
}
|
|
339
|
+
if (typeof tc === "object" && tc !== null) {
|
|
340
|
+
const obj = tc;
|
|
341
|
+
if (obj.type === "auto")
|
|
342
|
+
return "auto";
|
|
343
|
+
if (obj.type === "any")
|
|
344
|
+
return "required";
|
|
345
|
+
if (obj.type === "tool" && obj.name) {
|
|
346
|
+
return { type: "function", name: String(obj.name) };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return "auto";
|
|
350
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge (lossy) response 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 structured reasoning summaries, built-in tool outputs,
|
|
8
|
+
* or response-level metadata.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Convert a Responses API response body to a Chat Completions response body.
|
|
12
|
+
*
|
|
13
|
+
* Lossy: structured reasoning summaries are flattened to a single string;
|
|
14
|
+
* built-in tool output items (web_search_call, etc.) are skipped.
|
|
15
|
+
*/
|
|
16
|
+
export declare function responsesToChatResponse(bodyStr: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Convert a Chat Completions response body to a Responses API response body.
|
|
19
|
+
*
|
|
20
|
+
* Lossy: Chat Completions has no equivalent for built-in tool output items
|
|
21
|
+
* or structured reasoning summaries.
|
|
22
|
+
*/
|
|
23
|
+
export declare function chatToResponsesResponse(bodyStr: string): string;
|