@jsonstudio/llms 0.4.6 → 0.6.2

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.
Files changed (99) hide show
  1. package/dist/conversion/codecs/anthropic-openai-codec.js +28 -2
  2. package/dist/conversion/codecs/gemini-openai-codec.js +23 -0
  3. package/dist/conversion/codecs/responses-openai-codec.js +8 -1
  4. package/dist/conversion/hub/node-support.js +14 -1
  5. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +66 -0
  6. package/dist/conversion/hub/pipeline/hub-pipeline.js +284 -193
  7. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.d.ts +11 -0
  8. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +6 -0
  9. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +16 -0
  10. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +17 -0
  11. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.d.ts +5 -0
  12. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.js +17 -0
  13. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.d.ts +19 -0
  14. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +269 -0
  15. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.d.ts +18 -0
  16. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +141 -0
  17. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +11 -0
  18. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +29 -0
  19. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.d.ts +16 -0
  20. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +15 -0
  21. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.d.ts +17 -0
  22. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +18 -0
  23. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.d.ts +17 -0
  24. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +63 -0
  25. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.d.ts +11 -0
  26. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +6 -0
  27. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +12 -0
  28. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +6 -0
  29. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +13 -0
  30. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +43 -0
  31. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.d.ts +17 -0
  32. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +22 -0
  33. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.d.ts +16 -0
  34. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +19 -0
  35. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.d.ts +17 -0
  36. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +19 -0
  37. package/dist/conversion/hub/pipeline/stages/utils.d.ts +2 -0
  38. package/dist/conversion/hub/pipeline/stages/utils.js +11 -0
  39. package/dist/conversion/hub/pipeline/target-utils.d.ts +5 -0
  40. package/dist/conversion/hub/pipeline/target-utils.js +87 -0
  41. package/dist/conversion/hub/process/chat-process.js +23 -17
  42. package/dist/conversion/hub/response/provider-response.js +69 -122
  43. package/dist/conversion/hub/response/response-mappers.d.ts +19 -0
  44. package/dist/conversion/hub/response/response-mappers.js +22 -2
  45. package/dist/conversion/hub/response/response-runtime.d.ts +8 -0
  46. package/dist/conversion/hub/response/response-runtime.js +239 -6
  47. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +8 -0
  48. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +135 -55
  49. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +80 -40
  50. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +5 -29
  51. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +16 -13
  52. package/dist/conversion/hub/snapshot-recorder.d.ts +13 -0
  53. package/dist/conversion/hub/snapshot-recorder.js +90 -50
  54. package/dist/conversion/hub/standardized-bridge.js +49 -38
  55. package/dist/conversion/hub/types/chat-envelope.d.ts +68 -0
  56. package/dist/conversion/hub/types/standardized.d.ts +97 -0
  57. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +29 -2
  58. package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.js +68 -1
  59. package/dist/conversion/responses/responses-openai-bridge.d.ts +6 -1
  60. package/dist/conversion/responses/responses-openai-bridge.js +132 -10
  61. package/dist/conversion/shared/anthropic-message-utils.d.ts +9 -1
  62. package/dist/conversion/shared/anthropic-message-utils.js +414 -26
  63. package/dist/conversion/shared/bridge-actions.js +267 -95
  64. package/dist/conversion/shared/bridge-message-utils.js +54 -8
  65. package/dist/conversion/shared/bridge-policies.js +21 -2
  66. package/dist/conversion/shared/chat-envelope-validator.d.ts +8 -0
  67. package/dist/conversion/shared/chat-envelope-validator.js +128 -0
  68. package/dist/conversion/shared/chat-request-filters.js +109 -28
  69. package/dist/conversion/shared/mcp-injection.js +41 -20
  70. package/dist/conversion/shared/openai-finalizer.d.ts +11 -0
  71. package/dist/conversion/shared/openai-finalizer.js +73 -0
  72. package/dist/conversion/shared/openai-message-normalize.js +32 -31
  73. package/dist/conversion/shared/protocol-state.d.ts +4 -0
  74. package/dist/conversion/shared/protocol-state.js +23 -0
  75. package/dist/conversion/shared/reasoning-normalizer.d.ts +1 -0
  76. package/dist/conversion/shared/reasoning-normalizer.js +50 -18
  77. package/dist/conversion/shared/responses-output-builder.d.ts +1 -1
  78. package/dist/conversion/shared/responses-output-builder.js +76 -25
  79. package/dist/conversion/shared/responses-reasoning-registry.d.ts +8 -0
  80. package/dist/conversion/shared/responses-reasoning-registry.js +61 -0
  81. package/dist/conversion/shared/responses-response-utils.js +32 -2
  82. package/dist/conversion/shared/responses-tool-utils.js +28 -2
  83. package/dist/conversion/shared/snapshot-hooks.d.ts +9 -0
  84. package/dist/conversion/shared/snapshot-hooks.js +60 -6
  85. package/dist/conversion/shared/snapshot-utils.d.ts +16 -0
  86. package/dist/conversion/shared/snapshot-utils.js +84 -0
  87. package/dist/conversion/shared/tool-filter-pipeline.js +46 -7
  88. package/dist/conversion/shared/tool-mapping.js +13 -2
  89. package/dist/filters/index.d.ts +18 -0
  90. package/dist/filters/index.js +0 -1
  91. package/dist/filters/special/request-streaming-to-nonstreaming.d.ts +13 -0
  92. package/dist/filters/special/request-streaming-to-nonstreaming.js +13 -1
  93. package/dist/filters/special/request-tool-choice-policy.js +3 -1
  94. package/dist/filters/special/request-tool-list-filter.d.ts +11 -0
  95. package/dist/filters/special/request-tool-list-filter.js +20 -7
  96. package/dist/sse/shared/responses-output-normalizer.js +5 -4
  97. package/dist/sse/sse-to-json/builders/response-builder.js +24 -1
  98. package/dist/sse/types/responses-types.d.ts +2 -0
  99. package/package.json +1 -1
