@jsonstudio/llms 0.6.230 → 0.6.375
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/conversion/codecs/gemini-openai-codec.js +9 -1
- package/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
- package/dist/conversion/compat/actions/gemini-web-search.js +68 -0
- package/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
- package/dist/conversion/compat/actions/glm-image-content.js +83 -0
- package/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
- package/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
- package/dist/conversion/compat/actions/glm-web-search.js +25 -28
- package/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +17 -0
- package/dist/conversion/compat/profiles/chat-glm.json +190 -184
- package/dist/conversion/compat/profiles/chat-iflow.json +195 -195
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/config/sample-config.json +1 -1
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +18 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +6 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +28 -1
- package/dist/conversion/hub/pipeline/target-utils.js +6 -0
- package/dist/conversion/hub/process/chat-process.js +100 -18
- package/dist/conversion/hub/response/provider-response.d.ts +13 -1
- package/dist/conversion/hub/response/provider-response.js +84 -35
- package/dist/conversion/hub/response/server-side-tools.js +61 -4
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +123 -3
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
- package/dist/conversion/hub/standardized-bridge.js +14 -0
- package/dist/conversion/responses/responses-openai-bridge.js +35 -2
- package/dist/conversion/shared/anthropic-message-utils.js +92 -3
- package/dist/conversion/shared/bridge-message-utils.js +137 -10
- package/dist/conversion/shared/responses-output-builder.js +43 -2
- package/dist/conversion/shared/tool-filter-pipeline.js +1 -0
- package/dist/router/virtual-router/bootstrap.js +44 -12
- package/dist/router/virtual-router/classifier.js +11 -17
- package/dist/router/virtual-router/engine.d.ts +9 -0
- package/dist/router/virtual-router/engine.js +160 -18
- package/dist/router/virtual-router/features.js +1 -1
- package/dist/router/virtual-router/message-utils.js +36 -24
- package/dist/router/virtual-router/provider-registry.js +2 -1
- package/dist/router/virtual-router/token-counter.js +14 -3
- package/dist/router/virtual-router/types.d.ts +45 -0
- package/dist/router/virtual-router/types.js +2 -1
- package/dist/servertool/engine.d.ts +27 -0
- package/dist/servertool/engine.js +60 -0
- package/dist/servertool/flow-types.d.ts +40 -0
- package/dist/servertool/flow-types.js +1 -0
- package/dist/servertool/handlers/vision.d.ts +1 -0
- package/dist/servertool/handlers/vision.js +194 -0
- package/dist/servertool/handlers/web-search.d.ts +1 -0
- package/dist/servertool/handlers/web-search.js +638 -0
- package/dist/servertool/orchestration-types.d.ts +33 -0
- package/dist/servertool/orchestration-types.js +1 -0
- package/dist/servertool/registry.d.ts +18 -0
- package/dist/servertool/registry.js +27 -0
- package/dist/servertool/server-side-tools.d.ts +8 -0
- package/dist/servertool/server-side-tools.js +208 -0
- package/dist/servertool/types.d.ts +88 -0
- package/dist/servertool/types.js +1 -0
- package/dist/servertool/vision-tool.d.ts +2 -0
- package/dist/servertool/vision-tool.js +185 -0
- package/dist/sse/sse-to-json/builders/response-builder.js +6 -3
- package/package.json +1 -1
|
@@ -11,6 +11,9 @@ import { writeCompatSnapshot } from '../../../compat/actions/snapshot.js';
|
|
|
11
11
|
import { applyQwenRequestTransform, applyQwenResponseTransform } from '../../../compat/actions/qwen-transform.js';
|
|
12
12
|
import { extractGlmToolMarkup } from '../../../compat/actions/glm-tool-extraction.js';
|
|
13
13
|
import { applyGlmWebSearchRequestTransform } from '../../../compat/actions/glm-web-search.js';
|
|
14
|
+
import { applyGeminiWebSearchCompat } from '../../../compat/actions/gemini-web-search.js';
|
|
15
|
+
import { applyGlmImageContentTransform } from '../../../compat/actions/glm-image-content.js';
|
|
16
|
+
import { applyGlmVisionPromptTransform } from '../../../compat/actions/glm-vision-prompt.js';
|
|
14
17
|
const RATE_LIMIT_ERROR = 'ERR_COMPAT_RATE_LIMIT_DETECTED';
|
|
15
18
|
const INTERNAL_STATE = Symbol('compat.internal_state');
|
|
16
19
|
export function runRequestCompatPipeline(profileId, payload, options) {
|
|
@@ -163,6 +166,21 @@ function applyMapping(root, mapping, state) {
|
|
|
163
166
|
replaceRoot(root, applyGlmWebSearchRequestTransform(root));
|
|
164
167
|
}
|
|
165
168
|
break;
|
|
169
|
+
case 'gemini_web_search_request':
|
|
170
|
+
if (state.direction === 'request') {
|
|
171
|
+
replaceRoot(root, applyGeminiWebSearchCompat(root, state.adapterContext));
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
case 'glm_image_content':
|
|
175
|
+
if (state.direction === 'request') {
|
|
176
|
+
replaceRoot(root, applyGlmImageContentTransform(root));
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case 'glm_vision_prompt':
|
|
180
|
+
if (state.direction === 'request') {
|
|
181
|
+
replaceRoot(root, applyGlmVisionPromptTransform(root));
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
166
184
|
default:
|
|
167
185
|
break;
|
|
168
186
|
}
|
|
@@ -100,6 +100,12 @@ export type MappingInstruction = {
|
|
|
100
100
|
action: 'qwen_response_transform';
|
|
101
101
|
} | {
|
|
102
102
|
action: 'glm_web_search_request';
|
|
103
|
+
} | {
|
|
104
|
+
action: 'glm_image_content';
|
|
105
|
+
} | {
|
|
106
|
+
action: 'glm_vision_prompt';
|
|
107
|
+
} | {
|
|
108
|
+
action: 'gemini_web_search_request';
|
|
103
109
|
};
|
|
104
110
|
export type FilterInstruction = {
|
|
105
111
|
action: 'rate_limit_text';
|
|
@@ -120,6 +120,9 @@ export class HubPipeline {
|
|
|
120
120
|
const responsesResume = normalizedMeta && typeof normalizedMeta.responsesResume === 'object'
|
|
121
121
|
? normalizedMeta.responsesResume
|
|
122
122
|
: undefined;
|
|
123
|
+
const stdMetadata = workingRequest?.metadata;
|
|
124
|
+
const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
|
|
125
|
+
stdMetadata?.serverToolRequired === true;
|
|
123
126
|
const metadataInput = {
|
|
124
127
|
requestId: normalized.id,
|
|
125
128
|
entryEndpoint: normalized.entryEndpoint,
|
|
@@ -129,7 +132,8 @@ export class HubPipeline {
|
|
|
129
132
|
providerProtocol: normalized.providerProtocol,
|
|
130
133
|
routeHint: normalized.routeHint,
|
|
131
134
|
stage: normalized.stage,
|
|
132
|
-
responsesResume: responsesResume
|
|
135
|
+
responsesResume: responsesResume,
|
|
136
|
+
...(serverToolRequired ? { serverToolRequired: true } : {})
|
|
133
137
|
};
|
|
134
138
|
const routing = runReqProcessStage2RouteSelect({
|
|
135
139
|
routerEngine: this.routerEngine,
|
|
@@ -230,8 +234,25 @@ export class HubPipeline {
|
|
|
230
234
|
}
|
|
231
235
|
});
|
|
232
236
|
}
|
|
237
|
+
// 为响应侧 servertool/web_search 提供一次性 Chat 请求快照,便于在 Hub 内部实现
|
|
238
|
+
// 第三跳(将工具结果注入消息历史后重新调用主模型)。
|
|
239
|
+
let capturedChatRequest;
|
|
240
|
+
if (normalized.processMode !== 'passthrough') {
|
|
241
|
+
try {
|
|
242
|
+
capturedChatRequest = JSON.parse(JSON.stringify({
|
|
243
|
+
model: workingRequest.model,
|
|
244
|
+
messages: workingRequest.messages,
|
|
245
|
+
tools: workingRequest.tools,
|
|
246
|
+
parameters: workingRequest.parameters
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
capturedChatRequest = undefined;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
233
253
|
const metadata = {
|
|
234
254
|
...normalized.metadata,
|
|
255
|
+
...(capturedChatRequest ? { capturedChatRequest } : {}),
|
|
235
256
|
entryEndpoint: normalized.entryEndpoint,
|
|
236
257
|
providerProtocol: outboundProtocol,
|
|
237
258
|
stream: normalized.stream,
|
|
@@ -351,6 +372,12 @@ export class HubPipeline {
|
|
|
351
372
|
if (typeof metadata.assignedModelId === 'string') {
|
|
352
373
|
adapterContext.modelId = metadata.assignedModelId;
|
|
353
374
|
}
|
|
375
|
+
// 将 serverToolFollowup 等 ServerTool 相关标记从 normalized.metadata 透传到 AdapterContext,
|
|
376
|
+
// 便于响应侧的 convertProviderResponse 正确识别“二跳/内部跳转”并跳过 servertool 编排。
|
|
377
|
+
if (Object.prototype.hasOwnProperty.call(metadata, 'serverToolFollowup')) {
|
|
378
|
+
adapterContext.serverToolFollowup = metadata
|
|
379
|
+
.serverToolFollowup;
|
|
380
|
+
}
|
|
354
381
|
if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
|
|
355
382
|
adapterContext.compatibilityProfile = target.compatibilityProfile;
|
|
356
383
|
}
|
|
@@ -9,6 +9,12 @@ export function applyTargetMetadata(metadata, target, routeName, originalModel)
|
|
|
9
9
|
metadata.providerType = target.providerType;
|
|
10
10
|
metadata.modelId = target.modelId;
|
|
11
11
|
metadata.processMode = target.processMode || 'chat';
|
|
12
|
+
if (target.forceWebSearch === true) {
|
|
13
|
+
metadata.forceWebSearch = true;
|
|
14
|
+
}
|
|
15
|
+
if (target.forceVision === true) {
|
|
16
|
+
metadata.forceVision = true;
|
|
17
|
+
}
|
|
12
18
|
if (target.responsesConfig?.toolCallIdStyle) {
|
|
13
19
|
metadata.toolCallIdStyle = target.responsesConfig.toolCallIdStyle;
|
|
14
20
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js';
|
|
2
2
|
import { ToolGovernanceEngine } from '../tool-governance/index.js';
|
|
3
|
+
import { detectLastAssistantToolCategory } from '../../../router/virtual-router/tool-signals.js';
|
|
3
4
|
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
4
5
|
export async function runHubChatProcess(options) {
|
|
5
6
|
const startTime = Date.now();
|
|
@@ -71,6 +72,14 @@ async function applyRequestToolGovernance(request, context) {
|
|
|
71
72
|
governanceTimestamp: Date.now()
|
|
72
73
|
}
|
|
73
74
|
};
|
|
75
|
+
if (containsImageAttachment(merged.messages)) {
|
|
76
|
+
if (!merged.metadata) {
|
|
77
|
+
merged.metadata = {
|
|
78
|
+
originalEndpoint: request.metadata?.originalEndpoint ?? '/v1/chat/completions'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
merged.metadata.hasImageAttachment = true;
|
|
82
|
+
}
|
|
74
83
|
if (typeof inboundStreamIntent === 'boolean') {
|
|
75
84
|
merged.metadata = {
|
|
76
85
|
...merged.metadata,
|
|
@@ -196,6 +205,34 @@ function castSingleTool(tool) {
|
|
|
196
205
|
}
|
|
197
206
|
};
|
|
198
207
|
}
|
|
208
|
+
function containsImageAttachment(messages) {
|
|
209
|
+
if (!Array.isArray(messages)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
for (const message of messages) {
|
|
213
|
+
if (!message || typeof message !== 'object') {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const content = message.content;
|
|
217
|
+
if (!Array.isArray(content)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
for (const part of content) {
|
|
221
|
+
if (!part || typeof part !== 'object') {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const typeValue = part.type;
|
|
225
|
+
if (typeof typeValue !== 'string') {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const normalized = typeValue.toLowerCase();
|
|
229
|
+
if (normalized.includes('image')) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
199
236
|
function castCustomTool(tool) {
|
|
200
237
|
if (!isRecord(tool)) {
|
|
201
238
|
return null;
|
|
@@ -277,15 +314,34 @@ function isRecord(value) {
|
|
|
277
314
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
278
315
|
}
|
|
279
316
|
function maybeInjectWebSearchTool(request, metadata) {
|
|
317
|
+
// ServerTool 二/三跳(serverToolFollowup=true)不再注入 web_search 工具,
|
|
318
|
+
// 以避免在 web_search 流程内部形成循环命中。
|
|
319
|
+
if (metadata.serverToolFollowup === true) {
|
|
320
|
+
return request;
|
|
321
|
+
}
|
|
280
322
|
const rawConfig = metadata.webSearch;
|
|
281
323
|
if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
|
|
282
324
|
return request;
|
|
283
325
|
}
|
|
284
|
-
const injectPolicy =
|
|
326
|
+
const injectPolicy = rawConfig.injectPolicy === 'always' || rawConfig.injectPolicy === 'selective'
|
|
285
327
|
? rawConfig.injectPolicy
|
|
286
328
|
: 'selective';
|
|
287
|
-
if (injectPolicy === 'selective'
|
|
288
|
-
|
|
329
|
+
if (injectPolicy === 'selective') {
|
|
330
|
+
const hasExplicitIntent = detectWebSearchIntent(request);
|
|
331
|
+
if (!hasExplicitIntent) {
|
|
332
|
+
// 当最近一条用户消息没有明显的“联网搜索”关键词时,
|
|
333
|
+
// 如果上一轮 assistant 的工具调用已经属于搜索类(如 web_search),
|
|
334
|
+
// 则仍然视为 web_search 续写场景,强制注入 web_search 工具,
|
|
335
|
+
// 以便在后续路由中按 servertool 逻辑跳过不适配的 Provider(例如 serverToolsDisabled 的 crs)。
|
|
336
|
+
const assistantMessages = Array.isArray(request.messages)
|
|
337
|
+
? request.messages.filter((msg) => msg && msg.role === 'assistant')
|
|
338
|
+
: [];
|
|
339
|
+
const lastTool = detectLastAssistantToolCategory(assistantMessages);
|
|
340
|
+
const hasSearchToolContext = lastTool?.category === 'search';
|
|
341
|
+
if (!hasSearchToolContext) {
|
|
342
|
+
return request;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
289
345
|
}
|
|
290
346
|
const existingTools = Array.isArray(request.tools) ? request.tools : [];
|
|
291
347
|
const hasWebSearch = existingTools.some((tool) => {
|
|
@@ -297,7 +353,7 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
297
353
|
if (hasWebSearch) {
|
|
298
354
|
return request;
|
|
299
355
|
}
|
|
300
|
-
const engines = rawConfig.engines.filter((engine) => typeof engine?.id === 'string' && !!engine.id.trim());
|
|
356
|
+
const engines = rawConfig.engines.filter((engine) => typeof engine?.id === 'string' && !!engine.id.trim() && !engine.serverToolsDisabled);
|
|
301
357
|
if (!engines.length) {
|
|
302
358
|
return request;
|
|
303
359
|
}
|
|
@@ -311,19 +367,14 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
311
367
|
return desc ? `${id}: ${desc}` : id;
|
|
312
368
|
})
|
|
313
369
|
.join('; ');
|
|
314
|
-
const hasMultipleEngines = engineIds.length > 1;
|
|
315
370
|
const parameters = {
|
|
316
371
|
type: 'object',
|
|
317
372
|
properties: {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
description: engineDescriptions
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
: {}),
|
|
373
|
+
engine: {
|
|
374
|
+
type: 'string',
|
|
375
|
+
enum: engineIds,
|
|
376
|
+
description: engineDescriptions
|
|
377
|
+
},
|
|
327
378
|
query: {
|
|
328
379
|
type: 'string',
|
|
329
380
|
description: 'Search query or user question.'
|
|
@@ -340,7 +391,9 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
340
391
|
description: 'Number of results to retrieve.'
|
|
341
392
|
}
|
|
342
393
|
},
|
|
343
|
-
required
|
|
394
|
+
// 对于 Responses 内建 web_search,required 需要覆盖 properties 中的所有字段,
|
|
395
|
+
// 否则上游会报 "required is required to be supplied and to be an array including every key in properties"。
|
|
396
|
+
required: ['engine', 'query', 'recency', 'count'],
|
|
344
397
|
additionalProperties: false
|
|
345
398
|
};
|
|
346
399
|
const webSearchTool = {
|
|
@@ -367,11 +420,40 @@ function detectWebSearchIntent(request) {
|
|
|
367
420
|
if (!messages.length) {
|
|
368
421
|
return false;
|
|
369
422
|
}
|
|
370
|
-
|
|
371
|
-
|
|
423
|
+
// 从末尾向前找到最近一条 user 消息,忽略 tool / assistant 的工具调用轮次,
|
|
424
|
+
// 以便在 Responses / 多轮工具调用场景下仍然根据“最近一条用户输入”判断意图。
|
|
425
|
+
let lastUser;
|
|
426
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
427
|
+
const candidate = messages[idx];
|
|
428
|
+
if (candidate && candidate.role === 'user') {
|
|
429
|
+
lastUser = candidate;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!lastUser) {
|
|
372
434
|
return false;
|
|
373
435
|
}
|
|
374
|
-
|
|
436
|
+
// 支持多模态 content:既可能是纯文本字符串,也可能是带 image_url 的分段数组。
|
|
437
|
+
let content = '';
|
|
438
|
+
if (typeof lastUser.content === 'string') {
|
|
439
|
+
content = lastUser.content;
|
|
440
|
+
}
|
|
441
|
+
else if (Array.isArray(lastUser.content)) {
|
|
442
|
+
const parts = lastUser.content;
|
|
443
|
+
const texts = [];
|
|
444
|
+
for (const part of parts) {
|
|
445
|
+
if (typeof part === 'string') {
|
|
446
|
+
texts.push(part);
|
|
447
|
+
}
|
|
448
|
+
else if (part && typeof part === 'object') {
|
|
449
|
+
const maybeText = part.text;
|
|
450
|
+
if (typeof maybeText === 'string' && maybeText.trim()) {
|
|
451
|
+
texts.push(maybeText);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
content = texts.join('\n');
|
|
456
|
+
}
|
|
375
457
|
if (!content) {
|
|
376
458
|
return false;
|
|
377
459
|
}
|
|
@@ -2,7 +2,7 @@ import { Readable } from 'node:stream';
|
|
|
2
2
|
import type { AdapterContext } from '../types/chat-envelope.js';
|
|
3
3
|
import type { JsonObject } from '../types/json.js';
|
|
4
4
|
import type { StageRecorder } from '../format-adapters/index.js';
|
|
5
|
-
import type { ProviderInvoker } from '
|
|
5
|
+
import type { ProviderInvoker } from '../../../servertool/types.js';
|
|
6
6
|
type ProviderProtocol = 'openai-chat' | 'openai-responses' | 'anthropic-messages' | 'gemini-chat';
|
|
7
7
|
export interface ProviderResponseConversionOptions {
|
|
8
8
|
providerProtocol: ProviderProtocol;
|
|
@@ -12,6 +12,18 @@ export interface ProviderResponseConversionOptions {
|
|
|
12
12
|
wantsStream: boolean;
|
|
13
13
|
stageRecorder?: StageRecorder;
|
|
14
14
|
providerInvoker?: ProviderInvoker;
|
|
15
|
+
/**
|
|
16
|
+
* 可选:由 Host 注入的二次请求入口。Server-side 工具在需要发起
|
|
17
|
+
* followup 请求(例如 web_search 二跳)时,可以通过该回调将构造
|
|
18
|
+
* 好的请求体交给 Host,由 Host 走完整 HubPipeline + VirtualRouter
|
|
19
|
+
* 再返回最终客户端响应形状。
|
|
20
|
+
*/
|
|
21
|
+
reenterPipeline?: (options: {
|
|
22
|
+
entryEndpoint: string;
|
|
23
|
+
requestId: string;
|
|
24
|
+
body: JsonObject;
|
|
25
|
+
metadata?: JsonObject;
|
|
26
|
+
}) => Promise<ProviderResponseConversionResult>;
|
|
15
27
|
}
|
|
16
28
|
export interface ProviderResponseConversionResult {
|
|
17
29
|
body?: JsonObject;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordStage } from '../pipeline/stages/utils.js';
|
|
1
2
|
import { ChatFormatAdapter } from '../format-adapters/chat-format-adapter.js';
|
|
2
3
|
import { ResponsesFormatAdapter } from '../format-adapters/responses-format-adapter.js';
|
|
3
4
|
import { AnthropicFormatAdapter } from '../format-adapters/anthropic-format-adapter.js';
|
|
@@ -12,45 +13,36 @@ import { runRespProcessStage2Finalize } from '../pipeline/stages/resp_process/re
|
|
|
12
13
|
import { runRespOutboundStage1ClientRemap } from '../pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js';
|
|
13
14
|
import { runRespOutboundStage2SseStream } from '../pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js';
|
|
14
15
|
import { recordResponsesResponse } from '../../shared/responses-conversation-store.js';
|
|
15
|
-
import {
|
|
16
|
-
function resolveChatReasoningMode(entryEndpoint) {
|
|
17
|
-
const envRaw = (process.env.ROUTECODEX_CHAT_REASONING_MODE || process.env.RCC_CHAT_REASONING_MODE || '').trim().toLowerCase();
|
|
18
|
-
const map = {
|
|
19
|
-
keep: 'keep',
|
|
20
|
-
drop: 'drop',
|
|
21
|
-
discard: 'drop',
|
|
22
|
-
text: 'append_to_content',
|
|
23
|
-
append: 'append_to_content',
|
|
24
|
-
append_text: 'append_to_content',
|
|
25
|
-
append_to_content: 'append_to_content'
|
|
26
|
-
};
|
|
27
|
-
if (envRaw && map[envRaw]) {
|
|
28
|
-
return map[envRaw];
|
|
29
|
-
}
|
|
30
|
-
return 'keep';
|
|
31
|
-
}
|
|
16
|
+
import { runServerToolOrchestration } from '../../../servertool/engine.js';
|
|
32
17
|
const PROVIDER_RESPONSE_REGISTRY = {
|
|
33
18
|
'openai-chat': {
|
|
34
|
-
protocol: 'openai-chat',
|
|
35
19
|
createFormatAdapter: () => new ChatFormatAdapter(),
|
|
36
20
|
createMapper: () => new OpenAIChatResponseMapper()
|
|
37
21
|
},
|
|
38
22
|
'openai-responses': {
|
|
39
|
-
protocol: 'openai-responses',
|
|
40
23
|
createFormatAdapter: () => new ResponsesFormatAdapter(),
|
|
41
24
|
createMapper: () => new ResponsesResponseMapper()
|
|
42
25
|
},
|
|
43
26
|
'anthropic-messages': {
|
|
44
|
-
protocol: 'anthropic-messages',
|
|
45
27
|
createFormatAdapter: () => new AnthropicFormatAdapter(),
|
|
46
28
|
createMapper: () => new AnthropicResponseMapper()
|
|
47
29
|
},
|
|
48
30
|
'gemini-chat': {
|
|
49
|
-
protocol: 'gemini-chat',
|
|
50
31
|
createFormatAdapter: () => new GeminiFormatAdapter(),
|
|
51
32
|
createMapper: () => new GeminiResponseMapper()
|
|
52
33
|
}
|
|
53
34
|
};
|
|
35
|
+
function isServerToolFollowup(context) {
|
|
36
|
+
const raw = context.serverToolFollowup;
|
|
37
|
+
if (raw === true) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (typeof raw === 'string') {
|
|
41
|
+
const v = raw.trim().toLowerCase();
|
|
42
|
+
return v === '1' || v === 'true';
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
54
46
|
function resolveClientProtocol(entryEndpoint) {
|
|
55
47
|
const lowered = (entryEndpoint || '').toLowerCase();
|
|
56
48
|
if (lowered.includes('/v1/responses'))
|
|
@@ -86,8 +78,28 @@ function applyModelOverride(payload, model) {
|
|
|
86
78
|
/* ignore */
|
|
87
79
|
}
|
|
88
80
|
}
|
|
81
|
+
function resolveChatReasoningMode(_entryEndpoint) {
|
|
82
|
+
// 当前保持默认策略:保留 reasoning_content 字段,不做额外拼接或删除。
|
|
83
|
+
return 'keep';
|
|
84
|
+
}
|
|
89
85
|
export async function convertProviderResponse(options) {
|
|
90
86
|
const clientProtocol = resolveClientProtocol(options.entryEndpoint);
|
|
87
|
+
const hasServerToolSupport = Boolean(options.providerInvoker) || Boolean(options.reenterPipeline);
|
|
88
|
+
const skipServerTools = isServerToolFollowup(options.context) || !hasServerToolSupport;
|
|
89
|
+
// 对于由 server-side 工具触发的内部跳转(二跳/三跳),统一禁用 SSE 聚合输出,
|
|
90
|
+
// 始终返回完整的 ChatCompletion JSON,便于在 llms 内部直接解析,而不是拿到
|
|
91
|
+
// __sse_responses 可读流。
|
|
92
|
+
const wantsStream = isServerToolFollowup(options.context) ? false : options.wantsStream;
|
|
93
|
+
try {
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.log(`\x1b[38;5;33m[servertool][orchestrator][debug] requestId=${options.context.requestId} ` +
|
|
96
|
+
`protocol=${options.providerProtocol} endpoint=${options.entryEndpoint} ` +
|
|
97
|
+
`skipServerTools=${skipServerTools} hasInvoker=${Boolean(options.providerInvoker)} ` +
|
|
98
|
+
`hasReenter=${Boolean(options.reenterPipeline)}\x1b[0m`);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* logging best-effort */
|
|
102
|
+
}
|
|
91
103
|
const displayModel = extractDisplayModel(options.context);
|
|
92
104
|
const plan = PROVIDER_RESPONSE_REGISTRY[options.providerProtocol];
|
|
93
105
|
if (!plan) {
|
|
@@ -97,7 +109,7 @@ export async function convertProviderResponse(options) {
|
|
|
97
109
|
providerProtocol: options.providerProtocol,
|
|
98
110
|
payload: options.providerResponse,
|
|
99
111
|
adapterContext: options.context,
|
|
100
|
-
wantsStream
|
|
112
|
+
wantsStream,
|
|
101
113
|
stageRecorder: options.stageRecorder
|
|
102
114
|
});
|
|
103
115
|
const formatAdapter = plan.createFormatAdapter();
|
|
@@ -138,18 +150,55 @@ export async function convertProviderResponse(options) {
|
|
|
138
150
|
mapper,
|
|
139
151
|
stageRecorder: options.stageRecorder
|
|
140
152
|
});
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
// 记录语义映射后的 ChatCompletion,便于回放 server-side 工具流程。
|
|
154
|
+
recordStage(options.stageRecorder, 'resp_inbound_stage3_semantic_map.chat', chatResponse);
|
|
155
|
+
// 检查是否需要进行 ServerTool 编排
|
|
156
|
+
// 使用新的 ChatEnvelope 级别的 servertool 实现
|
|
157
|
+
let effectiveChatResponse = chatResponse;
|
|
158
|
+
if (!skipServerTools && options.reenterPipeline) {
|
|
159
|
+
try {
|
|
160
|
+
// eslint-disable-next-line no-console
|
|
161
|
+
console.log(`\x1b[38;5;33m[servertool][orchestrator] start requestId=${options.context.requestId} ` +
|
|
162
|
+
`protocol=${options.providerProtocol} endpoint=${options.entryEndpoint}\x1b[0m`);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
/* logging best-effort */
|
|
166
|
+
}
|
|
167
|
+
const orchestration = await runServerToolOrchestration({
|
|
168
|
+
chat: chatResponse,
|
|
169
|
+
adapterContext: options.context,
|
|
170
|
+
requestId: options.context.requestId,
|
|
171
|
+
entryEndpoint: options.entryEndpoint,
|
|
172
|
+
providerProtocol: options.providerProtocol,
|
|
173
|
+
providerInvoker: options.providerInvoker,
|
|
174
|
+
reenterPipeline: options.reenterPipeline
|
|
175
|
+
});
|
|
176
|
+
if (orchestration.executed) {
|
|
177
|
+
const flowLabel = orchestration.flowId ?? 'servertool_flow';
|
|
178
|
+
try {
|
|
179
|
+
// eslint-disable-next-line no-console
|
|
180
|
+
console.log(`\x1b[38;5;33m[servertool][orchestrator] completed requestId=${options.context.requestId} ` +
|
|
181
|
+
`mode=${flowLabel}\x1b[0m`);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* logging best-effort */
|
|
185
|
+
}
|
|
186
|
+
effectiveChatResponse = orchestration.chat;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
try {
|
|
190
|
+
// eslint-disable-next-line no-console
|
|
191
|
+
console.log(`\x1b[38;5;33m[servertool][orchestrator] skipped requestId=${options.context.requestId} ` +
|
|
192
|
+
'reason=no_servertool_match\x1b[0m');
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
/* logging best-effort */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// 如果没有执行 servertool,继续原来的处理流程
|
|
151
200
|
const governanceResult = await runRespProcessStage1ToolGovernance({
|
|
152
|
-
payload:
|
|
201
|
+
payload: effectiveChatResponse,
|
|
153
202
|
entryEndpoint: options.entryEndpoint,
|
|
154
203
|
requestId: options.context.requestId,
|
|
155
204
|
clientProtocol,
|
|
@@ -159,7 +208,7 @@ export async function convertProviderResponse(options) {
|
|
|
159
208
|
payload: governanceResult.governedPayload,
|
|
160
209
|
entryEndpoint: options.entryEndpoint,
|
|
161
210
|
requestId: options.context.requestId,
|
|
162
|
-
wantsStream
|
|
211
|
+
wantsStream,
|
|
163
212
|
reasoningMode: resolveChatReasoningMode(options.entryEndpoint),
|
|
164
213
|
stageRecorder: options.stageRecorder
|
|
165
214
|
});
|
|
@@ -176,7 +225,7 @@ export async function convertProviderResponse(options) {
|
|
|
176
225
|
clientPayload,
|
|
177
226
|
clientProtocol,
|
|
178
227
|
requestId: options.context.requestId,
|
|
179
|
-
wantsStream
|
|
228
|
+
wantsStream,
|
|
180
229
|
stageRecorder: options.stageRecorder
|
|
181
230
|
});
|
|
182
231
|
if (outbound.stream) {
|
|
@@ -32,7 +32,28 @@ function extractToolCalls(chatResponse) {
|
|
|
32
32
|
return calls;
|
|
33
33
|
}
|
|
34
34
|
function extractTextFromChatLike(payload) {
|
|
35
|
-
|
|
35
|
+
// 1) 解包常见包装层:data / response 节点
|
|
36
|
+
let current = payload;
|
|
37
|
+
const visited = new Set();
|
|
38
|
+
while (current && typeof current === 'object' && !Array.isArray(current) && !visited.has(current)) {
|
|
39
|
+
visited.add(current);
|
|
40
|
+
if (Array.isArray(current.choices) || Array.isArray(current.output)) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
const data = current.data;
|
|
44
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
45
|
+
current = data;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const response = current.response;
|
|
49
|
+
if (response && typeof response === 'object' && !Array.isArray(response)) {
|
|
50
|
+
current = response;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
// 2) 优先从 choices[].message.content 提取(OpenAI/GLM 兼容)
|
|
56
|
+
const choices = getArray(current.choices);
|
|
36
57
|
if (!choices.length)
|
|
37
58
|
return '';
|
|
38
59
|
const first = asObject(choices[0]);
|
|
@@ -43,7 +64,7 @@ function extractTextFromChatLike(payload) {
|
|
|
43
64
|
return '';
|
|
44
65
|
const content = message.content;
|
|
45
66
|
if (typeof content === 'string')
|
|
46
|
-
return content;
|
|
67
|
+
return content.trim();
|
|
47
68
|
const parts = getArray(content);
|
|
48
69
|
const texts = [];
|
|
49
70
|
for (const part of parts) {
|
|
@@ -55,9 +76,45 @@ function extractTextFromChatLike(payload) {
|
|
|
55
76
|
if (typeof record.text === 'string') {
|
|
56
77
|
texts.push(record.text);
|
|
57
78
|
}
|
|
79
|
+
else if (typeof record.content === 'string') {
|
|
80
|
+
texts.push(record.content);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const joinedFromChoices = texts.join('\n').trim();
|
|
85
|
+
if (joinedFromChoices) {
|
|
86
|
+
return joinedFromChoices;
|
|
87
|
+
}
|
|
88
|
+
// 3) 回退:从 output[].content[] 中提取(部分 Responses/自定义后端)
|
|
89
|
+
const output = current.output;
|
|
90
|
+
if (Array.isArray(output)) {
|
|
91
|
+
const altTexts = [];
|
|
92
|
+
for (const entry of output) {
|
|
93
|
+
if (!entry || typeof entry !== 'object')
|
|
94
|
+
continue;
|
|
95
|
+
const blocks = entry.content;
|
|
96
|
+
const blockArray = Array.isArray(blocks) ? blocks : [];
|
|
97
|
+
for (const block of blockArray) {
|
|
98
|
+
if (!block || typeof block !== 'object')
|
|
99
|
+
continue;
|
|
100
|
+
const record = block;
|
|
101
|
+
if (typeof record.text === 'string') {
|
|
102
|
+
altTexts.push(record.text);
|
|
103
|
+
}
|
|
104
|
+
else if (typeof record.output_text === 'string') {
|
|
105
|
+
altTexts.push(record.output_text);
|
|
106
|
+
}
|
|
107
|
+
else if (typeof record.content === 'string') {
|
|
108
|
+
altTexts.push(record.content);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const joined = altTexts.join('\n').trim();
|
|
113
|
+
if (joined) {
|
|
114
|
+
return joined;
|
|
58
115
|
}
|
|
59
116
|
}
|
|
60
|
-
return
|
|
117
|
+
return '';
|
|
61
118
|
}
|
|
62
119
|
function getWebSearchConfig(ctx) {
|
|
63
120
|
const raw = ctx.webSearch;
|
|
@@ -131,7 +188,7 @@ function resolveEnvServerSideToolsEnabled() {
|
|
|
131
188
|
return false;
|
|
132
189
|
if (raw === '1' || raw === 'true' || raw === 'yes')
|
|
133
190
|
return true;
|
|
134
|
-
if (raw === 'web_search'
|
|
191
|
+
if (raw === 'web_search')
|
|
135
192
|
return true;
|
|
136
193
|
return false;
|
|
137
194
|
}
|