@jsonstudio/rcc 0.89.333 → 0.89.524

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 (165) hide show
  1. package/dist/build-info.js +3 -3
  2. package/dist/build-info.js.map +1 -1
  3. package/dist/cli.js +62 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/token-daemon.d.ts +2 -0
  6. package/dist/commands/token-daemon.js +183 -0
  7. package/dist/commands/token-daemon.js.map +1 -0
  8. package/dist/index.js +4 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/modules/llmswitch/bridge.d.ts +1 -1
  11. package/dist/modules/llmswitch/bridge.js +3 -2
  12. package/dist/modules/llmswitch/bridge.js.map +1 -1
  13. package/dist/modules/pipeline/utils/colored-logger.js +3 -1
  14. package/dist/modules/pipeline/utils/colored-logger.js.map +1 -1
  15. package/dist/providers/auth/gemini-cli-userinfo-helper.js +12 -2
  16. package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -1
  17. package/dist/providers/auth/oauth-lifecycle.js +261 -22
  18. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  19. package/dist/providers/core/config/oauth-flows.d.ts +23 -0
  20. package/dist/providers/core/config/oauth-flows.js +92 -5
  21. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  22. package/dist/providers/core/config/provider-oauth-configs.js +9 -3
  23. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  24. package/dist/providers/core/config/service-profiles.js +18 -10
  25. package/dist/providers/core/config/service-profiles.js.map +1 -1
  26. package/dist/providers/core/runtime/gemini-cli-http-provider.js +87 -20
  27. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  28. package/dist/providers/core/runtime/http-request-executor.d.ts +1 -0
  29. package/dist/providers/core/runtime/http-request-executor.js +41 -1
  30. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  31. package/dist/providers/core/runtime/http-transport-provider.d.ts +2 -0
  32. package/dist/providers/core/runtime/http-transport-provider.js +37 -2
  33. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  34. package/dist/providers/core/runtime/responses-provider.js +8 -3
  35. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  36. package/dist/providers/core/runtime/vision-debug-utils.d.ts +13 -0
  37. package/dist/providers/core/runtime/vision-debug-utils.js +114 -0
  38. package/dist/providers/core/runtime/vision-debug-utils.js.map +1 -0
  39. package/dist/providers/core/strategies/oauth-auth-code-flow.js +75 -26
  40. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  41. package/dist/providers/core/utils/http-client.js +2 -1
  42. package/dist/providers/core/utils/http-client.js.map +1 -1
  43. package/dist/providers/core/utils/snapshot-writer.d.ts +1 -1
  44. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  45. package/dist/server/handlers/sse-dispatcher.js +22 -2
  46. package/dist/server/handlers/sse-dispatcher.js.map +1 -1
  47. package/dist/server/runtime/http-server/index.d.ts +9 -0
  48. package/dist/server/runtime/http-server/index.js +512 -144
  49. package/dist/server/runtime/http-server/index.js.map +1 -1
  50. package/dist/server/runtime/http-server/request-executor.d.ts +10 -0
  51. package/dist/server/runtime/http-server/request-executor.js +553 -159
  52. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  53. package/dist/server/runtime/http-server/routes.d.ts +5 -0
  54. package/dist/server/runtime/http-server/routes.js +69 -0
  55. package/dist/server/runtime/http-server/routes.js.map +1 -1
  56. package/dist/server/runtime/http-server/runtime-manager.js +18 -0
  57. package/dist/server/runtime/http-server/runtime-manager.js.map +1 -1
  58. package/dist/server/utils/utf8-chunk-buffer.d.ts +43 -0
  59. package/dist/server/utils/utf8-chunk-buffer.js +132 -0
  60. package/dist/server/utils/utf8-chunk-buffer.js.map +1 -0
  61. package/dist/token-daemon/index.d.ts +7 -0
  62. package/dist/token-daemon/index.js +242 -0
  63. package/dist/token-daemon/index.js.map +1 -0
  64. package/dist/token-daemon/server-utils.d.ts +33 -0
  65. package/dist/token-daemon/server-utils.js +155 -0
  66. package/dist/token-daemon/server-utils.js.map +1 -0
  67. package/dist/token-daemon/token-daemon.d.ts +20 -0
  68. package/dist/token-daemon/token-daemon.js +144 -0
  69. package/dist/token-daemon/token-daemon.js.map +1 -0
  70. package/dist/token-daemon/token-types.d.ts +44 -0
  71. package/dist/token-daemon/token-types.js +18 -0
  72. package/dist/token-daemon/token-types.js.map +1 -0
  73. package/dist/token-daemon/token-utils.d.ts +17 -0
  74. package/dist/token-daemon/token-utils.js +153 -0
  75. package/dist/token-daemon/token-utils.js.map +1 -0
  76. package/dist/tools/semantic-replay.js +7 -6
  77. package/dist/tools/semantic-replay.js.map +1 -1
  78. package/dist/utils/error-handler-registry.d.ts +36 -0
  79. package/dist/utils/error-handler-registry.js +93 -7
  80. package/dist/utils/error-handler-registry.js.map +1 -1
  81. package/node_modules/@jsonstudio/llms/README.md +2 -0
  82. package/node_modules/@jsonstudio/llms/dist/conversion/codecs/gemini-openai-codec.js +137 -5
  83. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
  84. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/gemini-web-search.js +68 -0
  85. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
  86. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-image-content.js +83 -0
  87. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
  88. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
  89. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-web-search.d.ts +2 -0
  90. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-web-search.js +63 -0
  91. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
  92. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-gemini.json +17 -0
  93. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-glm.json +190 -181
  94. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-iflow.json +195 -195
  95. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  96. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  97. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  98. package/node_modules/@jsonstudio/llms/dist/conversion/config/sample-config.json +1 -1
  99. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
  100. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  101. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline.js +39 -4
  102. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/target-utils.js +6 -0
  103. package/node_modules/@jsonstudio/llms/dist/conversion/hub/process/chat-process.js +213 -1
  104. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.d.ts +34 -0
  105. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.js +84 -24
  106. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/server-side-tools.d.ts +26 -0
  107. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/server-side-tools.js +383 -0
  108. package/node_modules/@jsonstudio/llms/dist/conversion/hub/semantic-mappers/gemini-mapper.js +241 -14
  109. package/node_modules/@jsonstudio/llms/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
  110. package/node_modules/@jsonstudio/llms/dist/conversion/hub/standardized-bridge.js +14 -0
  111. package/node_modules/@jsonstudio/llms/dist/conversion/hub/types/standardized.d.ts +1 -0
  112. package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.js +82 -3
  113. package/node_modules/@jsonstudio/llms/dist/conversion/shared/anthropic-message-utils.js +92 -3
  114. package/node_modules/@jsonstudio/llms/dist/conversion/shared/bridge-message-utils.js +137 -10
  115. package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-output-builder.js +43 -2
  116. package/node_modules/@jsonstudio/llms/dist/conversion/shared/snapshot-utils.js +17 -47
  117. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-filter-pipeline.js +1 -0
  118. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-mapping.js +25 -2
  119. package/node_modules/@jsonstudio/llms/dist/index.d.ts +1 -0
  120. package/node_modules/@jsonstudio/llms/dist/index.js +1 -0
  121. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/bootstrap.js +308 -43
  122. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/classifier.js +11 -17
  123. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/context-advisor.d.ts +0 -2
  124. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/context-advisor.js +0 -12
  125. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.d.ts +17 -2
  126. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.js +332 -95
  127. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/features.js +1 -1
  128. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/message-utils.js +36 -24
  129. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.js +2 -1
  130. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/token-counter.js +14 -3
  131. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.d.ts +66 -2
  132. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.js +2 -1
  133. package/node_modules/@jsonstudio/llms/dist/servertool/engine.d.ts +27 -0
  134. package/node_modules/@jsonstudio/llms/dist/servertool/engine.js +60 -0
  135. package/node_modules/@jsonstudio/llms/dist/servertool/flow-types.d.ts +40 -0
  136. package/node_modules/@jsonstudio/llms/dist/servertool/flow-types.js +1 -0
  137. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.d.ts +1 -0
  138. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.js +194 -0
  139. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.d.ts +1 -0
  140. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.js +638 -0
  141. package/node_modules/@jsonstudio/llms/dist/servertool/orchestration-types.d.ts +33 -0
  142. package/node_modules/@jsonstudio/llms/dist/servertool/orchestration-types.js +1 -0
  143. package/node_modules/@jsonstudio/llms/dist/servertool/registry.d.ts +18 -0
  144. package/node_modules/@jsonstudio/llms/dist/servertool/registry.js +27 -0
  145. package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.d.ts +8 -0
  146. package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.js +208 -0
  147. package/node_modules/@jsonstudio/llms/dist/servertool/types.d.ts +88 -0
  148. package/node_modules/@jsonstudio/llms/dist/servertool/types.js +1 -0
  149. package/node_modules/@jsonstudio/llms/dist/servertool/vision-tool.d.ts +2 -0
  150. package/node_modules/@jsonstudio/llms/dist/servertool/vision-tool.js +185 -0
  151. package/node_modules/@jsonstudio/llms/dist/sse/json-to-sse/event-generators/responses.js +15 -3
  152. package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/builders/response-builder.js +6 -3
  153. package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
  154. package/node_modules/@jsonstudio/llms/dist/sse/types/gemini-types.d.ts +20 -1
  155. package/node_modules/@jsonstudio/llms/dist/sse/types/responses-types.js +1 -1
  156. package/node_modules/@jsonstudio/llms/dist/telemetry/stats-center.d.ts +73 -0
  157. package/node_modules/@jsonstudio/llms/dist/telemetry/stats-center.js +280 -0
  158. package/node_modules/@jsonstudio/llms/package.json +1 -1
  159. package/package.json +2 -2
  160. package/scripts/pack-mode.mjs +2 -1
  161. package/scripts/publish-rcc.mjs +20 -4
  162. package/scripts/tests/virtual-router-health.mjs +141 -6
  163. package/dist/tools/replay-request.d.ts +0 -0
  164. package/dist/tools/replay-request.js +0 -2
  165. package/dist/tools/replay-request.js.map +0 -1
