@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.
Files changed (63) hide show
  1. package/README.md +2 -0
  2. package/dist/conversion/codecs/gemini-openai-codec.js +9 -1
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
  4. package/dist/conversion/compat/actions/gemini-web-search.js +68 -0
  5. package/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
  6. package/dist/conversion/compat/actions/glm-image-content.js +83 -0
  7. package/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
  8. package/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
  9. package/dist/conversion/compat/actions/glm-web-search.js +25 -28
  10. package/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
  11. package/dist/conversion/compat/profiles/chat-gemini.json +17 -0
  12. package/dist/conversion/compat/profiles/chat-glm.json +190 -184
  13. package/dist/conversion/compat/profiles/chat-iflow.json +195 -195
  14. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  15. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  16. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  17. package/dist/conversion/config/sample-config.json +1 -1
  18. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +18 -0
  19. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +6 -0
  20. package/dist/conversion/hub/pipeline/hub-pipeline.js +28 -1
  21. package/dist/conversion/hub/pipeline/target-utils.js +6 -0
  22. package/dist/conversion/hub/process/chat-process.js +100 -18
  23. package/dist/conversion/hub/response/provider-response.d.ts +13 -1
  24. package/dist/conversion/hub/response/provider-response.js +84 -35
  25. package/dist/conversion/hub/response/server-side-tools.js +61 -4
  26. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +123 -3
  27. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
  28. package/dist/conversion/hub/standardized-bridge.js +14 -0
  29. package/dist/conversion/responses/responses-openai-bridge.js +35 -2
  30. package/dist/conversion/shared/anthropic-message-utils.js +92 -3
  31. package/dist/conversion/shared/bridge-message-utils.js +137 -10
  32. package/dist/conversion/shared/responses-output-builder.js +43 -2
  33. package/dist/conversion/shared/tool-filter-pipeline.js +1 -0
  34. package/dist/router/virtual-router/bootstrap.js +44 -12
  35. package/dist/router/virtual-router/classifier.js +11 -17
  36. package/dist/router/virtual-router/engine.d.ts +9 -0
  37. package/dist/router/virtual-router/engine.js +160 -18
  38. package/dist/router/virtual-router/features.js +1 -1
  39. package/dist/router/virtual-router/message-utils.js +36 -24
  40. package/dist/router/virtual-router/provider-registry.js +2 -1
  41. package/dist/router/virtual-router/token-counter.js +14 -3
  42. package/dist/router/virtual-router/types.d.ts +45 -0
  43. package/dist/router/virtual-router/types.js +2 -1
  44. package/dist/servertool/engine.d.ts +27 -0
  45. package/dist/servertool/engine.js +60 -0
  46. package/dist/servertool/flow-types.d.ts +40 -0
  47. package/dist/servertool/flow-types.js +1 -0
  48. package/dist/servertool/handlers/vision.d.ts +1 -0
  49. package/dist/servertool/handlers/vision.js +194 -0
  50. package/dist/servertool/handlers/web-search.d.ts +1 -0
  51. package/dist/servertool/handlers/web-search.js +638 -0
  52. package/dist/servertool/orchestration-types.d.ts +33 -0
  53. package/dist/servertool/orchestration-types.js +1 -0
  54. package/dist/servertool/registry.d.ts +18 -0
  55. package/dist/servertool/registry.js +27 -0
  56. package/dist/servertool/server-side-tools.d.ts +8 -0
  57. package/dist/servertool/server-side-tools.js +208 -0
  58. package/dist/servertool/types.d.ts +88 -0
  59. package/dist/servertool/types.js +1 -0
  60. package/dist/servertool/vision-tool.d.ts +2 -0
  61. package/dist/servertool/vision-tool.js +185 -0
  62. package/dist/sse/sse-to-json/builders/response-builder.js +6 -3
  63. 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 = (rawConfig.injectPolicy === 'always' || rawConfig.injectPolicy === 'selective')
326
+ const injectPolicy = rawConfig.injectPolicy === 'always' || rawConfig.injectPolicy === 'selective'
285
327
  ? rawConfig.injectPolicy
286
328
  : 'selective';
287
- if (injectPolicy === 'selective' && !detectWebSearchIntent(request)) {
288
- return request;
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
- ...(hasMultipleEngines
319
- ? {
320
- engine: {
321
- type: 'string',
322
- enum: engineIds,
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: ['query'],
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
- const last = messages[messages.length - 1];
371
- if (!last || last.role !== 'user') {
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
- const content = typeof last.content === 'string' ? last.content : '';
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 './server-side-tools.js';
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 { runServerSideToolEngine } from './server-side-tools.js';
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: options.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
- // Server-side tool orchestration hook (web_search, etc.).
142
- const serverSideResult = await runServerSideToolEngine({
143
- chatResponse,
144
- adapterContext: options.context,
145
- entryEndpoint: options.entryEndpoint,
146
- requestId: options.context.requestId,
147
- providerProtocol: options.providerProtocol,
148
- providerInvoker: options.providerInvoker
149
- });
150
- const chatForGovernance = serverSideResult.finalChatResponse;
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: chatForGovernance,
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: options.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: options.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
- const choices = getArray(payload.choices);
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 texts.join('\n').trim();
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' || raw === 'websearch')
191
+ if (raw === 'web_search')
135
192
  return true;
136
193
  return false;
137
194
  }