@jsonstudio/llms 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversion/codec-registry.js +11 -1
- package/dist/conversion/codecs/anthropic-openai-codec.d.ts +13 -0
- package/dist/conversion/codecs/anthropic-openai-codec.js +18 -473
- package/dist/conversion/codecs/gemini-openai-codec.js +91 -48
- package/dist/conversion/codecs/responses-openai-codec.js +9 -2
- package/dist/conversion/hub/format-adapters/anthropic-format-adapter.js +3 -0
- package/dist/conversion/hub/format-adapters/chat-format-adapter.js +3 -0
- package/dist/conversion/hub/format-adapters/gemini-format-adapter.js +3 -0
- package/dist/conversion/hub/format-adapters/responses-format-adapter.d.ts +19 -0
- package/dist/conversion/hub/format-adapters/responses-format-adapter.js +9 -0
- package/dist/conversion/hub/node-support.js +3 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +37 -32
- package/dist/conversion/hub/response/provider-response.js +1 -1
- package/dist/conversion/hub/response/response-mappers.js +1 -1
- package/dist/conversion/hub/response/response-runtime.js +109 -10
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +70 -156
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +63 -52
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +76 -143
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +40 -160
- package/dist/conversion/hub/standardized-bridge.js +3 -0
- package/dist/conversion/hub/tool-governance/rules.js +2 -2
- package/dist/conversion/index.d.ts +5 -0
- package/dist/conversion/index.js +5 -0
- package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.d.ts +12 -0
- package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +100 -0
- package/dist/conversion/pipeline/codecs/v2/openai-openai-pipeline.d.ts +15 -0
- package/dist/conversion/pipeline/codecs/v2/openai-openai-pipeline.js +174 -0
- package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.d.ts +14 -0
- package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.js +166 -0
- package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.d.ts +13 -0
- package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +66 -0
- package/dist/conversion/pipeline/hooks/adapter-context.d.ts +7 -0
- package/dist/conversion/pipeline/hooks/adapter-context.js +18 -0
- package/dist/conversion/pipeline/hooks/protocol-hooks.d.ts +67 -0
- package/dist/conversion/pipeline/hooks/protocol-hooks.js +1 -0
- package/dist/conversion/pipeline/index.d.ts +35 -0
- package/dist/conversion/pipeline/index.js +103 -0
- package/dist/conversion/pipeline/meta/meta-bag.d.ts +20 -0
- package/dist/conversion/pipeline/meta/meta-bag.js +81 -0
- package/dist/conversion/pipeline/schema/canonical-chat.d.ts +18 -0
- package/dist/conversion/pipeline/schema/canonical-chat.js +1 -0
- package/dist/conversion/pipeline/schema/index.d.ts +1 -0
- package/dist/conversion/pipeline/schema/index.js +1 -0
- package/dist/conversion/responses/responses-openai-bridge.d.ts +48 -0
- package/dist/conversion/responses/responses-openai-bridge.js +157 -1146
- package/dist/conversion/shared/anthropic-message-utils.d.ts +12 -0
- package/dist/conversion/shared/anthropic-message-utils.js +587 -0
- package/dist/conversion/shared/bridge-actions.d.ts +39 -0
- package/dist/conversion/shared/bridge-actions.js +709 -0
- package/dist/conversion/shared/bridge-conversation-store.d.ts +41 -0
- package/dist/conversion/shared/bridge-conversation-store.js +279 -0
- package/dist/conversion/shared/bridge-id-utils.d.ts +7 -0
- package/dist/conversion/shared/bridge-id-utils.js +42 -0
- package/dist/conversion/shared/bridge-instructions.d.ts +1 -0
- package/dist/conversion/shared/bridge-instructions.js +113 -0
- package/dist/conversion/shared/bridge-message-types.d.ts +39 -0
- package/dist/conversion/shared/bridge-message-types.js +1 -0
- package/dist/conversion/shared/bridge-message-utils.d.ts +22 -0
- package/dist/conversion/shared/bridge-message-utils.js +473 -0
- package/dist/conversion/shared/bridge-metadata.d.ts +1 -0
- package/dist/conversion/shared/bridge-metadata.js +1 -0
- package/dist/conversion/shared/bridge-policies.d.ts +18 -0
- package/dist/conversion/shared/bridge-policies.js +276 -0
- package/dist/conversion/shared/bridge-request-adapter.d.ts +28 -0
- package/dist/conversion/shared/bridge-request-adapter.js +430 -0
- package/dist/conversion/shared/chat-output-normalizer.d.ts +4 -0
- package/dist/conversion/shared/chat-output-normalizer.js +56 -0
- package/dist/conversion/shared/chat-request-filters.js +24 -1
- package/dist/conversion/shared/gemini-tool-utils.d.ts +5 -0
- package/dist/conversion/shared/gemini-tool-utils.js +130 -0
- package/dist/conversion/shared/metadata-passthrough.d.ts +11 -0
- package/dist/conversion/shared/metadata-passthrough.js +57 -0
- package/dist/conversion/shared/output-content-normalizer.d.ts +12 -0
- package/dist/conversion/shared/output-content-normalizer.js +119 -0
- package/dist/conversion/shared/reasoning-normalizer.d.ts +21 -0
- package/dist/conversion/shared/reasoning-normalizer.js +368 -0
- package/dist/conversion/shared/reasoning-tool-normalizer.d.ts +12 -0
- package/dist/conversion/shared/reasoning-tool-normalizer.js +132 -0
- package/dist/conversion/shared/reasoning-tool-parser.d.ts +10 -0
- package/dist/conversion/shared/reasoning-tool-parser.js +95 -0
- package/dist/conversion/shared/reasoning-utils.d.ts +2 -0
- package/dist/conversion/shared/reasoning-utils.js +42 -0
- package/dist/conversion/shared/responses-conversation-store.js +5 -11
- package/dist/conversion/shared/responses-message-utils.d.ts +15 -0
- package/dist/conversion/shared/responses-message-utils.js +206 -0
- package/dist/conversion/shared/responses-output-builder.d.ts +15 -0
- package/dist/conversion/shared/responses-output-builder.js +179 -0
- package/dist/conversion/shared/responses-output-utils.d.ts +7 -0
- package/dist/conversion/shared/responses-output-utils.js +108 -0
- package/dist/conversion/shared/responses-request-adapter.d.ts +28 -0
- package/dist/conversion/shared/responses-request-adapter.js +9 -40
- package/dist/conversion/shared/responses-response-utils.d.ts +3 -0
- package/dist/conversion/shared/responses-response-utils.js +209 -0
- package/dist/conversion/shared/responses-tool-utils.d.ts +12 -0
- package/dist/conversion/shared/responses-tool-utils.js +90 -0
- package/dist/conversion/shared/responses-types.d.ts +33 -0
- package/dist/conversion/shared/responses-types.js +1 -0
- package/dist/conversion/shared/tool-call-utils.d.ts +11 -0
- package/dist/conversion/shared/tool-call-utils.js +56 -0
- package/dist/conversion/shared/tool-mapping.d.ts +19 -0
- package/dist/conversion/shared/tool-mapping.js +124 -0
- package/dist/conversion/shared/tool-normalizers.d.ts +4 -0
- package/dist/conversion/shared/tool-normalizers.js +84 -0
- package/dist/router/virtual-router/bootstrap.js +18 -3
- package/dist/router/virtual-router/provider-registry.js +4 -2
- package/dist/router/virtual-router/types.d.ts +212 -0
- package/dist/sse/index.d.ts +38 -2
- package/dist/sse/index.js +27 -0
- package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.d.ts +14 -0
- package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.js +106 -73
- package/dist/sse/json-to-sse/chat-json-to-sse-converter.js +6 -2
- package/dist/sse/json-to-sse/gemini-json-to-sse-converter.d.ts +14 -0
- package/dist/sse/json-to-sse/gemini-json-to-sse-converter.js +99 -0
- package/dist/sse/json-to-sse/index.d.ts +7 -0
- package/dist/sse/json-to-sse/index.js +2 -0
- package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.d.ts +13 -0
- package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.js +150 -0
- package/dist/sse/json-to-sse/sequencers/chat-sequencer.d.ts +39 -0
- package/dist/sse/json-to-sse/sequencers/chat-sequencer.js +49 -3
- package/dist/sse/json-to-sse/sequencers/gemini-sequencer.d.ts +10 -0
- package/dist/sse/json-to-sse/sequencers/gemini-sequencer.js +95 -0
- package/dist/sse/json-to-sse/sequencers/responses-sequencer.js +31 -5
- package/dist/sse/registry/sse-codec-registry.d.ts +32 -0
- package/dist/sse/registry/sse-codec-registry.js +30 -1
- package/dist/sse/shared/reasoning-dispatcher.d.ts +10 -0
- package/dist/sse/shared/reasoning-dispatcher.js +25 -0
- package/dist/sse/shared/responses-output-normalizer.d.ts +12 -0
- package/dist/sse/shared/responses-output-normalizer.js +45 -0
- package/dist/sse/shared/serializers/anthropic-event-serializer.d.ts +2 -0
- package/dist/sse/shared/serializers/anthropic-event-serializer.js +9 -0
- package/dist/sse/shared/serializers/gemini-event-serializer.d.ts +2 -0
- package/dist/sse/shared/serializers/gemini-event-serializer.js +5 -0
- package/dist/sse/shared/serializers/index.d.ts +41 -0
- package/dist/sse/shared/serializers/index.js +2 -0
- package/dist/sse/shared/writer.d.ts +127 -0
- package/dist/sse/shared/writer.js +37 -1
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +11 -0
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +92 -127
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.d.ts +16 -0
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +151 -0
- package/dist/sse/sse-to-json/builders/response-builder.d.ts +165 -0
- package/dist/sse/sse-to-json/builders/response-builder.js +27 -6
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +114 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +79 -3
- package/dist/sse/sse-to-json/gemini-sse-to-json-converter.d.ts +13 -0
- package/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +160 -0
- package/dist/sse/sse-to-json/index.d.ts +7 -0
- package/dist/sse/sse-to-json/index.js +2 -0
- package/dist/sse/sse-to-json/parsers/sse-parser.js +53 -1
- package/dist/sse/types/anthropic-types.d.ts +170 -0
- package/dist/sse/types/anthropic-types.js +8 -5
- package/dist/sse/types/chat-types.d.ts +10 -0
- package/dist/sse/types/chat-types.js +2 -1
- package/dist/sse/types/core-interfaces.d.ts +1 -1
- package/dist/sse/types/gemini-types.d.ts +116 -0
- package/dist/sse/types/gemini-types.js +5 -0
- package/dist/sse/types/index.d.ts +5 -2
- package/dist/sse/types/index.js +2 -0
- package/package.json +1 -1
|
@@ -1,85 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ensureBridgeInstructions } from '../shared/bridge-instructions.js';
|
|
2
|
+
import { convertMessagesToBridgeInput, convertBridgeInputToChatMessages } from '../shared/bridge-message-utils.js';
|
|
3
|
+
import { createToolCallIdTransformer, enforceToolCallIdStyle, resolveToolCallIdStyle, stripInternalToolingMetadata, sanitizeResponsesFunctionName } from '../shared/responses-tool-utils.js';
|
|
4
|
+
import { mapBridgeToolsToChat, mapChatToolsToBridge } from '../shared/tool-mapping.js';
|
|
3
5
|
// --- Utilities (ported strictly) ---
|
|
4
6
|
import { canonicalizeChatResponseTools } from '../shared/tool-canonicalizer.js';
|
|
7
|
+
import { normalizeMessageReasoningTools } from '../shared/reasoning-tool-normalizer.js';
|
|
8
|
+
import { createBridgeActionState, runBridgeActionPipeline } from '../shared/bridge-actions.js';
|
|
9
|
+
import { resolveBridgePolicy, resolvePolicyActions } from '../shared/bridge-policies.js';
|
|
10
|
+
import { buildResponsesOutputFromChat } from '../shared/responses-output-builder.js';
|
|
5
11
|
function isObject(v) {
|
|
6
12
|
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
7
13
|
}
|
|
8
|
-
// tryParseJson now shared in ../shared/jsonish.ts
|
|
9
|
-
// splitCommandString now unified in ../shared/tooling.ts
|
|
10
|
-
// parseLenient now shared in ../shared/jsonish.ts
|
|
11
|
-
function defaultObjectSchema() { return { type: 'object', properties: {}, additionalProperties: true }; }
|
|
12
14
|
// normalizeTools unified in ../shared/args-mapping.ts
|
|
13
15
|
// --- Structured self-repair helpers for tool failures (Responses path) ---
|
|
14
16
|
// use shared isImagePath
|
|
15
17
|
// NOTE: 自修复提示已移除(统一标准:不做模糊兜底)。
|
|
16
|
-
function getExpectedType(schema, key) {
|
|
17
|
-
if (!schema || !isObject(schema))
|
|
18
|
-
return { kind: 'any' };
|
|
19
|
-
const props = isObject(schema.properties) ? schema.properties : undefined;
|
|
20
|
-
const s = props && isObject(props[key]) ? props[key] : undefined;
|
|
21
|
-
if (!s)
|
|
22
|
-
return { kind: 'any' };
|
|
23
|
-
const t = s.type;
|
|
24
|
-
if (t === 'string')
|
|
25
|
-
return { kind: 'string' };
|
|
26
|
-
if (t === 'object')
|
|
27
|
-
return { kind: 'object' };
|
|
28
|
-
if (t === 'array') {
|
|
29
|
-
const items = s.items;
|
|
30
|
-
const it = items && isObject(items) ? items.type : undefined;
|
|
31
|
-
if (it === 'string')
|
|
32
|
-
return { kind: 'arrayString' };
|
|
33
|
-
return { kind: 'any' };
|
|
34
|
-
}
|
|
35
|
-
return { kind: 'any' };
|
|
36
|
-
}
|
|
37
|
-
function normalizeArgumentsBySchema(argsStringOrObj, _functionName, _tools) {
|
|
38
|
-
// 只做最小字符串化:字符串原样透传;对象仅 stringify 一次;不做 parse/修复
|
|
39
|
-
if (typeof argsStringOrObj === 'string')
|
|
40
|
-
return argsStringOrObj;
|
|
41
|
-
try {
|
|
42
|
-
return JSON.stringify(argsStringOrObj ?? {});
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return String(argsStringOrObj);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function parseFunctionArguments(args) {
|
|
49
|
-
if (typeof args === 'string') {
|
|
50
|
-
try {
|
|
51
|
-
const obj = JSON.parse(args);
|
|
52
|
-
return isObject(obj) ? obj : {};
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return {};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (isObject(args))
|
|
59
|
-
return args;
|
|
60
|
-
return {};
|
|
61
|
-
}
|
|
62
|
-
// Responses-only: 保持“只映射,不解析/不修复”。
|
|
63
|
-
function normalizeToolOutput(entry) {
|
|
64
|
-
const out = entry?.output;
|
|
65
|
-
if (typeof out === 'string')
|
|
66
|
-
return out;
|
|
67
|
-
if (out && typeof out === 'object') {
|
|
68
|
-
try {
|
|
69
|
-
return JSON.stringify(out);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return String(out);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
18
|
// --- Public bridge functions ---
|
|
78
19
|
export function captureResponsesContext(payload, dto) {
|
|
79
|
-
|
|
20
|
+
ensureBridgeInstructions(payload);
|
|
80
21
|
const context = {
|
|
81
22
|
requestId: dto?.route?.requestId,
|
|
82
|
-
instructions: typeof payload.instructions === 'string' ? payload.instructions : undefined,
|
|
83
23
|
input: Array.isArray(payload.input) ? payload.input : undefined,
|
|
84
24
|
include: payload.include,
|
|
85
25
|
store: payload.store,
|
|
@@ -100,46 +40,39 @@ export function buildChatRequestFromResponses(payload, context) {
|
|
|
100
40
|
// V3: 对 Responses 路径仅做“形状转换”,不做参数解析/修复。
|
|
101
41
|
// 将顶层 { type,name,description,parameters,strict } 归一为 OpenAI Chat tools 形状:
|
|
102
42
|
// { type:'function', function:{ name,description,parameters,strict? } }
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const anyT = t;
|
|
107
|
-
const originalType = typeof anyT.type === 'string' ? anyT.type : 'function';
|
|
108
|
-
const mappedType = ['function', 'custom'].includes(originalType.toLowerCase()) ? 'function' : originalType;
|
|
109
|
-
const fn = (anyT.function && typeof anyT.function === 'object') ? anyT.function : {};
|
|
110
|
-
const topName = typeof anyT.name === 'string' ? anyT.name : undefined;
|
|
111
|
-
const fnName = typeof fn.name === 'string' ? fn.name : undefined;
|
|
112
|
-
const name = (fnName || topName || '').trim();
|
|
113
|
-
if (!name)
|
|
114
|
-
return null;
|
|
115
|
-
const topDesc = typeof anyT.description === 'string' ? anyT.description : undefined;
|
|
116
|
-
const fnDesc = typeof fn.description === 'string' ? fn.description : undefined;
|
|
117
|
-
const description = fnDesc || topDesc;
|
|
118
|
-
const fnParams = Object.prototype.hasOwnProperty.call(fn, 'parameters') ? fn.parameters : undefined;
|
|
119
|
-
const topParams = Object.prototype.hasOwnProperty.call(anyT, 'parameters') ? anyT.parameters : undefined;
|
|
120
|
-
// 不解析/修复参数;保留原始 schema
|
|
121
|
-
const parameters = fnParams !== undefined ? fnParams : topParams;
|
|
122
|
-
const strict = typeof fn.strict === 'boolean' ? fn.strict :
|
|
123
|
-
typeof anyT.strict === 'boolean' ? anyT.strict :
|
|
124
|
-
undefined;
|
|
125
|
-
const fnOut = { name };
|
|
126
|
-
if (description)
|
|
127
|
-
fnOut.description = description;
|
|
128
|
-
if (parameters !== undefined)
|
|
129
|
-
fnOut.parameters = parameters;
|
|
130
|
-
if (strict !== undefined)
|
|
131
|
-
fnOut.strict = strict;
|
|
132
|
-
return { type: mappedType, function: fnOut };
|
|
133
|
-
};
|
|
134
|
-
const toolsNormalized = Array.isArray(payload.tools)
|
|
135
|
-
? payload.tools.map(mapToolToChatShape).filter(Boolean)
|
|
136
|
-
: undefined;
|
|
43
|
+
const toolsNormalized = mapBridgeToolsToChat(payload.tools, {
|
|
44
|
+
sanitizeName: sanitizeResponsesFunctionName
|
|
45
|
+
});
|
|
137
46
|
// 不在 Responses 路径进行 MCP 工具注入;统一由 Chat 后半段治理注入
|
|
138
|
-
let messages =
|
|
139
|
-
instructions: context.instructions,
|
|
47
|
+
let messages = convertBridgeInputToChatMessages({
|
|
140
48
|
input: context.input,
|
|
141
|
-
toolsNormalized
|
|
49
|
+
tools: toolsNormalized,
|
|
50
|
+
normalizeFunctionName: sanitizeResponsesFunctionName,
|
|
51
|
+
toolResultFallbackText: 'Command succeeded (no output).'
|
|
142
52
|
});
|
|
53
|
+
try {
|
|
54
|
+
const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-responses', moduleType: 'openai-responses' });
|
|
55
|
+
const policyActions = resolvePolicyActions(bridgePolicy, 'request_inbound');
|
|
56
|
+
if (policyActions?.length) {
|
|
57
|
+
const actionState = createBridgeActionState({
|
|
58
|
+
messages,
|
|
59
|
+
requiredAction: typeof payload?.required_action === 'object' ? payload.required_action : undefined,
|
|
60
|
+
rawRequest: payload
|
|
61
|
+
});
|
|
62
|
+
runBridgeActionPipeline({
|
|
63
|
+
stage: 'request_inbound',
|
|
64
|
+
actions: policyActions,
|
|
65
|
+
protocol: bridgePolicy?.protocol ?? 'openai-responses',
|
|
66
|
+
moduleType: bridgePolicy?.moduleType ?? 'openai-responses',
|
|
67
|
+
requestId: context.requestId,
|
|
68
|
+
state: actionState
|
|
69
|
+
});
|
|
70
|
+
messages = actionState.messages;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Policy application is best-effort; fall back to raw mapping on failure.
|
|
75
|
+
}
|
|
143
76
|
if (Array.isArray(context.originalSystemMessages) && context.originalSystemMessages.length) {
|
|
144
77
|
const preservedSystems = context.originalSystemMessages
|
|
145
78
|
.map(text => ({ role: 'system', content: text }))
|
|
@@ -152,52 +85,9 @@ export function buildChatRequestFromResponses(payload, context) {
|
|
|
152
85
|
// 不在 Responses 路径做工具治理;统一在 Chat 后半段处理
|
|
153
86
|
// No system tips for MCP on OpenAI Responses path (avoid leaking tool names)
|
|
154
87
|
if (!messages.length) {
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
const input = Array.isArray(context.input) ? context.input : [];
|
|
158
|
-
for (const entry of input) {
|
|
159
|
-
if (!entry || typeof entry !== 'object')
|
|
160
|
-
continue;
|
|
161
|
-
const role = normalizeResponseRole(entry.role || 'user');
|
|
162
|
-
if (role !== 'user')
|
|
163
|
-
continue;
|
|
164
|
-
const blocks = Array.isArray(entry.content) ? entry.content : [];
|
|
165
|
-
const parts = [];
|
|
166
|
-
for (const b of blocks) {
|
|
167
|
-
if (!b || typeof b !== 'object')
|
|
168
|
-
continue;
|
|
169
|
-
const t = String(b.type || '').toLowerCase();
|
|
170
|
-
if ((t === 'input_text' || t === 'text' || t === 'commentary') && typeof b.text === 'string') {
|
|
171
|
-
parts.push(b.text);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const txt = parts.join('\n').trim();
|
|
175
|
-
if (txt) {
|
|
176
|
-
messages.push({ role: 'user', content: txt });
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
catch { /* ignore */ }
|
|
182
|
-
if (!messages.length) {
|
|
183
|
-
// 最小兜底:若存在 instructions,则用作一条 user 消息(部分上游要求至少一条 user)
|
|
184
|
-
if (typeof context.instructions === 'string' && context.instructions.trim().length > 0) {
|
|
185
|
-
const s = context.instructions.trim();
|
|
186
|
-
messages.push({ role: 'user', content: s });
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
throw new Error('Responses payload produced no chat messages');
|
|
190
|
-
}
|
|
191
|
-
}
|
|
88
|
+
throw new Error('Responses payload produced no chat messages');
|
|
192
89
|
}
|
|
193
|
-
// 如果只有 system 消息且无 user/assistant/tool
|
|
194
|
-
try {
|
|
195
|
-
const hasNonSystem = messages.some((m) => (m && typeof m === 'object' && (m.role === 'user' || m.role === 'assistant' || m.role === 'tool')));
|
|
196
|
-
if (!hasNonSystem && typeof context.instructions === 'string' && context.instructions.trim().length > 0) {
|
|
197
|
-
messages.push({ role: 'user', content: context.instructions.trim() });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
catch { /* ignore */ }
|
|
90
|
+
// 如果只有 system 消息且无 user/assistant/tool,后续桥接 action 会从 instructions 注入兜底 user 消息
|
|
201
91
|
const result = { model: payload.model, messages };
|
|
202
92
|
if (typeof payload.stream === 'boolean') {
|
|
203
93
|
result.stream = payload.stream;
|
|
@@ -224,31 +114,39 @@ export function buildChatRequestFromResponses(payload, context) {
|
|
|
224
114
|
* - 将 system 消息折叠到 instructions;
|
|
225
115
|
* - 将 user/assistant/tool 消息编码为 input[] 中的 message 块,使得 mapResponsesInputToChat 能够还原为等价 Chat 请求。
|
|
226
116
|
*/
|
|
227
|
-
|
|
117
|
+
function normalizeBridgeHistory(seed) {
|
|
118
|
+
if (!seed || typeof seed !== 'object') {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const record = seed;
|
|
122
|
+
if (!Array.isArray(record.input)) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
const systemMessages = Array.isArray(record.originalSystemMessages)
|
|
126
|
+
? record.originalSystemMessages.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
|
|
127
|
+
: [];
|
|
128
|
+
return {
|
|
129
|
+
input: record.input,
|
|
130
|
+
combinedSystemInstruction: typeof record.combinedSystemInstruction === 'string' && record.combinedSystemInstruction.trim().length
|
|
131
|
+
? record.combinedSystemInstruction
|
|
132
|
+
: undefined,
|
|
133
|
+
latestUserInstruction: typeof record.latestUserInstruction === 'string' && record.latestUserInstruction.trim().length
|
|
134
|
+
? record.latestUserInstruction
|
|
135
|
+
: undefined,
|
|
136
|
+
originalSystemMessages: systemMessages
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function buildResponsesRequestFromChat(payload, ctx, extras) {
|
|
228
140
|
const chat = unwrapData(payload);
|
|
229
141
|
const out = {};
|
|
230
142
|
// 基本字段
|
|
231
143
|
out.model = chat.model;
|
|
232
144
|
// tools: 反向映射为 ResponsesToolDefinition 形状
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const fn = anyT.function;
|
|
239
|
-
const base = { type: 'function' };
|
|
240
|
-
if (typeof fn.name === 'string' && fn.name.trim())
|
|
241
|
-
base.name = fn.name.trim();
|
|
242
|
-
if (typeof fn.description === 'string')
|
|
243
|
-
base.description = fn.description;
|
|
244
|
-
if (Object.prototype.hasOwnProperty.call(fn, 'parameters'))
|
|
245
|
-
base.parameters = fn.parameters;
|
|
246
|
-
if (typeof fn.strict === 'boolean')
|
|
247
|
-
base.strict = fn.strict;
|
|
248
|
-
return base;
|
|
249
|
-
}
|
|
250
|
-
return anyT;
|
|
251
|
-
});
|
|
145
|
+
const responsesTools = mapChatToolsToBridge(chat.tools, {
|
|
146
|
+
sanitizeName: sanitizeResponsesFunctionName
|
|
147
|
+
});
|
|
148
|
+
if (responsesTools?.length) {
|
|
149
|
+
out.tools = responsesTools;
|
|
252
150
|
}
|
|
253
151
|
const passthroughKeys = [
|
|
254
152
|
'tool_choice',
|
|
@@ -267,192 +165,57 @@ export function buildResponsesRequestFromChat(payload, ctx) {
|
|
|
267
165
|
if (typeof chat.max_tokens === 'number' && chat.max_output_tokens === undefined) {
|
|
268
166
|
out.max_output_tokens = chat.max_tokens;
|
|
269
167
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (typeof val === 'object') {
|
|
280
|
-
if (typeof val.text === 'string')
|
|
281
|
-
return String(val.text);
|
|
282
|
-
if (Array.isArray(val.content))
|
|
283
|
-
return collectText(val.content);
|
|
284
|
-
if (typeof val.content === 'string')
|
|
285
|
-
return String(val.content);
|
|
286
|
-
}
|
|
287
|
-
return '';
|
|
288
|
-
};
|
|
289
|
-
const extractUserTextFromEntry = (entry) => {
|
|
290
|
-
if (!entry || typeof entry !== 'object')
|
|
291
|
-
return '';
|
|
292
|
-
const directContent = entry.content ?? entry.message?.content;
|
|
293
|
-
if (typeof directContent === 'string')
|
|
294
|
-
return directContent.trim();
|
|
295
|
-
if (Array.isArray(directContent)) {
|
|
296
|
-
return directContent.map(block => collectText(block)).join('').trim();
|
|
297
|
-
}
|
|
298
|
-
const text = entry.text ?? entry.message?.text;
|
|
299
|
-
if (typeof text === 'string')
|
|
300
|
-
return text.trim();
|
|
301
|
-
return '';
|
|
302
|
-
};
|
|
303
|
-
const systemParts = [];
|
|
304
|
-
const originalSystemMessages = [];
|
|
305
|
-
let latestUserInstruction = null;
|
|
306
|
-
const pendingToolCallIds = [];
|
|
307
|
-
const knownToolCallIds = new Set();
|
|
308
|
-
const toolCallIdAliases = new Map();
|
|
309
|
-
// 将 system 消息折叠到 instructions,其余 role 映射到 input[].message.content 中
|
|
310
|
-
for (const m of messages) {
|
|
311
|
-
if (!m || typeof m !== 'object')
|
|
312
|
-
continue;
|
|
313
|
-
const role = String(m.role || '').toLowerCase();
|
|
314
|
-
const content = m.content;
|
|
315
|
-
const collectedText = collectText(content);
|
|
316
|
-
const text = role === 'system' ? collectedText : collectedText.trim();
|
|
317
|
-
if (role === 'system') {
|
|
318
|
-
if (collectedText && collectedText.length) {
|
|
319
|
-
originalSystemMessages.push(collectedText);
|
|
320
|
-
systemParts.push(collectedText);
|
|
321
|
-
}
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
// tool 结果消息:Chat 中 role='tool' 的消息,应回写为 Responses 的 function_call_output 输入块,
|
|
325
|
-
// 以便 FC 等上游识别为工具结果,而不是普通 message(其 role 不接受 'tool')。
|
|
326
|
-
if (role === 'tool') {
|
|
327
|
-
const rawToolId = m.tool_call_id ||
|
|
328
|
-
m.call_id ||
|
|
329
|
-
m.tool_use_id ||
|
|
330
|
-
m.id ||
|
|
331
|
-
undefined;
|
|
332
|
-
let callId = typeof rawToolId === 'string' ? rawToolId : undefined;
|
|
333
|
-
if (callId && toolCallIdAliases.has(callId)) {
|
|
334
|
-
callId = toolCallIdAliases.get(callId);
|
|
335
|
-
}
|
|
336
|
-
if (!callId || !knownToolCallIds.has(callId)) {
|
|
337
|
-
const fallbackId = pendingToolCallIds.length ? pendingToolCallIds.shift() : undefined;
|
|
338
|
-
if (fallbackId) {
|
|
339
|
-
if (callId && callId !== fallbackId) {
|
|
340
|
-
toolCallIdAliases.set(callId, fallbackId);
|
|
341
|
-
}
|
|
342
|
-
callId = fallbackId;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
else if (callId) {
|
|
346
|
-
const idx = pendingToolCallIds.indexOf(callId);
|
|
347
|
-
if (idx >= 0)
|
|
348
|
-
pendingToolCallIds.splice(idx, 1);
|
|
349
|
-
}
|
|
350
|
-
const normalizedCallId = normalizeFunctionCallId({
|
|
351
|
-
callId,
|
|
352
|
-
fallback: `fc_call_${input.length + 1}`
|
|
168
|
+
let messages = Array.isArray(chat.messages) ? chat.messages : [];
|
|
169
|
+
let bridgeMetadata;
|
|
170
|
+
try {
|
|
171
|
+
const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-responses', moduleType: 'openai-responses' });
|
|
172
|
+
const policyActions = resolvePolicyActions(bridgePolicy, 'request_outbound');
|
|
173
|
+
if (policyActions?.length) {
|
|
174
|
+
const actionState = createBridgeActionState({
|
|
175
|
+
messages,
|
|
176
|
+
rawRequest: chat
|
|
353
177
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
178
|
+
runBridgeActionPipeline({
|
|
179
|
+
stage: 'request_outbound',
|
|
180
|
+
actions: policyActions,
|
|
181
|
+
protocol: bridgePolicy?.protocol ?? 'openai-responses',
|
|
182
|
+
moduleType: bridgePolicy?.moduleType ?? 'openai-responses',
|
|
183
|
+
requestId: ctx?.requestId,
|
|
184
|
+
state: actionState
|
|
360
185
|
});
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
call_id: normalizedCallId,
|
|
365
|
-
output: text || undefined
|
|
366
|
-
};
|
|
367
|
-
input.push(entry);
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
// tool_calls: 将 Chat 中的 tool_calls 归一为 function_call/tool_call 输入块。
|
|
371
|
-
// Responses V3 要求 call_id 稳定并与 Chat payload 完全一致,以便多轮工具结果匹配。
|
|
372
|
-
// 因此这里不再改写 id,仅在缺失时生成随机 call_id。
|
|
373
|
-
const toolCalls = Array.isArray(m.tool_calls) ? m.tool_calls : [];
|
|
374
|
-
if (toolCalls.length) {
|
|
375
|
-
for (const tc of toolCalls) {
|
|
376
|
-
try {
|
|
377
|
-
const rawId = typeof tc?.id === 'string' && tc.id
|
|
378
|
-
? String(tc.id)
|
|
379
|
-
: `call_${Math.random().toString(36).slice(2, 8)}`;
|
|
380
|
-
const normalizedCallId = normalizeFunctionCallId({
|
|
381
|
-
callId: rawId,
|
|
382
|
-
fallback: `fc_call_${input.length + 1}`
|
|
383
|
-
});
|
|
384
|
-
if (rawId !== normalizedCallId) {
|
|
385
|
-
toolCallIdAliases.set(rawId, normalizedCallId);
|
|
386
|
-
}
|
|
387
|
-
const fn = tc?.function || {};
|
|
388
|
-
const name = typeof fn?.name === 'string' && fn.name.trim()
|
|
389
|
-
? String(fn.name).trim()
|
|
390
|
-
: 'tool';
|
|
391
|
-
const argsRaw = fn?.arguments;
|
|
392
|
-
const args = typeof argsRaw === 'string' ? argsRaw : normalizeArgumentsBySchema(argsRaw, name, out.tools);
|
|
393
|
-
const entry = {
|
|
394
|
-
type: 'function_call',
|
|
395
|
-
id: normalizedCallId,
|
|
396
|
-
call_id: normalizedCallId,
|
|
397
|
-
name,
|
|
398
|
-
arguments: args
|
|
399
|
-
};
|
|
400
|
-
input.push(entry);
|
|
401
|
-
knownToolCallIds.add(normalizedCallId);
|
|
402
|
-
pendingToolCallIds.push(normalizedCallId);
|
|
403
|
-
}
|
|
404
|
-
catch {
|
|
405
|
-
// ignore single tool_call
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
// 普通 user/assistant/tool 消息 → message 块
|
|
411
|
-
// FC 在 user 输入块上要求 content[*].type 是 input_text/input_image/input_file,
|
|
412
|
-
// 在 assistant 历史消息上要求 output_text/refusal。
|
|
413
|
-
if (text) {
|
|
414
|
-
const tRole = role === 'assistant' ? 'output_text' : 'input_text';
|
|
415
|
-
const entry = {
|
|
416
|
-
type: 'message',
|
|
417
|
-
role,
|
|
418
|
-
content: [{ type: tRole, text }]
|
|
419
|
-
};
|
|
420
|
-
input.push(entry);
|
|
421
|
-
if (role === 'user' && text) {
|
|
422
|
-
latestUserInstruction = text;
|
|
186
|
+
messages = actionState.messages;
|
|
187
|
+
if (actionState.metadata && Object.keys(actionState.metadata).length) {
|
|
188
|
+
bridgeMetadata = actionState.metadata;
|
|
423
189
|
}
|
|
424
190
|
}
|
|
425
191
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
192
|
+
catch {
|
|
193
|
+
// ignore policy errors
|
|
194
|
+
}
|
|
195
|
+
const envelopeMetadata = ctx?.metadata && typeof ctx.metadata === 'object' ? ctx.metadata : undefined;
|
|
196
|
+
const toolCallIdStyle = resolveToolCallIdStyle(envelopeMetadata);
|
|
197
|
+
const historySeed = normalizeBridgeHistory(bridgeMetadata?.bridgeHistory ?? envelopeMetadata?.bridgeHistory ?? extras?.bridgeHistory);
|
|
198
|
+
const history = historySeed ??
|
|
199
|
+
convertMessagesToBridgeInput({
|
|
200
|
+
messages,
|
|
201
|
+
tools: Array.isArray(out.tools) ? out.tools : undefined
|
|
202
|
+
});
|
|
203
|
+
const callIdTransformer = createToolCallIdTransformer(toolCallIdStyle);
|
|
204
|
+
if (callIdTransformer) {
|
|
205
|
+
enforceToolCallIdStyle(history.input, callIdTransformer);
|
|
206
|
+
}
|
|
207
|
+
const { input, combinedSystemInstruction, originalSystemMessages } = history;
|
|
208
|
+
const instructionCandidates = [
|
|
209
|
+
typeof extras?.systemInstruction === 'string' && extras.systemInstruction.trim().length ? extras.systemInstruction.trim() : undefined,
|
|
210
|
+
typeof envelopeMetadata?.systemInstruction === 'string' && envelopeMetadata.systemInstruction.trim().length ? envelopeMetadata.systemInstruction.trim() : undefined,
|
|
211
|
+
combinedSystemInstruction && combinedSystemInstruction.length > 0 ? combinedSystemInstruction : undefined
|
|
212
|
+
];
|
|
213
|
+
for (const candidate of instructionCandidates) {
|
|
214
|
+
if (candidate && candidate.length) {
|
|
215
|
+
out.instructions = candidate;
|
|
216
|
+
break;
|
|
438
217
|
}
|
|
439
218
|
}
|
|
440
|
-
const combinedSystemInstruction = systemParts.join('\n\n').trim();
|
|
441
|
-
if (combinedSystemInstruction.length > 0) {
|
|
442
|
-
out.instructions = combinedSystemInstruction;
|
|
443
|
-
}
|
|
444
|
-
else if (typeof ctx?.instructions === 'string' && ctx.instructions.trim().length) {
|
|
445
|
-
out.instructions = ctx.instructions.trim();
|
|
446
|
-
}
|
|
447
|
-
if (!out.instructions && typeof latestUserInstruction === 'string' && latestUserInstruction.trim().length) {
|
|
448
|
-
out.instructions = latestUserInstruction.trim();
|
|
449
|
-
}
|
|
450
|
-
if (ctx?.instructionsIsRaw === true) {
|
|
451
|
-
out.instructions_is_raw = true;
|
|
452
|
-
}
|
|
453
|
-
else if ('instructions_is_raw' in out) {
|
|
454
|
-
delete out.instructions_is_raw;
|
|
455
|
-
}
|
|
456
219
|
// 不追加 metadata,以便 roundtrip 与原始 payload 对齐;系统提示直接写入 instructions。
|
|
457
220
|
if (input.length) {
|
|
458
221
|
out.input = input;
|
|
@@ -470,7 +233,7 @@ export function buildResponsesRequestFromChat(payload, ctx) {
|
|
|
470
233
|
else if (resolvedStream === false) {
|
|
471
234
|
out.stream = false;
|
|
472
235
|
}
|
|
473
|
-
|
|
236
|
+
ensureBridgeInstructions(out);
|
|
474
237
|
return { request: out, originalSystemMessages };
|
|
475
238
|
}
|
|
476
239
|
export function buildResponsesPayloadFromChat(payload, context) {
|
|
@@ -479,571 +242,79 @@ export function buildResponsesPayloadFromChat(payload, context) {
|
|
|
479
242
|
const response = unwrapData(payload);
|
|
480
243
|
if (!response || typeof response !== 'object')
|
|
481
244
|
return payload;
|
|
482
|
-
// If provider already returned Responses shape, sanitize textual output to remove tool result envelopes
|
|
483
245
|
if (response.object === 'response' && Array.isArray(response.output)) {
|
|
484
|
-
try {
|
|
485
|
-
const outArr = response.output;
|
|
486
|
-
for (const item of outArr) {
|
|
487
|
-
if (!item || typeof item !== 'object')
|
|
488
|
-
continue;
|
|
489
|
-
const t = String(item.type || '').toLowerCase();
|
|
490
|
-
if (t === 'message' && item.message && typeof item.message === 'object') {
|
|
491
|
-
const msg = item.message;
|
|
492
|
-
const content = msg.content;
|
|
493
|
-
if (Array.isArray(content)) {
|
|
494
|
-
for (const part of content) {
|
|
495
|
-
if (part && typeof part === 'object' && String(part.type || '').toLowerCase() === 'output_text' && typeof part.text === 'string') {
|
|
496
|
-
// rcc.tool.v1 剥离已移除
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
else if (typeof content === 'string') {
|
|
501
|
-
// rcc.tool.v1 剥离已移除
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
if (t === 'reasoning' && Array.isArray(item.content)) {
|
|
505
|
-
for (const part of item.content) {
|
|
506
|
-
if (part && typeof part === 'object' && String(part.type || '').toLowerCase() === 'output_text' && typeof part.text === 'string') {
|
|
507
|
-
// rcc.tool.v1 剥离已移除
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
if (typeof response.output_text === 'string') {
|
|
513
|
-
// rcc.tool.v1 剥离已移除
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
catch { /* ignore sanitize errors */ }
|
|
517
246
|
return response;
|
|
518
247
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
let responseForBridge = response;
|
|
522
|
-
try {
|
|
523
|
-
const maybeType = String(response.type || '').toLowerCase();
|
|
524
|
-
if (maybeType === 'message' && Array.isArray(response.content) && !Array.isArray(response.choices)) {
|
|
525
|
-
const msg = response;
|
|
526
|
-
const contentArr = Array.isArray(msg.content) ? msg.content : [];
|
|
527
|
-
const textParts = [];
|
|
528
|
-
const toolCalls = [];
|
|
529
|
-
for (const b of contentArr) {
|
|
530
|
-
if (!b || typeof b !== 'object')
|
|
531
|
-
continue;
|
|
532
|
-
const t = String(b.type || '').toLowerCase();
|
|
533
|
-
if (t === 'text' && typeof b.text === 'string') {
|
|
534
|
-
const s = b.text;
|
|
535
|
-
if (s && s.trim().length)
|
|
536
|
-
textParts.push(s);
|
|
537
|
-
}
|
|
538
|
-
else if (t === 'tool_use') {
|
|
539
|
-
const name = typeof b.name === 'string' ? String(b.name) : '';
|
|
540
|
-
const id = typeof b.id === 'string' ? String(b.id) : `call_${Math.random().toString(36).slice(2, 10)}`;
|
|
541
|
-
let args = '';
|
|
542
|
-
const input = b.input;
|
|
543
|
-
if (typeof input === 'string')
|
|
544
|
-
args = input;
|
|
545
|
-
else {
|
|
546
|
-
try {
|
|
547
|
-
args = JSON.stringify(input ?? {});
|
|
548
|
-
}
|
|
549
|
-
catch {
|
|
550
|
-
args = '{}';
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
if (name)
|
|
554
|
-
toolCalls.push({ id, name, args });
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
const mapStopToFinish = (sr) => {
|
|
558
|
-
const v = String(sr || '');
|
|
559
|
-
if (v === 'tool_use')
|
|
560
|
-
return 'tool_calls';
|
|
561
|
-
if (v === 'max_tokens')
|
|
562
|
-
return 'length';
|
|
563
|
-
if (v === 'stop_sequence')
|
|
564
|
-
return 'content_filter';
|
|
565
|
-
return 'stop';
|
|
566
|
-
};
|
|
567
|
-
const finish_reason = mapStopToFinish(msg.stop_reason);
|
|
568
|
-
const chatMsg = {
|
|
569
|
-
role: msg.role || 'assistant',
|
|
570
|
-
content: textParts.join('\n')
|
|
571
|
-
};
|
|
572
|
-
if (toolCalls.length) {
|
|
573
|
-
chatMsg.tool_calls = toolCalls.map(tc => ({
|
|
574
|
-
id: tc.id,
|
|
575
|
-
type: 'function',
|
|
576
|
-
function: { name: tc.name, arguments: tc.args }
|
|
577
|
-
}));
|
|
578
|
-
}
|
|
579
|
-
const usageIn = msg.usage || {};
|
|
580
|
-
responseForBridge = {
|
|
581
|
-
id: msg.id || `chatcmpl_${Date.now()}`,
|
|
582
|
-
object: 'chat.completion',
|
|
583
|
-
model: msg.model || 'unknown',
|
|
584
|
-
choices: [
|
|
585
|
-
{
|
|
586
|
-
index: 0,
|
|
587
|
-
finish_reason,
|
|
588
|
-
message: chatMsg
|
|
589
|
-
}
|
|
590
|
-
],
|
|
591
|
-
usage: usageIn
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
catch {
|
|
596
|
-
responseForBridge = response;
|
|
248
|
+
if (!Array.isArray(response.choices) || !response.choices.length) {
|
|
249
|
+
throw new Error('Responses bridge expects OpenAI Chat completion payload');
|
|
597
250
|
}
|
|
598
|
-
|
|
599
|
-
const canonical = canonicalizeChatResponseTools(responseForBridge);
|
|
251
|
+
const canonical = canonicalizeChatResponseTools(response);
|
|
600
252
|
const choices = Array.isArray(canonical?.choices) ? canonical.choices : [];
|
|
601
253
|
const primaryChoice = choices[0] && typeof choices[0] === 'object' ? choices[0] : undefined;
|
|
602
254
|
const message = primaryChoice && typeof primaryChoice.message === 'object' ? primaryChoice.message : undefined;
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const reasoningText = typeof message?.reasoning_content === 'string' && message.reasoning_content.trim().length
|
|
606
|
-
? String(message.reasoning_content).trim() : undefined;
|
|
607
|
-
const outputItems = [];
|
|
608
|
-
const allocateOutputId = (prefix) => `${prefix}_${context?.requestId ?? Date.now()}_${outputItems.length + 1}`;
|
|
609
|
-
if (reasoningText) {
|
|
610
|
-
outputItems.push({
|
|
611
|
-
id: allocateOutputId('reasoning'),
|
|
612
|
-
type: 'reasoning',
|
|
613
|
-
status: 'completed',
|
|
614
|
-
summary: [],
|
|
615
|
-
content: [{ type: 'reasoning_text', text: reasoningText }]
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
const convertedContent = convertChatContentToResponses(content);
|
|
619
|
-
if (convertedContent.length > 0) {
|
|
620
|
-
outputItems.push({
|
|
621
|
-
id: allocateOutputId('message'),
|
|
622
|
-
type: 'message',
|
|
623
|
-
status: 'completed',
|
|
624
|
-
role,
|
|
625
|
-
content: convertedContent
|
|
626
|
-
});
|
|
255
|
+
if (!message) {
|
|
256
|
+
throw new Error('Responses bridge could not locate assistant message in Chat completion');
|
|
627
257
|
}
|
|
628
|
-
|
|
629
|
-
const toolCallIdMap = new Map();
|
|
630
|
-
// 过滤无效函数调用(name 缺失或为空)
|
|
631
|
-
try {
|
|
632
|
-
toolCalls = toolCalls.filter((it) => {
|
|
633
|
-
const nm = (it && typeof it === 'object') ? (it?.function?.name || it.name) : undefined;
|
|
634
|
-
return typeof nm === 'string' && nm.trim().length > 0 && nm.toLowerCase() !== 'tool';
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
catch { /* ignore */ }
|
|
638
|
-
for (const call of toolCalls) {
|
|
258
|
+
if (message) {
|
|
639
259
|
try {
|
|
640
|
-
const
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
catch {
|
|
654
|
-
return '{}';
|
|
655
|
-
}
|
|
656
|
-
})();
|
|
657
|
-
const originalCallId = typeof call.id === 'string' && call.id.trim().length
|
|
658
|
-
? String(call.id)
|
|
659
|
-
: (typeof call.call_id === 'string' && call.call_id.trim().length
|
|
660
|
-
? String(call.call_id)
|
|
661
|
-
: allocateOutputId('tool_call'));
|
|
662
|
-
const callId = normalizeFunctionCallId({
|
|
663
|
-
callId: originalCallId,
|
|
664
|
-
fallback: `fc_call_${outputItems.length + 1}`
|
|
665
|
-
});
|
|
666
|
-
if (originalCallId && originalCallId !== callId) {
|
|
667
|
-
toolCallIdMap.set(originalCallId, callId);
|
|
260
|
+
const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-responses', moduleType: 'openai-responses' });
|
|
261
|
+
const policyActions = resolvePolicyActions(bridgePolicy, 'response_outbound');
|
|
262
|
+
if (policyActions?.length) {
|
|
263
|
+
const actionState = createBridgeActionState({ messages: [message] });
|
|
264
|
+
runBridgeActionPipeline({
|
|
265
|
+
stage: 'response_outbound',
|
|
266
|
+
actions: policyActions,
|
|
267
|
+
protocol: bridgePolicy?.protocol ?? 'openai-responses',
|
|
268
|
+
moduleType: bridgePolicy?.moduleType ?? 'openai-responses',
|
|
269
|
+
requestId: context?.requestId,
|
|
270
|
+
state: actionState
|
|
271
|
+
});
|
|
668
272
|
}
|
|
669
|
-
toolCallIdMap.set(callId, callId);
|
|
670
|
-
outputItems.push({
|
|
671
|
-
id: allocateOutputId('function_call'),
|
|
672
|
-
type: 'function_call',
|
|
673
|
-
status: 'in_progress',
|
|
674
|
-
name: callName,
|
|
675
|
-
call_id: callId,
|
|
676
|
-
arguments: argsStr
|
|
677
|
-
});
|
|
678
273
|
}
|
|
679
274
|
catch {
|
|
680
|
-
//
|
|
275
|
+
// best-effort bridge policy application
|
|
681
276
|
}
|
|
682
277
|
}
|
|
683
|
-
|
|
684
|
-
let usage = undefined;
|
|
685
|
-
if (usageRaw && typeof usageRaw === 'object') {
|
|
686
|
-
usage = { ...usageRaw };
|
|
687
|
-
if (usage.input_tokens != null && usage.prompt_tokens == null) {
|
|
688
|
-
usage.prompt_tokens = usage.input_tokens;
|
|
689
|
-
}
|
|
690
|
-
if (usage.output_tokens != null && usage.completion_tokens == null) {
|
|
691
|
-
usage.completion_tokens = usage.output_tokens;
|
|
692
|
-
}
|
|
693
|
-
if (usage.prompt_tokens != null && usage.completion_tokens != null && usage.total_tokens == null) {
|
|
694
|
-
const total = Number(usage.prompt_tokens) + Number(usage.completion_tokens);
|
|
695
|
-
if (!Number.isNaN(total))
|
|
696
|
-
usage.total_tokens = total;
|
|
697
|
-
}
|
|
278
|
+
if (message) {
|
|
698
279
|
try {
|
|
699
|
-
|
|
700
|
-
|
|
280
|
+
normalizeMessageReasoningTools(message, {
|
|
281
|
+
idPrefix: `responses_reasoning_${context?.requestId ?? 'canonical'}`
|
|
282
|
+
});
|
|
701
283
|
}
|
|
702
|
-
catch {
|
|
703
|
-
|
|
704
|
-
else if (usageRaw !== undefined) {
|
|
705
|
-
usage = usageRaw;
|
|
706
|
-
}
|
|
707
|
-
const outputText = extractOutputText(convertedContent, toolCalls);
|
|
708
|
-
const hasToolCalls = toolCalls.length > 0;
|
|
709
|
-
const status = hasToolCalls ? 'requires_action' : 'completed';
|
|
710
|
-
if (hasToolCalls) {
|
|
711
|
-
for (const item of outputItems) {
|
|
712
|
-
if (item.type === 'message') {
|
|
713
|
-
item.status = 'in_progress';
|
|
714
|
-
}
|
|
284
|
+
catch {
|
|
285
|
+
// best-effort reasoning normalization
|
|
715
286
|
}
|
|
716
287
|
}
|
|
288
|
+
const outputBuild = buildResponsesOutputFromChat({
|
|
289
|
+
response: response,
|
|
290
|
+
message,
|
|
291
|
+
requestId: context?.requestId,
|
|
292
|
+
sanitizeFunctionName: sanitizeResponsesFunctionName
|
|
293
|
+
});
|
|
717
294
|
const out = {
|
|
718
295
|
id: response.id || `resp-${Date.now()}`,
|
|
719
296
|
object: 'response',
|
|
720
297
|
created_at: response.created_at || response.created || Math.floor(Date.now() / 1000),
|
|
721
298
|
model: response.model,
|
|
722
|
-
status,
|
|
723
|
-
output: outputItems,
|
|
724
|
-
output_text: outputText || ''
|
|
299
|
+
status: outputBuild.status,
|
|
300
|
+
output: outputBuild.outputItems,
|
|
301
|
+
output_text: outputBuild.outputText || ''
|
|
725
302
|
};
|
|
726
|
-
if (usage !== undefined)
|
|
727
|
-
out.usage = usage;
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (toolCalls.length > 0) {
|
|
731
|
-
const toStringJson = (v) => { if (typeof v === 'string')
|
|
732
|
-
return v; try {
|
|
733
|
-
return JSON.stringify(v ?? {});
|
|
734
|
-
}
|
|
735
|
-
catch {
|
|
736
|
-
return '{}';
|
|
737
|
-
} };
|
|
738
|
-
const callsAll = toolCalls.filter((it) => {
|
|
739
|
-
const nm = (it && typeof it === 'object') ? (it?.function?.name || it.name) : undefined;
|
|
740
|
-
return typeof nm === 'string' && nm.trim().length > 0 && nm.toLowerCase() !== 'tool';
|
|
741
|
-
}).map((it) => {
|
|
742
|
-
const rawId = typeof it.id === 'string' ? it.id : (typeof it.call_id === 'string' ? it.call_id : `call_${Math.random().toString(36).slice(2, 8)}`);
|
|
743
|
-
const id = toolCallIdMap.get(rawId) ?? normalizeFunctionCallId({ callId: rawId, fallback: `fc_call_${outputItems.length + 1}` });
|
|
744
|
-
const nm = typeof it?.function?.name === 'string' ? it.function.name : (typeof it.name === 'string' ? it.name : 'tool');
|
|
745
|
-
const rawArgs = it?.function?.arguments ?? it.arguments ?? {};
|
|
746
|
-
const argsStr = toStringJson(rawArgs);
|
|
747
|
-
return { id, type: 'function', function: { name: String(nm), arguments: String(argsStr) } };
|
|
748
|
-
});
|
|
749
|
-
// 仅做最小形状映射:不进行校验与归一,仅透传(字符串化)。
|
|
750
|
-
const calls = callsAll;
|
|
751
|
-
const required_action = { type: 'submit_tool_outputs', submit_tool_outputs: { tool_calls: calls } };
|
|
752
|
-
out.required_action = required_action;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
catch { /* ignore */ }
|
|
303
|
+
if (outputBuild.usage !== undefined)
|
|
304
|
+
out.usage = outputBuild.usage;
|
|
305
|
+
if (outputBuild.requiredAction)
|
|
306
|
+
out.required_action = outputBuild.requiredAction;
|
|
756
307
|
// Do not inject captured tool results here; keep Chat back-half behavior standard.
|
|
757
308
|
if (context) {
|
|
758
|
-
for (const k of ['metadata', '
|
|
309
|
+
for (const k of ['metadata', 'parallel_tool_calls', 'tool_choice', 'include', 'store']) {
|
|
759
310
|
if (context[k] !== undefined)
|
|
760
311
|
out[k] = context[k];
|
|
761
312
|
}
|
|
762
313
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
function mapResponsesInputToChat(options) {
|
|
766
|
-
const { instructions, input, toolsNormalized } = options;
|
|
767
|
-
const messages = [];
|
|
768
|
-
if (typeof instructions === 'string') {
|
|
769
|
-
const trimmed = instructions.trim();
|
|
770
|
-
if (trimmed.length) {
|
|
771
|
-
messages.push({ role: 'system', content: trimmed });
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
if (!Array.isArray(input))
|
|
775
|
-
return messages;
|
|
776
|
-
const toolNameById = new Map();
|
|
777
|
-
const toolArgsById = new Map();
|
|
778
|
-
const toolCallIdAliases = new Map();
|
|
779
|
-
let lastToolCallId = null;
|
|
780
|
-
for (const entry of input) {
|
|
781
|
-
if (!entry || typeof entry !== 'object')
|
|
782
|
-
continue;
|
|
783
|
-
const entryType = typeof entry.type === 'string' ? entry.type.toLowerCase() : 'message';
|
|
784
|
-
if (typeof entry.content === 'string') {
|
|
785
|
-
const directText = (entry.content || '').toString().trim();
|
|
786
|
-
if (directText.length) {
|
|
787
|
-
const normalizedRole = normalizeResponseRole(entry.role || 'user');
|
|
788
|
-
messages.push({ role: normalizedRole, content: directText });
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
if (entryType === 'function_call' || entryType === 'tool_call') {
|
|
793
|
-
const rawName = typeof entry.name === 'string' ? entry.name : (typeof entry?.function?.name === 'string' ? entry.function.name : undefined);
|
|
794
|
-
const name = (typeof rawName === 'string' && rawName.includes('.')) ? rawName.slice(rawName.indexOf('.') + 1).trim() : rawName;
|
|
795
|
-
if (typeof name !== 'string' || !name.trim() || name.toLowerCase() === 'tool') {
|
|
796
|
-
// 无效函数名:不生成 tool_calls,避免污染会话
|
|
797
|
-
continue;
|
|
798
|
-
}
|
|
799
|
-
const args = entry.arguments ?? entry?.function?.arguments ?? {};
|
|
800
|
-
const parsedArgs = args; // 不解析,原样透传(对象在后续 stringify)
|
|
801
|
-
const callIdRaw = typeof entry.id === 'string' ? entry.id : (typeof entry.call_id === 'string' ? entry.call_id : `call_${Math.random().toString(36).slice(2, 8)}`);
|
|
802
|
-
const callId = normalizeFunctionCallId({
|
|
803
|
-
callId: callIdRaw,
|
|
804
|
-
fallback: `fc_call_${messages.length + 1}`
|
|
805
|
-
});
|
|
806
|
-
if (callIdRaw && callIdRaw !== callId) {
|
|
807
|
-
toolCallIdAliases.set(callIdRaw, callId);
|
|
808
|
-
}
|
|
809
|
-
const serialized = normalizeArgumentsBySchema(parsedArgs, name, toolsNormalized).trim();
|
|
810
|
-
toolNameById.set(callId, name);
|
|
811
|
-
toolArgsById.set(callId, serialized);
|
|
812
|
-
messages.push({ role: 'assistant', tool_calls: [{ id: callId, type: 'function', function: { name, arguments: serialized } }] });
|
|
813
|
-
lastToolCallId = callId;
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
if (entryType === 'function_call_output' || entryType === 'tool_result' || entryType === 'tool_message') {
|
|
817
|
-
let toolCallId = entry.tool_call_id || entry.call_id || entry.tool_use_id || entry.id || lastToolCallId;
|
|
818
|
-
if (toolCallId && toolCallIdAliases.has(String(toolCallId))) {
|
|
819
|
-
toolCallId = toolCallIdAliases.get(String(toolCallId)) || toolCallId;
|
|
820
|
-
}
|
|
821
|
-
else if (typeof toolCallId === 'string' && toolCallId.trim().length) {
|
|
822
|
-
const normalized = normalizeFunctionCallId({ callId: toolCallId, fallback: toolCallId });
|
|
823
|
-
if (normalized !== toolCallId) {
|
|
824
|
-
toolCallIdAliases.set(toolCallId, normalized);
|
|
825
|
-
}
|
|
826
|
-
toolCallId = normalized;
|
|
827
|
-
}
|
|
828
|
-
const output = normalizeToolOutput(entry);
|
|
829
|
-
if (toolCallId) {
|
|
830
|
-
try {
|
|
831
|
-
// Emit a standard tool role message so downstream Chat back-half can govern correctly
|
|
832
|
-
let contentStr = output != null ? String(output) : '';
|
|
833
|
-
if (!contentStr || contentStr.trim().length === 0) {
|
|
834
|
-
contentStr = 'Command succeeded (no output).';
|
|
835
|
-
}
|
|
836
|
-
const nm = toolNameById.get(String(toolCallId));
|
|
837
|
-
const toolMsg = { role: 'tool', tool_call_id: String(toolCallId), content: contentStr };
|
|
838
|
-
if (typeof nm === 'string' && nm.trim().length)
|
|
839
|
-
toolMsg.name = nm;
|
|
840
|
-
messages.push(toolMsg);
|
|
841
|
-
}
|
|
842
|
-
catch {
|
|
843
|
-
const fallback = (output ?? '');
|
|
844
|
-
const nm = toolNameById.get(String(toolCallId));
|
|
845
|
-
const toolMsg = { role: 'tool', tool_call_id: String(toolCallId), content: String(fallback) };
|
|
846
|
-
if (typeof nm === 'string' && nm.trim().length)
|
|
847
|
-
toolMsg.name = nm;
|
|
848
|
-
messages.push(toolMsg);
|
|
849
|
-
}
|
|
850
|
-
lastToolCallId = null;
|
|
851
|
-
}
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
// Prefer explicit message shape when present: entry.message.{role,content[]}
|
|
855
|
-
let handledViaExplicitMessage = false;
|
|
856
|
-
if (entry && typeof entry.message === 'object' && Array.isArray(entry.message?.content)) {
|
|
857
|
-
const explicit = entry.message;
|
|
858
|
-
const { text, toolCalls, toolMessages, lastCallId } = processMessageBlocks(Array.isArray(explicit.content) ? explicit.content : [], toolsNormalized, toolNameById, toolCallIdAliases, lastToolCallId);
|
|
859
|
-
if (toolCalls.length)
|
|
860
|
-
messages.push({ role: 'assistant', tool_calls: toolCalls });
|
|
861
|
-
for (const msg of toolMessages)
|
|
862
|
-
messages.push(msg);
|
|
863
|
-
const normalizedRole = normalizeResponseRole((explicit.role ?? entry.role) || 'user');
|
|
864
|
-
if (typeof text === 'string' && text.trim().length) {
|
|
865
|
-
messages.push({ role: normalizedRole, content: text.trim() });
|
|
866
|
-
}
|
|
867
|
-
lastToolCallId = lastCallId;
|
|
868
|
-
handledViaExplicitMessage = true;
|
|
869
|
-
}
|
|
870
|
-
// Otherwise, consume entry.content[] if present
|
|
871
|
-
if (!handledViaExplicitMessage) {
|
|
872
|
-
const { text, toolCalls, toolMessages, lastCallId } = processMessageBlocks(Array.isArray(entry.content) ? entry.content : [], toolsNormalized, toolNameById, toolCallIdAliases, lastToolCallId);
|
|
873
|
-
if (toolCalls.length)
|
|
874
|
-
messages.push({ role: 'assistant', tool_calls: toolCalls });
|
|
875
|
-
for (const msg of toolMessages)
|
|
876
|
-
messages.push(msg);
|
|
877
|
-
const normalizedRole = normalizeResponseRole(entry.role || 'user');
|
|
878
|
-
if (typeof text === 'string' && text.trim().length) {
|
|
879
|
-
messages.push({ role: normalizedRole, content: text.trim() });
|
|
880
|
-
}
|
|
881
|
-
lastToolCallId = lastCallId;
|
|
882
|
-
}
|
|
883
|
-
// Finally, handle top-level text items (e.g., { type: 'input_text', text: '...' })
|
|
884
|
-
try {
|
|
885
|
-
const t = String(entry.type || '').toLowerCase();
|
|
886
|
-
if ((t === 'input_text' || t === 'text' || t === 'output_text' || t === 'commentary') && typeof entry.text === 'string') {
|
|
887
|
-
const normalizedRole = normalizeResponseRole(entry.role || 'user');
|
|
888
|
-
const s = entry.text;
|
|
889
|
-
if (s && s.length)
|
|
890
|
-
messages.push({ role: normalizedRole, content: s });
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
catch { /* ignore */ }
|
|
894
|
-
}
|
|
895
|
-
return messages;
|
|
896
|
-
}
|
|
897
|
-
function normalizeResponseRole(role) {
|
|
898
|
-
if (typeof role === 'string') {
|
|
899
|
-
const normalized = role.toLowerCase();
|
|
900
|
-
if (normalized === 'system' || normalized === 'assistant' || normalized === 'user' || normalized === 'tool')
|
|
901
|
-
return normalized;
|
|
902
|
-
}
|
|
903
|
-
return 'user';
|
|
904
|
-
}
|
|
905
|
-
function processMessageBlocks(blocks, toolsNormalized, toolNameById, toolCallIdAliases, lastToolCallId) {
|
|
906
|
-
const textParts = [];
|
|
907
|
-
const toolCalls = [];
|
|
908
|
-
const toolMessages = [];
|
|
909
|
-
let currentLastCall = lastToolCallId;
|
|
910
|
-
const toolArgsByIdLocal = new Map();
|
|
911
|
-
for (const block of blocks) {
|
|
912
|
-
if (!block || typeof block !== 'object')
|
|
913
|
-
continue;
|
|
914
|
-
const type = typeof block.type === 'string' ? block.type.toLowerCase() : '';
|
|
915
|
-
if (type === 'input_text' || type === 'output_text' || type === 'text' || type === 'commentary') {
|
|
916
|
-
if (typeof block.text === 'string')
|
|
917
|
-
textParts.push(block.text);
|
|
918
|
-
else if (typeof block.content === 'string')
|
|
919
|
-
textParts.push(block.content);
|
|
920
|
-
continue;
|
|
921
|
-
}
|
|
922
|
-
if (type === 'message' && Array.isArray(block.content)) {
|
|
923
|
-
const nested = processMessageBlocks(block.content, toolsNormalized, toolNameById, toolCallIdAliases, currentLastCall);
|
|
924
|
-
if (nested.text)
|
|
925
|
-
textParts.push(nested.text);
|
|
926
|
-
for (const tc of nested.toolCalls)
|
|
927
|
-
toolCalls.push(tc);
|
|
928
|
-
for (const tm of nested.toolMessages)
|
|
929
|
-
toolMessages.push(tm);
|
|
930
|
-
currentLastCall = nested.lastCallId;
|
|
931
|
-
continue;
|
|
932
|
-
}
|
|
933
|
-
if (type === 'function_call') {
|
|
934
|
-
const rawName = typeof block.name === 'string' ? block.name : (typeof block?.function?.name === 'string' ? block.function.name : undefined);
|
|
935
|
-
const name = (typeof rawName === 'string' && rawName.includes('.')) ? rawName.slice(rawName.indexOf('.') + 1).trim() : rawName;
|
|
936
|
-
const args = block.arguments ?? block?.function?.arguments ?? {};
|
|
937
|
-
const parsedArgs = args; // 不解析
|
|
938
|
-
const callIdRaw = typeof block.id === 'string' ? block.id : (typeof block.call_id === 'string' ? block.call_id : `call_${Math.random().toString(36).slice(2, 8)}`);
|
|
939
|
-
const callId = normalizeFunctionCallId({ callId: callIdRaw, fallback: `fc_call_${toolCalls.length + 1}` });
|
|
940
|
-
if (callIdRaw && callIdRaw !== callId) {
|
|
941
|
-
toolCallIdAliases.set(callIdRaw, callId);
|
|
942
|
-
}
|
|
943
|
-
if (typeof name !== 'string' || !name.trim()) {
|
|
944
|
-
// 缺少函数名:不生成 function_call,避免 downstream 进入错误循环
|
|
945
|
-
// 将内容留给文本路径或后续消息拼接处理
|
|
946
|
-
currentLastCall = null;
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
|
-
const serialized = normalizeArgumentsBySchema(parsedArgs, name, toolsNormalized).trim();
|
|
950
|
-
toolNameById.set(callId, name);
|
|
951
|
-
toolArgsByIdLocal.set(callId, serialized);
|
|
952
|
-
toolCalls.push({ id: callId, type: 'function', function: { name, arguments: serialized } });
|
|
953
|
-
currentLastCall = callId;
|
|
954
|
-
continue;
|
|
955
|
-
}
|
|
956
|
-
if (type === 'function_call_output' || type === 'tool_result' || type === 'tool_message') {
|
|
957
|
-
let toolCallId = block.tool_call_id || block.call_id || block.tool_use_id || block.id || currentLastCall;
|
|
958
|
-
if (toolCallId && toolCallIdAliases.has(String(toolCallId))) {
|
|
959
|
-
toolCallId = toolCallIdAliases.get(String(toolCallId)) || toolCallId;
|
|
960
|
-
}
|
|
961
|
-
else if (typeof toolCallId === 'string' && toolCallId.trim().length) {
|
|
962
|
-
const normalized = normalizeFunctionCallId({ callId: toolCallId, fallback: toolCallId });
|
|
963
|
-
if (normalized !== toolCallId) {
|
|
964
|
-
toolCallIdAliases.set(toolCallId, normalized);
|
|
965
|
-
}
|
|
966
|
-
toolCallId = normalized;
|
|
967
|
-
}
|
|
968
|
-
const output = normalizeToolOutput(block);
|
|
969
|
-
if (toolCallId) {
|
|
970
|
-
try {
|
|
971
|
-
// No rcc envelope: emit plain tool role message with text content (fallback message on empty)
|
|
972
|
-
let contentStr = output != null ? String(output) : '';
|
|
973
|
-
if (!contentStr || contentStr.trim().length === 0) {
|
|
974
|
-
contentStr = 'Command succeeded (no output).';
|
|
975
|
-
}
|
|
976
|
-
const nm = toolNameById.get(String(toolCallId));
|
|
977
|
-
const toolMsg = { role: 'tool', tool_call_id: String(toolCallId), content: contentStr };
|
|
978
|
-
if (typeof nm === 'string' && nm.trim().length)
|
|
979
|
-
toolMsg.name = nm;
|
|
980
|
-
toolMessages.push(toolMsg);
|
|
981
|
-
}
|
|
982
|
-
catch {
|
|
983
|
-
const fallback = (output ?? '');
|
|
984
|
-
const nm = toolNameById.get(String(toolCallId));
|
|
985
|
-
const toolMsg = { role: 'tool', tool_call_id: String(toolCallId), content: String(fallback) };
|
|
986
|
-
if (typeof nm === 'string' && nm.trim().length)
|
|
987
|
-
toolMsg.name = nm;
|
|
988
|
-
toolMessages.push(toolMsg);
|
|
989
|
-
}
|
|
990
|
-
currentLastCall = null;
|
|
991
|
-
}
|
|
992
|
-
continue;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
const text = textParts.length ? textParts.join('\n').trim() : null;
|
|
996
|
-
return { text, toolCalls, toolMessages, lastCallId: currentLastCall };
|
|
997
|
-
}
|
|
998
|
-
function sanitizeThink(s) {
|
|
999
|
-
try {
|
|
1000
|
-
let out = s.replace(/<think>[\s\S]*?<\/think>/gi, '');
|
|
1001
|
-
out = out.replace(/```\s*think[\s\S]*?```/gi, '');
|
|
1002
|
-
out = out.replace(/\n{3,}/g, '\n\n');
|
|
1003
|
-
return out.trim();
|
|
1004
|
-
}
|
|
1005
|
-
catch {
|
|
1006
|
-
return s;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
function convertChatContentToResponses(content) {
|
|
1010
|
-
if (content == null)
|
|
1011
|
-
return [];
|
|
1012
|
-
if (typeof content === 'string')
|
|
1013
|
-
return [{ type: 'output_text', text: content }];
|
|
1014
|
-
if (Array.isArray(content)) {
|
|
1015
|
-
return content.map((part) => {
|
|
1016
|
-
if (typeof part === 'string')
|
|
1017
|
-
return { type: 'output_text', text: sanitizeThink(part) };
|
|
1018
|
-
if (part && typeof part === 'object') {
|
|
1019
|
-
if (typeof part.text === 'string')
|
|
1020
|
-
return { type: 'output_text', text: sanitizeThink(part.text) };
|
|
1021
|
-
return { type: part.type || 'output_text', text: sanitizeThink(part.text ?? '') };
|
|
1022
|
-
}
|
|
1023
|
-
return { type: 'output_text', text: sanitizeThink(String(part)) };
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
if (typeof content === 'object') {
|
|
1027
|
-
try {
|
|
1028
|
-
return [{ type: 'output_text', text: JSON.stringify(content) }];
|
|
1029
|
-
}
|
|
1030
|
-
catch {
|
|
1031
|
-
return [{ type: 'output_text', text: String(content) }];
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
return [{ type: 'output_text', text: '' }];
|
|
1035
|
-
}
|
|
1036
|
-
function extractOutputText(parts, _toolCalls) {
|
|
1037
|
-
if (parts.length > 0) {
|
|
1038
|
-
const text = parts
|
|
1039
|
-
.filter(p => typeof p.text === 'string')
|
|
1040
|
-
.map(p => sanitizeThink(p.text))
|
|
1041
|
-
.join('\n')
|
|
1042
|
-
.trim();
|
|
1043
|
-
if (text.length)
|
|
1044
|
-
return text;
|
|
314
|
+
if (out.metadata) {
|
|
315
|
+
stripInternalToolingMetadata(out.metadata);
|
|
1045
316
|
}
|
|
1046
|
-
return
|
|
317
|
+
return out;
|
|
1047
318
|
}
|
|
1048
319
|
function unwrapData(value) {
|
|
1049
320
|
let current = value;
|
|
@@ -1068,265 +339,5 @@ export function extractRequestIdFromResponse(response) {
|
|
|
1068
339
|
}
|
|
1069
340
|
return undefined;
|
|
1070
341
|
}
|
|
342
|
+
export { buildChatResponseFromResponses } from '../shared/responses-response-utils.js';
|
|
1071
343
|
// (imports moved to top)
|
|
1072
|
-
function unwrapResponsesResponse(payload) {
|
|
1073
|
-
if (!payload || typeof payload !== 'object')
|
|
1074
|
-
return undefined;
|
|
1075
|
-
if (typeof payload.object === 'string' && payload.object === 'response') {
|
|
1076
|
-
return payload;
|
|
1077
|
-
}
|
|
1078
|
-
if (Array.isArray(payload.output) ||
|
|
1079
|
-
typeof payload.status === 'string' ||
|
|
1080
|
-
payload.required_action) {
|
|
1081
|
-
return payload;
|
|
1082
|
-
}
|
|
1083
|
-
let current = payload;
|
|
1084
|
-
const visited = new Set();
|
|
1085
|
-
while (current && typeof current === 'object' && !Array.isArray(current)) {
|
|
1086
|
-
if (visited.has(current))
|
|
1087
|
-
break;
|
|
1088
|
-
visited.add(current);
|
|
1089
|
-
if (typeof current.object === 'string' && current.object === 'response') {
|
|
1090
|
-
return current;
|
|
1091
|
-
}
|
|
1092
|
-
if (current.response && typeof current.response === 'object') {
|
|
1093
|
-
current = current.response;
|
|
1094
|
-
continue;
|
|
1095
|
-
}
|
|
1096
|
-
if (current.data && typeof current.data === 'object') {
|
|
1097
|
-
current = current.data;
|
|
1098
|
-
continue;
|
|
1099
|
-
}
|
|
1100
|
-
break;
|
|
1101
|
-
}
|
|
1102
|
-
return undefined;
|
|
1103
|
-
}
|
|
1104
|
-
function collectMessageTexts(blocks) {
|
|
1105
|
-
const parts = [];
|
|
1106
|
-
const pushText = (value) => {
|
|
1107
|
-
if (typeof value === 'string' && value.trim().length) {
|
|
1108
|
-
parts.push(sanitizeThink(value));
|
|
1109
|
-
}
|
|
1110
|
-
};
|
|
1111
|
-
if (typeof blocks === 'string') {
|
|
1112
|
-
pushText(blocks);
|
|
1113
|
-
return parts;
|
|
1114
|
-
}
|
|
1115
|
-
if (!Array.isArray(blocks))
|
|
1116
|
-
return parts;
|
|
1117
|
-
for (const block of blocks) {
|
|
1118
|
-
if (!block || typeof block !== 'object')
|
|
1119
|
-
continue;
|
|
1120
|
-
const type = typeof block.type === 'string' ? String(block.type).toLowerCase() : '';
|
|
1121
|
-
if (type === 'text' || type === 'input_text' || type === 'output_text' || type === 'commentary') {
|
|
1122
|
-
pushText(block.text ?? block.content);
|
|
1123
|
-
continue;
|
|
1124
|
-
}
|
|
1125
|
-
if (Array.isArray(block.content)) {
|
|
1126
|
-
parts.push(...collectMessageTexts(block.content));
|
|
1127
|
-
continue;
|
|
1128
|
-
}
|
|
1129
|
-
if (typeof block.text === 'string') {
|
|
1130
|
-
pushText(block.text);
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
return parts;
|
|
1134
|
-
}
|
|
1135
|
-
function collectResponseOutput(response) {
|
|
1136
|
-
const textParts = [];
|
|
1137
|
-
const reasoningParts = [];
|
|
1138
|
-
const outputItems = Array.isArray(response.output) ? response.output : [];
|
|
1139
|
-
for (const item of outputItems) {
|
|
1140
|
-
if (!item || typeof item !== 'object')
|
|
1141
|
-
continue;
|
|
1142
|
-
const type = typeof item.type === 'string' ? String(item.type).toLowerCase() : '';
|
|
1143
|
-
if (type === 'message') {
|
|
1144
|
-
const message = item.message && typeof item.message === 'object'
|
|
1145
|
-
? item.message
|
|
1146
|
-
: item;
|
|
1147
|
-
const content = Array.isArray(message.content) ? message.content : [];
|
|
1148
|
-
textParts.push(...collectMessageTexts(content));
|
|
1149
|
-
continue;
|
|
1150
|
-
}
|
|
1151
|
-
if (type === 'output_text') {
|
|
1152
|
-
if (typeof item.text === 'string') {
|
|
1153
|
-
textParts.push(sanitizeThink(item.text));
|
|
1154
|
-
}
|
|
1155
|
-
continue;
|
|
1156
|
-
}
|
|
1157
|
-
if (type === 'reasoning') {
|
|
1158
|
-
const content = Array.isArray(item.content) ? item.content : [];
|
|
1159
|
-
for (const block of content) {
|
|
1160
|
-
if (block && typeof block === 'object' && typeof block.text === 'string') {
|
|
1161
|
-
const sanitized = sanitizeThink(block.text);
|
|
1162
|
-
if (sanitized.length)
|
|
1163
|
-
reasoningParts.push(sanitized);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
return { textParts, reasoningParts };
|
|
1169
|
-
}
|
|
1170
|
-
function normalizeToolCallName(rawName) {
|
|
1171
|
-
if (typeof rawName !== 'string' || !rawName.trim().length)
|
|
1172
|
-
return undefined;
|
|
1173
|
-
if (!rawName.includes('.'))
|
|
1174
|
-
return rawName.trim();
|
|
1175
|
-
return rawName.slice(rawName.indexOf('.') + 1).trim() || undefined;
|
|
1176
|
-
}
|
|
1177
|
-
function selectCallId(entry) {
|
|
1178
|
-
const candidates = [
|
|
1179
|
-
entry?.call_id,
|
|
1180
|
-
entry?.tool_call_id,
|
|
1181
|
-
entry?.tool_use_id,
|
|
1182
|
-
entry?.id
|
|
1183
|
-
];
|
|
1184
|
-
for (const candidate of candidates) {
|
|
1185
|
-
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
1186
|
-
return candidate;
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
return undefined;
|
|
1190
|
-
}
|
|
1191
|
-
function normalizeToolCall(entry, fallbackPrefix) {
|
|
1192
|
-
if (!entry || typeof entry !== 'object')
|
|
1193
|
-
return null;
|
|
1194
|
-
const fn = entry.function || {};
|
|
1195
|
-
const rawName = normalizeToolCallName(fn.name ?? entry.name);
|
|
1196
|
-
if (!rawName)
|
|
1197
|
-
return null;
|
|
1198
|
-
const argsRaw = fn.arguments ?? entry.arguments ?? {};
|
|
1199
|
-
const argsStr = typeof argsRaw === 'string'
|
|
1200
|
-
? argsRaw
|
|
1201
|
-
: (() => { try {
|
|
1202
|
-
return JSON.stringify(argsRaw ?? {});
|
|
1203
|
-
}
|
|
1204
|
-
catch {
|
|
1205
|
-
return '{}';
|
|
1206
|
-
} })();
|
|
1207
|
-
const callIdRaw = selectCallId(entry);
|
|
1208
|
-
const callId = normalizeFunctionCallId({
|
|
1209
|
-
callId: callIdRaw,
|
|
1210
|
-
fallback: `${fallbackPrefix}_${Math.random().toString(36).slice(2, 10)}`
|
|
1211
|
-
});
|
|
1212
|
-
return {
|
|
1213
|
-
id: callId,
|
|
1214
|
-
type: 'function',
|
|
1215
|
-
function: {
|
|
1216
|
-
name: rawName,
|
|
1217
|
-
arguments: argsStr
|
|
1218
|
-
}
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
function collectToolCallsFromResponses(response) {
|
|
1222
|
-
const collected = [];
|
|
1223
|
-
const seenIds = new Set();
|
|
1224
|
-
const pushCall = (call, source) => {
|
|
1225
|
-
if (!call)
|
|
1226
|
-
return;
|
|
1227
|
-
const key = typeof call.id === 'string' ? call.id : `${source}_${collected.length}`;
|
|
1228
|
-
if (key && seenIds.has(key))
|
|
1229
|
-
return;
|
|
1230
|
-
if (key)
|
|
1231
|
-
seenIds.add(key);
|
|
1232
|
-
collected.push(call);
|
|
1233
|
-
};
|
|
1234
|
-
const required = response?.required_action?.submit_tool_outputs?.tool_calls;
|
|
1235
|
-
if (Array.isArray(required)) {
|
|
1236
|
-
for (const call of required) {
|
|
1237
|
-
pushCall(normalizeToolCall(call, 'req_call'), 'req_call');
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
const outputItems = Array.isArray(response.output) ? response.output : [];
|
|
1241
|
-
for (const item of outputItems) {
|
|
1242
|
-
if (!item || typeof item !== 'object')
|
|
1243
|
-
continue;
|
|
1244
|
-
const type = typeof item.type === 'string' ? String(item.type).toLowerCase() : '';
|
|
1245
|
-
if (type !== 'function_call')
|
|
1246
|
-
continue;
|
|
1247
|
-
pushCall(normalizeToolCall(item, 'output_call'), 'output_call');
|
|
1248
|
-
}
|
|
1249
|
-
return collected;
|
|
1250
|
-
}
|
|
1251
|
-
function resolveFinishReason(response, toolCalls) {
|
|
1252
|
-
const meta = response.metadata && typeof response.metadata === 'object'
|
|
1253
|
-
? response.metadata
|
|
1254
|
-
: undefined;
|
|
1255
|
-
if (meta && typeof meta.finish_reason === 'string') {
|
|
1256
|
-
return meta.finish_reason;
|
|
1257
|
-
}
|
|
1258
|
-
if (toolCalls.length > 0) {
|
|
1259
|
-
return 'tool_calls';
|
|
1260
|
-
}
|
|
1261
|
-
const status = typeof response.status === 'string' ? response.status.toLowerCase() : '';
|
|
1262
|
-
if (status === 'requires_action')
|
|
1263
|
-
return 'tool_calls';
|
|
1264
|
-
if (status === 'in_progress' || status === 'streaming')
|
|
1265
|
-
return 'length';
|
|
1266
|
-
if (status === 'cancelled')
|
|
1267
|
-
return 'cancelled';
|
|
1268
|
-
if (status === 'failed')
|
|
1269
|
-
return 'error';
|
|
1270
|
-
return 'stop';
|
|
1271
|
-
}
|
|
1272
|
-
/**
|
|
1273
|
-
* 将非流式 ResponsesResponse 形状还原为等价的 ChatResponse 形状(用于闭环测试)。
|
|
1274
|
-
*
|
|
1275
|
-
* - id / model / created(_at) / usage → 直接透传
|
|
1276
|
-
* - output_text / output[] → choices[0].message.content
|
|
1277
|
-
* - required_action.submit_tool_outputs.tool_calls → choices[0].message.tool_calls
|
|
1278
|
-
* - finish_reason:metadata.finish_reason 优先,其次 status/tool_calls 推断
|
|
1279
|
-
*
|
|
1280
|
-
* 若 payload 已是 ChatCompletion 形状,则直接返回。
|
|
1281
|
-
*/
|
|
1282
|
-
export function buildChatResponseFromResponses(payload) {
|
|
1283
|
-
if (!payload || typeof payload !== 'object')
|
|
1284
|
-
return payload;
|
|
1285
|
-
const response = unwrapResponsesResponse(payload);
|
|
1286
|
-
if (!response) {
|
|
1287
|
-
if (Array.isArray(payload.choices))
|
|
1288
|
-
return payload;
|
|
1289
|
-
return payload;
|
|
1290
|
-
}
|
|
1291
|
-
const id = typeof response.id === 'string' ? response.id : `resp_${Date.now()}`;
|
|
1292
|
-
const model = response.model;
|
|
1293
|
-
const created = typeof response.created_at === 'number'
|
|
1294
|
-
? response.created_at
|
|
1295
|
-
: (response.created ?? Math.floor(Date.now() / 1000));
|
|
1296
|
-
const usage = response.usage;
|
|
1297
|
-
const toolCalls = collectToolCallsFromResponses(response);
|
|
1298
|
-
const { textParts, reasoningParts } = collectResponseOutput(response);
|
|
1299
|
-
const explicitOutput = typeof response.output_text === 'string' && response.output_text.trim().length
|
|
1300
|
-
? sanitizeThink(response.output_text)
|
|
1301
|
-
: undefined;
|
|
1302
|
-
const messageContentText = explicitOutput || textParts.join('\n').trim();
|
|
1303
|
-
const messageContent = messageContentText || '';
|
|
1304
|
-
const message = {
|
|
1305
|
-
role: 'assistant',
|
|
1306
|
-
content: messageContent
|
|
1307
|
-
};
|
|
1308
|
-
if (toolCalls.length) {
|
|
1309
|
-
message.tool_calls = toolCalls;
|
|
1310
|
-
}
|
|
1311
|
-
if (reasoningParts.length) {
|
|
1312
|
-
message.reasoning_content = reasoningParts.join('\n');
|
|
1313
|
-
}
|
|
1314
|
-
const finishReason = resolveFinishReason(response, toolCalls);
|
|
1315
|
-
const chat = {
|
|
1316
|
-
id,
|
|
1317
|
-
object: 'chat.completion',
|
|
1318
|
-
created,
|
|
1319
|
-
model,
|
|
1320
|
-
choices: [
|
|
1321
|
-
{
|
|
1322
|
-
index: 0,
|
|
1323
|
-
finish_reason: finishReason,
|
|
1324
|
-
message
|
|
1325
|
-
}
|
|
1326
|
-
]
|
|
1327
|
-
};
|
|
1328
|
-
if (usage !== undefined) {
|
|
1329
|
-
chat.usage = usage;
|
|
1330
|
-
}
|
|
1331
|
-
return chat;
|
|
1332
|
-
}
|