@@ -0,0 +1,128 @@
1
+ import { isJsonObject } from '../hub/types/json.js';
2
+ const ALLOWED_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
3
+ const RESERVED_EXACT_KEYS = new Set(['stages', 'stageExpectations']);
4
+ const RESERVED_PREFIXES = ['__rcc_'];
5
+ export function validateChatEnvelope(chat, options) {
6
+ if (!chat || typeof chat !== 'object') {
7
+ throw buildError('chatEnvelope_missing', 'Chat envelope must be an object', options);
8
+ }
9
+ assertNoReservedKeys(chat, 'chatEnvelope', options);
10
+ if (!Array.isArray(chat.messages) || chat.messages.length === 0) {
11
+ throw buildError('messages_missing', 'Chat envelope must include at least one message', options);
12
+ }
13
+ chat.messages.forEach((message, index) => {
14
+ if (!message || typeof message !== 'object') {
15
+ throw buildError('message_shape', `messages[${index}] must be an object`, options);
16
+ }
17
+ assertNoReservedKeys(message, `messages[${index}]`, options);
18
+ if (typeof message.role !== 'string' || !ALLOWED_ROLES.has(message.role)) {
19
+ throw buildError('message_role', `messages[${index}].role must be one of ${Array.from(ALLOWED_ROLES).join(', ')}`, options);
20
+ }
21
+ validateToolCalls(message, index, options);
22
+ });
23
+ if (!isJsonObject(chat.parameters) || typeof chat.parameters.model !== 'string' || !chat.parameters.model.trim()) {
24
+ throw buildError('parameters_model', 'Chat envelope parameters.model must be a string', options);
25
+ }
26
+ assertNoReservedKeys(chat.parameters, 'parameters', options);
27
+ validateTools(chat.tools, options);
28
+ validateToolOutputs(chat.toolOutputs, options);
29
+ validateMetadata(chat.metadata, options);
30
+ }
31
+ function validateToolCalls(message, messageIndex, options) {
32
+ if (!Array.isArray(message.tool_calls)) {
33
+ return;
34
+ }
35
+ message.tool_calls.forEach((entry, callIndex) => {
36
+ if (!entry || typeof entry !== 'object') {
37
+ throw buildError('tool_call_shape', `messages[${messageIndex}].tool_calls[${callIndex}] must be an object`, options);
38
+ }
39
+ if (entry.type !== 'function') {
40
+ throw buildError('tool_call_type', `messages[${messageIndex}].tool_calls[${callIndex}].type must equal "function"`, options);
41
+ }
42
+ const fnNode = entry.function;
43
+ if (!fnNode || typeof fnNode !== 'object') {
44
+ throw buildError('tool_call_function', `messages[${messageIndex}].tool_calls[${callIndex}].function must be an object`, options);
45
+ }
46
+ const fnRecord = fnNode;
47
+ if (typeof fnRecord.name !== 'string' || !fnRecord.name.trim()) {
48
+ throw buildError('tool_call_name', `messages[${messageIndex}].tool_calls[${callIndex}].function.name must be a string`, options);
49
+ }
50
+ if (typeof fnRecord.arguments !== 'string') {
51
+ throw buildError('tool_call_arguments', `messages[${messageIndex}].tool_calls[${callIndex}].function.arguments must be a JSON string`, options);
52
+ }
53
+ });
54
+ }
55
+ function validateTools(tools, options) {
56
+ if (tools === undefined) {
57
+ return;
58
+ }
59
+ if (!Array.isArray(tools)) {
60
+ throw buildError('tools_shape', 'Chat envelope tools must be an array when provided', options);
61
+ }
62
+ tools.forEach((tool, index) => {
63
+ if (!tool || typeof tool !== 'object') {
64
+ throw buildError('tools_shape', `tools[${index}] must be an object`, options);
65
+ }
66
+ if (tool.type !== 'function') {
67
+ throw buildError('tools_type', `tools[${index}].type must equal "function"`, options);
68
+ }
69
+ const fnNode = tool.function;
70
+ if (!fnNode || typeof fnNode !== 'object' || typeof fnNode.name !== 'string' || !fnNode.name.trim()) {
71
+ throw buildError('tools_function', `tools[${index}].function.name must be a string`, options);
72
+ }
73
+ });
74
+ }
75
+ function validateToolOutputs(outputs, options) {
76
+ if (outputs === undefined) {
77
+ return;
78
+ }
79
+ if (!Array.isArray(outputs)) {
80
+ throw buildError('tool_outputs_shape', 'toolOutputs must be an array when provided', options);
81
+ }
82
+ outputs.forEach((entry, index) => {
83
+ if (!entry || typeof entry !== 'object') {
84
+ throw buildError('tool_outputs_shape', `toolOutputs[${index}] must be an object`, options);
85
+ }
86
+ if (typeof entry.tool_call_id !== 'string' || !entry.tool_call_id.trim()) {
87
+ throw buildError('tool_outputs_id', `toolOutputs[${index}].tool_call_id must be a string`, options);
88
+ }
89
+ if (typeof entry.content !== 'string') {
90
+ throw buildError('tool_outputs_content', `toolOutputs[${index}].content must be a string`, options);
91
+ }
92
+ });
93
+ }
94
+ function validateMetadata(metadata, options) {
95
+ if (!isJsonObject(metadata)) {
96
+ throw buildError('metadata_shape', 'Chat envelope metadata must be an object', options);
97
+ }
98
+ assertNoReservedKeys(metadata, 'metadata', options);
99
+ if (!isJsonObject(metadata.context)) {
100
+ throw buildError('metadata_context', 'metadata.context must be an object', options);
101
+ }
102
+ if (metadata.extraFields !== undefined) {
103
+ if (!isJsonObject(metadata.extraFields)) {
104
+ throw buildError('metadata_extra_fields', 'metadata.extraFields must be an object when provided', options);
105
+ }
106
+ assertNoReservedKeys(metadata.extraFields, 'metadata.extraFields', options, true);
107
+ }
108
+ if (metadata.providerMetadata !== undefined && !isJsonObject(metadata.providerMetadata)) {
109
+ throw buildError('metadata_provider', 'metadata.providerMetadata must be an object when provided', options);
110
+ }
111
+ }
112
+ function assertNoReservedKeys(target, path, options, allowStageKeys = false) {
113
+ if (!target || typeof target !== 'object') {
114
+ return;
115
+ }
116
+ for (const key of Object.keys(target)) {
117
+ if (!allowStageKeys && RESERVED_EXACT_KEYS.has(key)) {
118
+ throw buildError('reserved_key', `${path} contains reserved field "${key}"`, options);
119
+ }
120
+ if (RESERVED_PREFIXES.some((prefix) => key.startsWith(prefix))) {
121
+ throw buildError('reserved_key', `${path} contains reserved field "${key}"`, options);
122
+ }
123
+ }
124
+ }
125
+ function buildError(code, detail, options) {
126
+ const source = options.source ? ` source=${options.source}` : '';
127
+ return new Error(`ChatEnvelopeValidationError(${options.stage}/${options.direction})[${code}] ${detail}${source}`);
128
+ }
@@ -1,6 +1,7 @@
1
- import { FilterEngine, RequestToolCallsStringifyFilter, RequestToolChoicePolicyFilter, RequestStreamingToNonStreamingFilter } from '../../filters/index.js';
1
+ import { FilterEngine, RequestToolCallsStringifyFilter, RequestToolChoicePolicyFilter } from '../../filters/index.js';
2
2
  import { normalizeChatRequest } from '../index.js';