@@ -10,6 +10,10 @@ import { validateResponsePayload } from '../../../compat/actions/response-valida
10
10
  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
+ 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';
13
17
  const RATE_LIMIT_ERROR = 'ERR_COMPAT_RATE_LIMIT_DETECTED';
14
18
  const INTERNAL_STATE = Symbol('compat.internal_state');
15
19
  export function runRequestCompatPipeline(profileId, payload, options) {
@@ -157,6 +161,26 @@ function applyMapping(root, mapping, state) {
157
161
  case 'qwen_response_transform':
158
162
  replaceRoot(root, applyQwenResponseTransform(root));
159
163
  break;
164
+ case 'glm_web_search_request':
165
+ if (state.direction === 'request') {
166
+ replaceRoot(root, applyGlmWebSearchRequestTransform(root));
167
+ }
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;
160
184
  default:
161
185
  break;
162
186
  }
@@ -98,6 +98,14 @@ export type MappingInstruction = {
98
98
  action: 'qwen_request_transform';
99
99
  } | {
100
100
  action: 'qwen_response_transform';
101
+ } | {
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';
101
109
  };
102
110
  export type FilterInstruction = {
103
111
  action: 'rate_limit_text';
@@ -11,6 +11,7 @@ import { GeminiSemanticMapper } from '../semantic-mappers/gemini-mapper.js';
11
11
  import { ChatFormatAdapter } from '../format-adapters/chat-format-adapter.js';
12
12
  import { ChatSemanticMapper } from '../semantic-mappers/chat-mapper.js';
13
13
  import { createSnapshotRecorder } from '../snapshot-recorder.js';
14
+ import { shouldRecordSnapshots } from '../../shared/snapshot-utils.js';
14
15
  import { runReqInboundStage1FormatParse } from './stages/req_inbound/req_inbound_stage1_format_parse/index.js';
15
16
  import { runReqInboundStage2SemanticMap } from './stages/req_inbound/req_inbound_stage2_semantic_map/index.js';
16
17
  import { runChatContextCapture, captureResponsesContextSnapshot } from './stages/req_inbound/req_inbound_stage3_context_capture/index.js';
@@ -93,10 +94,18 @@ export class HubPipeline {
93
94
  });
94
95
  let processedRequest;
95
96
  if (normalized.processMode !== 'passthrough') {
97
+ const processMetadata = {
98
+ ...(normalized.metadata ?? {})
99
+ };
100
+ const webSearchConfig = this.config.virtualRouter?.webSearch;
101
+ if (webSearchConfig) {
102
+ processMetadata.webSearch = webSearchConfig;
103
+ }
104
+ normalized.metadata = processMetadata;
96
105
  const processResult = await runReqProcessStage1ToolGovernance({
97
106
  request: standardizedRequest,
98
107
  rawPayload: rawRequest,
99
- metadata: normalized.metadata,
108
+ metadata: processMetadata,
100
109
  entryEndpoint: normalized.entryEndpoint,
101
110
  requestId: normalized.id,
102
111
  stageRecorder: inboundRecorder
@@ -111,6 +120,9 @@ export class HubPipeline {
111
120
  const responsesResume = normalizedMeta && typeof normalizedMeta.responsesResume === 'object'
112
121
  ? normalizedMeta.responsesResume
113
122
  : undefined;
123
+ const stdMetadata = workingRequest?.metadata;
124
+ const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
125
+ stdMetadata?.serverToolRequired === true;
114
126
  const metadataInput = {
115
127
  requestId: normalized.id,
116
128
  entryEndpoint: normalized.entryEndpoint,
@@ -120,7 +132,8 @@ export class HubPipeline {
120
132
  providerProtocol: normalized.providerProtocol,
121
133
  routeHint: normalized.routeHint,
122
134
  stage: normalized.stage,
123
- responsesResume: responsesResume
135
+ responsesResume: responsesResume,
136
+ ...(serverToolRequired ? { serverToolRequired: true } : {})
124
137
  };
125
138
  const routing = runReqProcessStage2RouteSelect({
126
139
  routerEngine: this.routerEngine,
@@ -221,8 +234,25 @@ export class HubPipeline {
221
234
  }
222
235
  });
223
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
+ }
224
253
  const metadata = {
225
254
  ...normalized.metadata,
255
+ ...(capturedChatRequest ? { capturedChatRequest } : {}),
226
256
  entryEndpoint: normalized.entryEndpoint,
227
257
  providerProtocol: outboundProtocol,
228
258
  stream: normalized.stream,
@@ -342,14 +372,19 @@ export class HubPipeline {
342
372
  if (typeof metadata.assignedModelId === 'string') {
343
373
  adapterContext.modelId = metadata.assignedModelId;
344
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
+ }
345
381
  if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
346
382
  adapterContext.compatibilityProfile = target.compatibilityProfile;
347
383
  }
348
384
  return adapterContext;
349
385
  }
350
386
  maybeCreateStageRecorder(context, endpoint) {
351
- const flag = (process.env.ROUTECODEX_HUB_SNAPSHOTS || '').trim();
352
- if (flag === '0') {
387
+ if (!shouldRecordSnapshots()) {
353
388
  return undefined;
354
389
  }
355
390
  const effectiveEndpoint = endpoint || context.entryEndpoint || '/v1/chat/completions';
@@ -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();
@@ -51,7 +52,7 @@ async function applyRequestToolGovernance(request, context) {
51
52
  });
52
53
  const governed = normalizeRecord(governedPayload);
53
54
  const providerStreamIntent = typeof governed.stream === 'boolean' ? governed.stream : undefined;
54
- const merged = {
55
+ let merged = {
55
56
  ...request,
56
57
  messages: Array.isArray(governed.messages)
57
58
  ? governed.messages
@@ -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,
@@ -92,6 +101,8 @@ async function applyRequestToolGovernance(request, context) {
92
101
  if (typeof governed.model === 'string' && governed.model.trim()) {
93
102
  merged.model = governed.model.trim();
94
103
  }
104
+ // Server-side web_search tool injection (config-driven, best-effort).
105
+ merged = maybeInjectWebSearchTool(merged, metadata);
95
106
  const { request: sanitized, summary } = toolGovernanceEngine.governRequest(merged, providerProtocol);
96
107
  if (summary.applied) {
97
108
  sanitized.metadata = {
@@ -194,6 +205,34 @@ function castSingleTool(tool) {
194
205
  }
195
206
  };
196
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
+ }
197
236
  function castCustomTool(tool) {
198
237
  if (!isRecord(tool)) {
199
238
  return null;
@@ -274,3 +313,176 @@ function readToolChoice(value) {
274
313
  function isRecord(value) {
275
314
  return !!value && typeof value === 'object' && !Array.isArray(value);
276
315
  }
316
+ function maybeInjectWebSearchTool(request, metadata) {
317
+ // ServerTool 二/三跳(serverToolFollowup=true)不再注入 web_search 工具,
318
+ // 以避免在 web_search 流程内部形成循环命中。
319
+ if (metadata.serverToolFollowup === true) {
320
+ return request;
321
+ }
322
+ const rawConfig = metadata.webSearch;
323
+ if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
324
+ return request;
325
+ }
326
+ const injectPolicy = rawConfig.injectPolicy === 'always' || rawConfig.injectPolicy === 'selective'
327
+ ? rawConfig.injectPolicy
328
+ : 'selective';
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
+ }
345
+ }
346
+ const existingTools = Array.isArray(request.tools) ? request.tools : [];
347
+ const hasWebSearch = existingTools.some((tool) => {
348
+ if (!tool || typeof tool !== 'object')
349
+ return false;
350
+ const fn = tool.function;
351
+ return typeof fn?.name === 'string' && fn.name.trim() === 'web_search';
352
+ });
353
+ if (hasWebSearch) {
354
+ return request;
355
+ }
356
+ const engines = rawConfig.engines.filter((engine) => typeof engine?.id === 'string' && !!engine.id.trim() && !engine.serverToolsDisabled);
357
+ if (!engines.length) {
358
+ return request;
359
+ }
360
+ const engineIds = engines.map((engine) => engine.id.trim());
361
+ const engineDescriptions = engines
362
+ .map((engine) => {
363
+ const id = engine.id.trim();
364
+ const desc = typeof engine.description === 'string' && engine.description.trim()
365
+ ? engine.description.trim()
366
+ : '';
367
+ return desc ? `${id}: ${desc}` : id;
368
+ })
369
+ .join('; ');
370
+ const parameters = {
371
+ type: 'object',
372
+ properties: {
373
+ engine: {
374
+ type: 'string',
375
+ enum: engineIds,
376
+ description: engineDescriptions
377
+ },
378
+ query: {
379
+ type: 'string',
380
+ description: 'Search query or user question.'
381
+ },
382
+ recency: {
383
+ type: 'string',
384
+ enum: ['oneDay', 'oneWeek', 'oneMonth', 'oneYear', 'noLimit'],
385
+ description: 'Optional recency filter for web search results.'
386
+ },
387
+ count: {
388
+ type: 'integer',
389
+ minimum: 1,
390
+ maximum: 50,
391
+ description: 'Number of results to retrieve.'
392
+ }
393
+ },
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'],
397
+ additionalProperties: false
398
+ };
399
+ const webSearchTool = {
400
+ type: 'function',
401
+ function: {
402
+ name: 'web_search',
403
+ description: 'Perform web search using configured search engines. Use this when the user asks for up-to-date information or news.',
404
+ parameters,
405
+ strict: true
406
+ }
407
+ };
408
+ const nextMetadata = {
409
+ ...(request.metadata ?? {}),
410
+ webSearchEnabled: true
411
+ };
412
+ return {
413
+ ...request,
414
+ metadata: nextMetadata,
415
+ tools: [...existingTools, webSearchTool]
416
+ };
417
+ }
418
+ function detectWebSearchIntent(request) {
419
+ const messages = Array.isArray(request.messages) ? request.messages : [];
420
+ if (!messages.length) {
421
+ return false;
422
+ }
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) {
434
+ return false;
435
+ }
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
+ }
457
+ if (!content) {
458
+ return false;
459
+ }
460
+ const text = content.toLowerCase();
461
+ const keywords = [
462
+ // English
463
+ 'web search',
464
+ 'web_search',
465
+ 'websearch',
466
+ 'internet search',
467
+ 'search the web',
468
+ 'online search',
469
+ 'search online',
470
+ 'search on the internet',
471
+ 'search the internet',
472
+ 'web-search',
473
+ 'online-search',
474
+ 'internet-search',
475
+ // Chinese
476
+ '联网搜索',
477
+ '网络搜索',
478
+ '上网搜索',
479
+ '网上搜索',
480
+ '网上查',
481
+ '网上查找',
482
+ '上网查',
483
+ '上网搜',
484
+ // Command-style
485
+ '/search'
486
+ ];
487
+ return keywords.some((keyword) => text.includes(keyword.toLowerCase()));
488
+ }
@@ -0,0 +1,34 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { AdapterContext } from '../types/chat-envelope.js';
3
+ import type { JsonObject } from '../types/json.js';
4
+ import type { StageRecorder } from '../format-adapters/index.js';
5
+ import type { ProviderInvoker } from '../../../servertool/types.js';
6
+ type ProviderProtocol = 'openai-chat' | 'openai-responses' | 'anthropic-messages' | 'gemini-chat';
7
+ export interface ProviderResponseConversionOptions {
8
+ providerProtocol: ProviderProtocol;
9
+ providerResponse: JsonObject;
10
+ context: AdapterContext;
11
+ entryEndpoint: string;
12
+ wantsStream: boolean;
13
+ stageRecorder?: StageRecorder;
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>;
27
+ }
28
+ export interface ProviderResponseConversionResult {
29
+ body?: JsonObject;
30
+ __sse_responses?: Readable;
31
+ format?: string;
32
+ }
33
+ export declare function convertProviderResponse(options: ProviderResponseConversionOptions): Promise<ProviderResponseConversionResult>;
34
+ export {};
@@ -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,44 +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
- function resolveChatReasoningMode(entryEndpoint) {
16
- const envRaw = (process.env.ROUTECODEX_CHAT_REASONING_MODE || process.env.RCC_CHAT_REASONING_MODE || '').trim().toLowerCase();
17
- const map = {
18
- keep: 'keep',
19
- drop: 'drop',
20
- discard: 'drop',
21
- text: 'append_to_content',
22
- append: 'append_to_content',
23
- append_text: 'append_to_content',
24
- append_to_content: 'append_to_content'
25
- };
26
- if (envRaw && map[envRaw]) {
27
- return map[envRaw];
28
- }
29
- return 'keep';
30
- }
16
+ import { runServerToolOrchestration } from '../../../servertool/engine.js';
31
17
  const PROVIDER_RESPONSE_REGISTRY = {
32
18
  'openai-chat': {
33
- protocol: 'openai-chat',
34
19
  createFormatAdapter: () => new ChatFormatAdapter(),
35
20
  createMapper: () => new OpenAIChatResponseMapper()
36
21
  },
37
22
  'openai-responses': {
38
- protocol: 'openai-responses',
39
23
  createFormatAdapter: () => new ResponsesFormatAdapter(),
40
24
  createMapper: () => new ResponsesResponseMapper()
41
25
  },
42
26
  'anthropic-messages': {
43
- protocol: 'anthropic-messages',
44
27
  createFormatAdapter: () => new AnthropicFormatAdapter(),
45
28
  createMapper: () => new AnthropicResponseMapper()
46
29
  },
47
30
  'gemini-chat': {
48
- protocol: 'gemini-chat',
49
31
  createFormatAdapter: () => new GeminiFormatAdapter(),
50
32
  createMapper: () => new GeminiResponseMapper()
51
33
  }
52
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
+ }
53
46
  function resolveClientProtocol(entryEndpoint) {
54
47
  const lowered = (entryEndpoint || '').toLowerCase();
55
48
  if (lowered.includes('/v1/responses'))
@@ -85,8 +78,28 @@ function applyModelOverride(payload, model) {
85
78
  /* ignore */
86
79
  }
87
80
  }
81
+ function resolveChatReasoningMode(_entryEndpoint) {
82
+ // 当前保持默认策略:保留 reasoning_content 字段,不做额外拼接或删除。
83
+ return 'keep';
84
+ }
88
85
  export async function convertProviderResponse(options) {
89
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
+ }
90
103
  const displayModel = extractDisplayModel(options.context);
91
104
  const plan = PROVIDER_RESPONSE_REGISTRY[options.providerProtocol];
92
105
  if (!plan) {
@@ -96,7 +109,7 @@ export async function convertProviderResponse(options) {
96
109
  providerProtocol: options.providerProtocol,
97
110
  payload: options.providerResponse,
98
111
  adapterContext: options.context,
99
- wantsStream: options.wantsStream,
112
+ wantsStream,
100
113
  stageRecorder: options.stageRecorder
101
114
  });
102
115
  const formatAdapter = plan.createFormatAdapter();
@@ -137,8 +150,55 @@ export async function convertProviderResponse(options) {
137
150
  mapper,
138
151
  stageRecorder: options.stageRecorder
139
152
  });
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,继续原来的处理流程
140
200
  const governanceResult = await runRespProcessStage1ToolGovernance({
141
- payload: chatResponse,
201
+ payload: effectiveChatResponse,
142
202
  entryEndpoint: options.entryEndpoint,
143
203
  requestId: options.context.requestId,
144
204
  clientProtocol,
@@ -148,7 +208,7 @@ export async function convertProviderResponse(options) {
148
208
  payload: governanceResult.governedPayload,
149
209
  entryEndpoint: options.entryEndpoint,
150
210
  requestId: options.context.requestId,
151
- wantsStream: options.wantsStream,
211
+ wantsStream,
152
212
  reasoningMode: resolveChatReasoningMode(options.entryEndpoint),
153
213
  stageRecorder: options.stageRecorder
154
214
  });
@@ -165,7 +225,7 @@ export async function convertProviderResponse(options) {
165
225
  clientPayload,
166
226
  clientProtocol,
167
227
  requestId: options.context.requestId,
168
- wantsStream: options.wantsStream,
228
+ wantsStream,
169
229
  stageRecorder: options.stageRecorder
170
230
  });
171
231
  if (outbound.stream) {
@@ -0,0 +1,26 @@
1
+ import type { AdapterContext } from '../types/chat-envelope.js';
2
+ import type { JsonObject } from '../types/json.js';
3
+ export type ProviderInvoker = (options: {
4
+ providerKey: string;
5
+ providerType?: string;
6
+ modelId?: string;
7
+ providerProtocol: string;
8
+ payload: JsonObject;
9
+ entryEndpoint: string;
10
+ requestId: string;
11
+ }) => Promise<{
12
+ providerResponse: JsonObject;
13
+ }>;
14
+ export interface ServerSideToolEngineOptions {
15
+ chatResponse: JsonObject;
16
+ adapterContext: AdapterContext;
17
+ entryEndpoint: string;
18
+ requestId: string;
19
+ providerProtocol: string;
20
+ providerInvoker?: ProviderInvoker;
21
+ }
22
+ export interface ServerSideToolEngineResult {
23
+ mode: 'passthrough' | 'web_search_flow';
24
+ finalChatResponse: JsonObject;
25
+ }
26
+ export declare function runServerSideToolEngine(options: ServerSideToolEngineOptions): Promise<ServerSideToolEngineResult>;