3
3
  import { loadFieldMapConfig } from '../../filters/utils/fieldmap-loader.js';
4
+ import { createSnapshotWriter } from './snapshot-utils.js';
4
5
  /**
5
6
  * 统一的 Chat 请求侧过滤链路。
6
7
  *
@@ -9,6 +10,16 @@ import { loadFieldMapConfig } from '../../filters/utils/fieldmap-loader.js';
9
10
  * 都在这里走同一套工具治理与参数标准化逻辑。
10
11
  */
11
12
  export async function runStandardChatRequestFilters(chatRequest, profile, context) {
13
+ const existingMetadata = context.metadata ?? {};
14
+ if (!context.metadata) {
15
+ context.metadata = existingMetadata;
16
+ }
17
+ const inboundStreamFromContext = typeof existingMetadata.inboundStream === 'boolean' ? existingMetadata.inboundStream : undefined;
18
+ const inboundStreamDetected = chatRequest && typeof chatRequest === 'object' && chatRequest.stream === true;
19
+ const normalizedInboundStream = inboundStreamFromContext ?? inboundStreamDetected ?? undefined;
20
+ if (typeof normalizedInboundStream === 'boolean') {
21
+ existingMetadata.inboundStream = normalizedInboundStream;
22
+ }
12
23
  const requestId = context.requestId ?? `req_${Date.now()}`;
13
24
  const modelId = (chatRequest && typeof chatRequest === 'object' && typeof chatRequest.model === 'string')
14
25
  ? String(chatRequest.model)
@@ -16,6 +27,17 @@ export async function runStandardChatRequestFilters(chatRequest, profile, contex
16
27
  const endpoint = context.entryEndpoint ||
17
28
  context.endpoint ||
18
29
  '/v1/chat/completions';
30
+ const snapshot = createSnapshotWriter({
31
+ requestId,
32
+ endpoint,
33
+ folderHint: 'openai-chat'
34
+ });
35
+ const snapshotStage = (stage, payload) => {
36
+ if (!snapshot)
37
+ return;
38
+ snapshot(stage, payload);
39
+ };
40
+ snapshotStage('req_process_filters_input', chatRequest);
19
41
  const reqCtxBase = {
20
42
  requestId,
21
43
  model: modelId,
@@ -24,18 +46,26 @@ export async function runStandardChatRequestFilters(chatRequest, profile, contex
24
46
  debug: { emit: () => { } }
25
47
  };
26
48
  const engine = new FilterEngine();
49
+ const incomingProtocol = (profile.incomingProtocol || '').toLowerCase();
50
+ const entryEndpointLower = (endpoint || '').toLowerCase();
51
+ const originalToolCount = (chatRequest && typeof chatRequest === 'object' && Array.isArray(chatRequest.tools))
52
+ ? chatRequest.tools.length
53
+ : 0;
54
+ const isAnthropicProfile = incomingProtocol === 'anthropic-messages' ||
55
+ entryEndpointLower.includes('/v1/messages');
56
+ const skipAutoToolInjection = isAnthropicProfile && originalToolCount === 0;
27
57
  // Request-side initial filters(与 openai-openai-codec 保持一致)
28
- try {
29
- const { RequestToolListFilter } = await import('../../filters/index.js');
30
- engine.registerFilter(new RequestToolListFilter());
31
- }
32
- catch {
33
- // 可选过滤器,失败时保持向后兼容
58
+ if (!skipAutoToolInjection) {
59
+ try {
60
+ const { RequestToolListFilter } = await import('../../filters/index.js');
61
+ engine.registerFilter(new RequestToolListFilter());
62
+ }
63
+ catch {
64
+ // 可选过滤器,失败时保持向后兼容
65
+ }
34
66
  }
35
67
  engine.registerFilter(new RequestToolCallsStringifyFilter());
36
68
  engine.registerFilter(new RequestToolChoicePolicyFilter());
37
- // 统一将 Provider 请求转为非流式 Chat;流式语义由上层 SSE 合成处理
38
- engine.registerFilter(new RequestStreamingToNonStreamingFilter());
39
69
  // FieldMap:保持与 Chat 入口一致,使用 openai-openai.fieldmap.json
40
70
  try {
41
71
  const cfg = await loadFieldMapConfig('openai-openai.fieldmap.json');
@@ -56,42 +86,93 @@ export async function runStandardChatRequestFilters(chatRequest, profile, contex
56
86
  // best-effort:缺少 fieldmap 时保持原样
57
87
  }
58
88
  let staged = await engine.run('request_pre', chatRequest, reqCtxBase);
89
+ snapshotStage('req_process_filters_request_pre', staged);
59
90
  staged = await engine.run('request_map', staged, reqCtxBase);
91
+ snapshotStage('req_process_filters_request_map', staged);
60
92
  staged = await engine.run('request_post', staged, reqCtxBase);
93
+ snapshotStage('req_process_filters_request_post', staged);
94
+ if (skipAutoToolInjection && staged && typeof staged === 'object') {
95
+ if (!Array.isArray(staged.tools)) {
96
+ staged.tools = [];
97
+ }
98
+ staged.__rcc_disable_mcp_tools = true;
99
+ }
61
100
  // 归一化 Chat 请求后再做最终工具治理
62
101
  let normalized = normalizeChatRequest(staged);
102
+ snapshotStage('req_process_filters_normalized', normalized);
63
103
  try {
64
104
  const { RequestOpenAIToolsNormalizeFilter, ToolPostConstraintsFilter } = await import('../../filters/index.js');
65
105
  engine.registerFilter(new RequestOpenAIToolsNormalizeFilter());
66
106
  // 工具治理后的最后约束层:默认配置为空,由宿主/配置系统按 profile/provider/modelId 决定是否启用具体规则。
67
107
  engine.registerFilter(new ToolPostConstraintsFilter('request_finalize'));
68
108
  normalized = await engine.run('request_finalize', normalized, reqCtxBase);
109
+ snapshotStage('req_process_filters_request_finalize', normalized);
69
110
  }
70
111
  catch {
71
112
  // 可选工具归一过滤器,失败时保持已归一化结果
72
113
  }
73
- return ensureToolParameterDefaults(normalized);
114
+ const preserveStreamField = profile.incomingProtocol === 'openai-chat' && profile.outgoingProtocol === 'openai-chat';
115
+ const pruned = pruneChatRequestPayload(normalized, { preserveStreamField });
116
+ snapshotStage('req_process_filters_output', pruned);
117
+ return pruned;
74
118
  }
75
- function ensureToolParameterDefaults(chatRequest) {
119
+ function pruneChatRequestPayload(chatRequest, options) {
76
120
  if (!chatRequest || typeof chatRequest !== 'object')
77
121
  return chatRequest;
78
- const next = { ...chatRequest };
79
- if (!Array.isArray(next.tools))
80
- return next;
81
- next.tools = next.tools.map((tool) => {
82
- if (!tool || typeof tool !== 'object')
83
- return tool;
84
- if (tool.type !== 'function')
85
- return tool;
86
- const fn = tool.function && typeof tool.function === 'object' ? { ...tool.function } : tool.function;
87
- if (fn && typeof fn === 'object' && fn.parameters && typeof fn.parameters === 'object') {
88
- const params = { ...fn.parameters };
89
- if (!Object.prototype.hasOwnProperty.call(params, 'additionalProperties')) {
90
- params.additionalProperties = true;
91
- }
92
- fn.parameters = params;
122
+ const stripped = { ...chatRequest };
123
+ stripSentinelKeys(stripped);
124
+ if ('originalStream' in stripped) {
125
+ delete stripped.originalStream;
126
+ }
127
+ if ('_originalStreamOptions' in stripped) {
128
+ delete stripped._originalStreamOptions;
129
+ }
130
+ if ('metadata' in stripped) {
131
+ delete stripped.metadata;
132
+ }
133
+ if (!options?.preserveStreamField && 'stream' in stripped && stripped.stream !== true) {
134
+ delete stripped.stream;
135
+ }
136
+ if (Array.isArray(stripped.messages)) {
137
+ stripped.messages = stripped.messages.map((message) => sanitizeMessageEntry(message));
138
+ }
139
+ return stripped;
140
+ }
141
+ function sanitizeMessageEntry(message) {
142
+ if (!message || typeof message !== 'object')
143
+ return message;
144
+ const clone = { ...message };
145
+ if (Array.isArray(clone.tool_calls) && clone.tool_calls.length) {
146
+ clone.tool_calls = clone.tool_calls.map((call) => sanitizeToolCallEntry(call));
147
+ }
148
+ if (clone.role === 'tool') {
149
+ if (typeof clone.tool_call_id !== 'string' && typeof clone.call_id === 'string') {
150
+ clone.tool_call_id = clone.call_id;
151
+ }
152
+ if ('id' in clone) {
153
+ delete clone.id;
154
+ }
155
+ }
156
+ if ('call_id' in clone) {
157
+ delete clone.call_id;
158
+ }
159
+ return clone;
160
+ }
161
+ function sanitizeToolCallEntry(call) {
162
+ if (!call || typeof call !== 'object')
163
+ return call;
164
+ const clone = { ...call };
165
+ delete clone.call_id;
166
+ delete clone.tool_call_id;
167
+ if (clone.function && typeof clone.function === 'object') {
168
+ clone.function = { ...clone.function };
169
+ }
170
+ return clone;
171
+ }
172
+ function stripSentinelKeys(record) {
173
+ Object.keys(record).forEach((key) => {
174
+ if (key.startsWith('__rcc_')) {
175
+ delete record[key];
93
176
  }
94
- return { ...tool, function: fn };
95
177
  });
96
- return next;
97
178
  }
@@ -1,10 +1,15 @@
1
1
  // Shared MCP tool exposure logic (two-step), aligned with claude-code-router
2
- const obj = (props, req) => ({
3
- type: 'object',
4
- properties: props,
5
- required: req,
6
- additionalProperties: false,
7
- });
2
+ const obj = (props, req = []) => {
3
+ const schema = {
4
+ type: 'object',
5
+ properties: props,
6
+ additionalProperties: false
7
+ };
8
+ if (Array.isArray(req) && req.length > 0) {
9
+ schema.required = req;
10
+ }
11
+ return schema;
12
+ };
8
13
  function uniq(arr) { return Array.from(new Set(arr)); }
9
14
  export function injectMcpToolsForChat(tools, discoveredServers) {
10
15
  const out = [];
@@ -23,7 +28,7 @@ export function injectMcpToolsForChat(tools, discoveredServers) {
23
28
  continue;
24
29
  }
25
30
  if (lower === 'list_mcp_resources') {
26
- ensure('list_mcp_resources', { type: 'function', function: { name: 'list_mcp_resources', description: t.function?.description || 'List resources from a given MCP server.', parameters: obj({ server: listServers, filter: { type: 'string' }, root: { type: 'string' } }, []) } });
31
+ ensure('list_mcp_resources', { type: 'function', function: { name: 'list_mcp_resources', description: t.function?.description || 'List resources from a given MCP server (arguments.server = server label).', parameters: obj({ server: listServers, filter: { type: 'string' }, root: { type: 'string' } }) } });
27
32
  continue;
28
33
  }
29
34
  if (lower === 'read_mcp_resource' || lower === 'list_mcp_resource_templates') {
@@ -43,7 +48,7 @@ export function injectMcpToolsForChat(tools, discoveredServers) {
43
48
  }
44
49
  // Ensure list exists when user did not provide it
45
50
  if (!keep.has('list_mcp_resources')) {
46
- out.push({ type: 'function', function: { name: 'list_mcp_resources', description: 'List resources from a given MCP server (arguments.server = server label).', parameters: obj({ server: listServers, filter: { type: 'string' }, root: { type: 'string' } }, []) } });
51
+ out.push({ type: 'function', function: { name: 'list_mcp_resources', description: 'List resources from a given MCP server (arguments.server = server label).', parameters: obj({ server: listServers, filter: { type: 'string' }, root: { type: 'string' } }) } });
47
52
  }
48
53
  return out;
49
54
  }
@@ -51,6 +56,7 @@ export function injectMcpToolsForResponses(tools, discoveredServers) {
51
56
  const out = [];
52
57
  const keep = new Set();
53
58
  const listServers = { type: 'string' };
59
+ const templateServer = discoveredServers && discoveredServers.length ? { type: 'string', enum: uniq(discoveredServers) } : { type: 'string' };
54
60
  const ensure = (name, def) => { if (!keep.has(name)) {
55
61
  out.push(def);
56
62
  keep.add(name);
@@ -64,26 +70,41 @@ export function injectMcpToolsForResponses(tools, discoveredServers) {
64
70
  continue;
65
71
  }
66
72
  if (lower === 'list_mcp_resources') {
67
- ensure('list_mcp_resources', { type: 'function', function: { name: 'list_mcp_resources', description: t.function?.description || 'List resources from a given MCP server.', parameters: obj({ server: listServers, filter: { type: 'string' }, root: { type: 'string' } }, []) } });
73
+ ensure('list_mcp_resources', { type: 'function', function: { name: 'list_mcp_resources', description: t.function?.description || 'List resources from a given MCP server (arguments.server = server label).', parameters: obj({ server: listServers, filter: { type: 'string' }, root: { type: 'string' } }) } });
68
74
  continue;
69
75
  }
70
- if (lower === 'read_mcp_resource' || lower === 'list_mcp_resource_templates') {
71
- if (discoveredServers.length > 0) {
72
- const srv = { type: 'string', enum: uniq(discoveredServers) };
73
- if (lower === 'read_mcp_resource') {
74
- ensure('read_mcp_resource', { type: 'function', function: { name: 'read_mcp_resource', description: t.function?.description || 'Read a resource via MCP server.', parameters: obj({ server: srv, uri: { type: 'string' } }, ['server', 'uri']) } });
75
- }
76
- else {
77
- ensure('list_mcp_resource_templates', { type: 'function', function: { name: 'list_mcp_resource_templates', description: 'List resource templates via MCP server.', parameters: obj({ server: srv }, ['server']) } });
76
+ if (lower === 'list_mcp_resource_templates') {
77
+ ensure('list_mcp_resource_templates', {
78
+ type: 'function',
79
+ function: {
80
+ name: 'list_mcp_resource_templates',
81
+ description: t.function?.description || 'List resource templates from MCP servers.',
82
+ parameters: obj({ server: templateServer, cursor: { type: 'string' } })
78
83
  }
79
- continue;
80
- }
84
+ });
85
+ continue;
86
+ }
87
+ if (lower === 'read_mcp_resource' && discoveredServers.length > 0) {
88
+ const srv = { type: 'string', enum: uniq(discoveredServers) };
89
+ ensure('read_mcp_resource', { type: 'function', function: { name: 'read_mcp_resource', description: t.function?.description || 'Read a resource via MCP server.', parameters: obj({ server: srv, uri: { type: 'string' } }, ['server', 'uri']) } });
90
+ continue;
81
91
  }
82
92
  out.push(t);
83
93
  keep.add(lower);
84
94
  }
85
95
  if (!keep.has('list_mcp_resources')) {
86
- out.push({ type: 'function', function: { name: 'list_mcp_resources', description: 'List resources from a given MCP server (arguments.server = server label).', parameters: obj({ server: listServers, filter: { type: 'string' }, root: { type: 'string' } }, []) } });
96
+ out.push({ type: 'function', function: { name: 'list_mcp_resources', description: 'List resources from a given MCP server (arguments.server = server label).', parameters: obj({ server: listServers, filter: { type: 'string' }, root: { type: 'string' } }) } });
97
+ keep.add('list_mcp_resources');
98
+ }
99
+ if (!keep.has('list_mcp_resource_templates')) {
100
+ out.push({
101
+ type: 'function',
102
+ function: {
103
+ name: 'list_mcp_resource_templates',
104
+ description: 'List resource templates from MCP servers.',
105
+ parameters: obj({ server: templateServer, cursor: { type: 'string' } })
106
+ }
107
+ });
87
108
  }
88
109
  return out;
89
110
  }
@@ -0,0 +1,11 @@
1
+ type ProcessingMode = 'streaming' | 'non-streaming' | 'auto';
2
+ export type ChatReasoningMode = 'keep' | 'drop' | 'append_to_content';
3
+ export interface FinalizeOptions {
4
+ requestId?: string;
5
+ endpoint?: string;
6
+ processingMode?: ProcessingMode;
7
+ stream?: boolean;
8
+ reasoningMode?: ChatReasoningMode;
9
+ }
10
+ export declare function finalizeOpenAIChatResponse(chatLike: unknown, opts?: FinalizeOptions): Promise<unknown>;
11
+ export {};
@@ -100,5 +100,78 @@ export async function finalizeOpenAIChatResponse(chatLike, opts) {
100
100
  }
101
101
  }
102
102
  catch { /* ignore normalization errors */ }
103
+ applyChatReasoningPolicy(obj, opts?.reasoningMode);
103
104
  return obj;
104
105
  }
106
+ function applyChatReasoningPolicy(target, mode) {
107
+ if (!mode || mode === 'keep') {
108
+ return;
109
+ }
110
+ const containers = [];
111
+ if (Array.isArray(target.messages)) {
112
+ target.messages.forEach(entry => {
113
+ if (entry && typeof entry === 'object') {
114
+ containers.push(entry);
115
+ }
116
+ });
117
+ }
118
+ if (Array.isArray(target.choices)) {
119
+ target.choices.forEach(choice => {
120
+ if (!choice || typeof choice !== 'object') {
121
+ return;
122
+ }
123
+ const record = choice;
124
+ if (record.message && typeof record.message === 'object') {
125
+ containers.push(record.message);
126
+ }
127
+ if (record.delta && typeof record.delta === 'object') {
128
+ containers.push(record.delta);
129
+ }
130
+ });
131
+ }
132
+ containers.forEach(container => normalizeReasoningField(container, mode));
133
+ }
134
+ function normalizeReasoningField(container, mode) {
135
+ const bag = container;
136
+ const rawValue = typeof bag.reasoning_content === 'string' ? bag.reasoning_content : '';
137
+ const trimmed = rawValue.trim();
138
+ if (!trimmed && Object.prototype.hasOwnProperty.call(bag, 'reasoning_content')) {
139
+ delete bag.reasoning_content;
140
+ return;
141
+ }
142
+ if (!trimmed) {
143
+ return;
144
+ }
145
+ if (mode === 'drop') {
146
+ delete bag.reasoning_content;
147
+ return;
148
+ }
149
+ if (mode === 'append_to_content') {
150
+ appendReasoningToContent(bag, trimmed);
151
+ delete bag.reasoning_content;
152
+ }
153
+ }
154
+ function appendReasoningToContent(container, reasoning) {
155
+ const separator = reasoning.startsWith('\n') ? '' : '\n';
156
+ if (typeof container.content === 'string') {
157
+ const base = container.content.trim().length ? container.content : '';
158
+ container.content = base ? `${base}${separator}${reasoning}` : reasoning;
159
+ return;
160
+ }
161
+ if (Array.isArray(container.content)) {
162
+ const list = container.content;
163
+ list.push({ type: 'text', text: reasoning });
164
+ return;
165
+ }
166
+ if (container.content && typeof container.content === 'object') {
167
+ try {
168
+ const serialized = JSON.stringify(container.content);
169
+ container.content = serialized ? `${serialized}${separator}${reasoning}` : reasoning;
170
+ }
171
+ catch {
172
+ container.content = reasoning;
173
+ }
174
+ return;
175
+ }
176
+ container.content = reasoning;
177
+ }
@@ -1,3 +1,4 @@
1
+ import { injectMcpToolsForChat } from './mcp-injection.js';
1
2
  // Message normalization utilities for OpenAI chat payloads (renamed to avoid confusion
2
3
  // with the deprecated "openai-normalizer" module entry). This file contains the
3
4
  // previously-implemented logic from openai-normalize.ts.
@@ -47,48 +48,48 @@ export function normalizeChatRequest(request) {
47
48
  }
48
49
  catch { /* ignore limit errors */ }
49
50
  // MCP 注入(两步法)统一走共享实现,避免路径分叉
50
- try {
51
- const enableMcp = String(process?.env?.ROUTECODEX_MCP_ENABLE ?? '1') !== '0';
52
- if (enableMcp) {
53
- const known = new Set();
54
- try {
55
- const msgs = Array.isArray(normalized.messages) ? normalized.messages : [];
56
- for (const m of msgs) {
57
- if (!m || typeof m !== 'object')
58
- continue;
59
- if (m.role === 'tool' && typeof m.content === 'string') {
60
- try {
61
- const obj = JSON.parse(m.content);
62
- const sv = obj?.arguments?.server;
63
- if (typeof sv === 'string' && sv.trim())
64
- known.add(sv.trim());
65
- }
66
- catch { /* ignore */ }
67
- }
68
- if (m.role === 'assistant' && Array.isArray(m.tool_calls)) {
69
- for (const tc of m.tool_calls) {
51
+ const disableMcpTools = Boolean(normalized.__rcc_disable_mcp_tools);
52
+ if (!disableMcpTools) {
53
+ try {
54
+ const enableMcp = String(process?.env?.ROUTECODEX_MCP_ENABLE ?? '1') !== '0';
55
+ if (enableMcp) {
56
+ const known = new Set();
57
+ try {
58
+ const msgs = Array.isArray(normalized.messages) ? normalized.messages : [];
59
+ for (const m of msgs) {
60
+ if (!m || typeof m !== 'object')
61
+ continue;
62
+ if (m.role === 'tool' && typeof m.content === 'string') {
70
63
  try {
71
- const argStr = String(tc?.function?.arguments ?? '');
72
- const parsed = JSON.parse(argStr);
73
- const sv = parsed?.server;
64
+ const obj = JSON.parse(m.content);
65
+ const sv = obj?.arguments?.server;
74
66
  if (typeof sv === 'string' && sv.trim())
75
67
  known.add(sv.trim());
76
68
  }
77
69
  catch { /* ignore */ }
78
70
  }
71
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls)) {
72
+ for (const tc of m.tool_calls) {
73
+ try {
74
+ const argStr = String(tc?.function?.arguments ?? '');
75
+ const parsed = JSON.parse(argStr);
76
+ const sv = parsed?.server;
77
+ if (typeof sv === 'string' && sv.trim())
78
+ known.add(sv.trim());
79
+ }
80
+ catch { /* ignore */ }
81
+ }
82
+ }
79
83
  }
80
84
  }
85
+ catch { /* ignore */ }
86
+ const discovered = Array.from(known);
87
+ const currentTools = Array.isArray(normalized.tools) ? normalized.tools : [];
88
+ normalized.tools = injectMcpToolsForChat(currentTools, discovered);
81
89
  }
82
- catch { /* ignore */ }
83
- const discovered = Array.from(known);
84
- // Dynamic import replaced with require-style to keep function non-async
85
- // eslint-disable-next-line @typescript-eslint/no-var-requires
86
- const inj = require('./mcp-injection.js');
87
- const currentTools = Array.isArray(normalized.tools) ? normalized.tools : [];
88
- normalized.tools = inj.injectMcpToolsForChat(currentTools, discovered);
89
90
  }
91
+ catch { /* ignore MCP injection */ }
90
92
  }
91
- catch { /* ignore MCP injection */ }
92
93
  // Normalize assistant.tool_calls arguments (stringify only; do NOT reshape)
93
94
  try {
94
95
  const msgs = Array.isArray(normalized.messages) ? normalized.messages : [];
@@ -0,0 +1,4 @@
1
+ import type { ChatEnvelope } from '../hub/types/chat-envelope.js';
2
+ import { type JsonObject } from '../hub/types/json.js';
3
+ export declare function ensureProtocolState(metadata: ChatEnvelope['metadata'], protocol: string): JsonObject;
4
+ export declare function getProtocolState(metadata: ChatEnvelope['metadata'] | undefined, protocol: string): JsonObject | undefined;
@@ -0,0 +1,23 @@
1
+ import { isJsonObject } from '../hub/types/json.js';
2
+ const PROTOCOL_STATE_KEY = 'protocolState';
3
+ export function ensureProtocolState(metadata, protocol) {
4
+ if (!metadata[PROTOCOL_STATE_KEY] || !isJsonObject(metadata[PROTOCOL_STATE_KEY])) {
5
+ metadata[PROTOCOL_STATE_KEY] = {};
6
+ }
7
+ const container = metadata[PROTOCOL_STATE_KEY];
8
+ if (!isJsonObject(container[protocol])) {
9
+ container[protocol] = {};
10
+ }
11
+ return container[protocol];
12
+ }
13
+ export function getProtocolState(metadata, protocol) {
14
+ if (!metadata) {
15
+ return undefined;
16
+ }
17
+ const store = metadata[PROTOCOL_STATE_KEY];
18
+ if (!isJsonObject(store)) {
19
+ return undefined;
20
+ }
21
+ const node = store[protocol];
22
+ return isJsonObject(node) ? node : undefined;
23
+ }
@@ -8,6 +8,7 @@ interface ResponsesReasoningNormalizeOptions {
8
8
  }
9
9
  export declare function normalizeReasoningInChatPayload(payload: {
10
10
  messages?: JsonValue[];
11
+ choices?: JsonValue[];
11
12
  } | null | undefined): void;
12
13
  export declare function normalizeReasoningInResponsesPayload(payload: {
13
14
  output?: JsonValue[];