@jsonstudio/llms 0.6.954 → 0.6.1172

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 (134) hide show
  1. package/dist/conversion/hub/operation-table/operation-table-runner.d.ts +18 -0
  2. package/dist/conversion/hub/operation-table/operation-table-runner.js +158 -0
  3. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.d.ts +8 -0
  4. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +303 -0
  5. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.d.ts +8 -0
  6. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +413 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.d.ts +7 -0
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +841 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.d.ts +21 -0
  10. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +535 -0
  11. package/dist/conversion/hub/ops/operations.d.ts +19 -0
  12. package/dist/conversion/hub/ops/operations.js +126 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +9 -0
  14. package/dist/conversion/hub/pipeline/hub-pipeline.js +489 -19
  15. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
  16. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
  17. package/dist/conversion/hub/policy/policy-engine.js +41 -9
  18. package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
  19. package/dist/conversion/hub/policy/protocol-spec.js +73 -23
  20. package/dist/conversion/hub/process/chat-process.js +252 -41
  21. package/dist/conversion/hub/response/provider-response.js +175 -2
  22. package/dist/conversion/hub/response/response-runtime.js +1 -1
  23. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
  24. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
  25. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
  26. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -467
  27. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
  28. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -903
  29. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
  30. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
  31. package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
  32. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
  33. package/dist/conversion/responses/responses-openai-bridge.js +14 -2
  34. package/dist/conversion/shared/bridge-message-utils.js +2 -8
  35. package/dist/conversion/shared/bridge-policies.js +5 -105
  36. package/dist/conversion/shared/gemini-tool-utils.js +89 -15
  37. package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
  38. package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
  39. package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
  40. package/dist/conversion/shared/snapshot-hooks.js +166 -3
  41. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  42. package/dist/conversion/shared/text-markup-normalizer.js +345 -9
  43. package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
  44. package/dist/conversion/shared/thought-signature-validator.js +170 -0
  45. package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
  46. package/dist/conversion/shared/tool-argument-repairer.js +56 -0
  47. package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
  48. package/dist/conversion/shared/tool-call-id-manager.js +231 -0
  49. package/dist/conversion/shared/tool-canonicalizer.js +2 -11
  50. package/dist/router/virtual-router/bootstrap.js +70 -5
  51. package/dist/router/virtual-router/context-advisor.d.ts +4 -0
  52. package/dist/router/virtual-router/context-advisor.js +3 -0
  53. package/dist/router/virtual-router/context-weighted.d.ts +31 -0
  54. package/dist/router/virtual-router/context-weighted.js +54 -0
  55. package/dist/router/virtual-router/engine-selection.js +284 -47
  56. package/dist/router/virtual-router/engine.d.ts +3 -0
  57. package/dist/router/virtual-router/engine.js +142 -33
  58. package/dist/router/virtual-router/health-weighted.d.ts +25 -0
  59. package/dist/router/virtual-router/health-weighted.js +63 -0
  60. package/dist/router/virtual-router/load-balancer.d.ts +2 -0
  61. package/dist/router/virtual-router/load-balancer.js +45 -16
  62. package/dist/router/virtual-router/routing-instructions.js +17 -1
  63. package/dist/router/virtual-router/sticky-session-store.js +136 -24
  64. package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
  65. package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
  66. package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
  67. package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
  68. package/dist/router/virtual-router/types.d.ts +98 -0
  69. package/dist/servertool/clock/config.d.ts +7 -0
  70. package/dist/servertool/clock/config.js +27 -0
  71. package/dist/servertool/clock/daemon.d.ts +3 -0
  72. package/dist/servertool/clock/daemon.js +79 -0
  73. package/dist/servertool/clock/io.d.ts +2 -0
  74. package/dist/servertool/clock/io.js +13 -0
  75. package/dist/servertool/clock/paths.d.ts +4 -0
  76. package/dist/servertool/clock/paths.js +25 -0
  77. package/dist/servertool/clock/session-store.d.ts +3 -0
  78. package/dist/servertool/clock/session-store.js +56 -0
  79. package/dist/servertool/clock/state.d.ts +5 -0
  80. package/dist/servertool/clock/state.js +62 -0
  81. package/dist/servertool/clock/task-store.d.ts +5 -0
  82. package/dist/servertool/clock/task-store.js +4 -0
  83. package/dist/servertool/clock/tasks.d.ts +17 -0
  84. package/dist/servertool/clock/tasks.js +221 -0
  85. package/dist/servertool/clock/types.d.ts +36 -0
  86. package/dist/servertool/clock/types.js +1 -0
  87. package/dist/servertool/engine.d.ts +2 -0
  88. package/dist/servertool/engine.js +161 -7
  89. package/dist/servertool/followup-shadow.d.ts +16 -0
  90. package/dist/servertool/followup-shadow.js +145 -0
  91. package/dist/servertool/handlers/apply-patch-guard.js +1 -265
  92. package/dist/servertool/handlers/clock-auto.d.ts +1 -0
  93. package/dist/servertool/handlers/clock-auto.js +160 -0
  94. package/dist/servertool/handlers/clock.d.ts +1 -0
  95. package/dist/servertool/handlers/clock.js +197 -0
  96. package/dist/servertool/handlers/exec-command-guard.js +7 -555
  97. package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
  98. package/dist/servertool/handlers/followup-request-builder.js +248 -28
  99. package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
  100. package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
  101. package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
  102. package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
  103. package/dist/servertool/handlers/stop-message-auto.js +47 -175
  104. package/dist/servertool/handlers/vision.d.ts +7 -1
  105. package/dist/servertool/handlers/vision.js +61 -117
  106. package/dist/servertool/handlers/web-search.d.ts +7 -1
  107. package/dist/servertool/handlers/web-search.js +122 -105
  108. package/dist/servertool/reenter-backend.d.ts +23 -0
  109. package/dist/servertool/reenter-backend.js +18 -0
  110. package/dist/servertool/server-side-tools.d.ts +3 -2
  111. package/dist/servertool/server-side-tools.js +64 -10
  112. package/dist/servertool/types.d.ts +92 -3
  113. package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
  114. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
  115. package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
  116. package/dist/sse/shared/writer.js +24 -7
  117. package/dist/tools/apply-patch/execution-capturer.js +3 -1
  118. package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
  119. package/dist/tools/apply-patch/json/parse-loose.js +139 -0
  120. package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
  121. package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
  122. package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
  123. package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
  124. package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
  125. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
  126. package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
  127. package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
  128. package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
  129. package/dist/tools/apply-patch/structured/coercion.js +82 -0
  130. package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
  131. package/dist/tools/apply-patch/validation/shared.js +6 -0
  132. package/dist/tools/apply-patch/validator.d.ts +2 -2
  133. package/dist/tools/apply-patch/validator.js +6 -556
  134. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
2
  import { cloneJson, extractTextFromChatLike } from '../server-side-tools.js';
3
- import { buildEntryAwareFollowupPayload, dropToolByFunctionName, extractCapturedChatSeed } from './followup-request-builder.js';
3
+ import { extractCapturedChatSeed } from './followup-request-builder.js';
4
+ import { reenterServerToolBackend } from '../reenter-backend.js';
4
5
  const FLOW_ID = 'vision_flow';
5
6
  const handler = async (ctx) => {
6
- if (!ctx.options.reenterPipeline) {
7
+ if (!ctx.capabilities.reenterPipeline) {
7
8
  return null;
8
9
  }
9
10
  if (!shouldRunVisionFlow(ctx)) {
@@ -17,43 +18,70 @@ const handler = async (ctx) => {
17
18
  if (!analysisPayload) {
18
19
  return null;
19
20
  }
20
- const visionResponse = await ctx.options.reenterPipeline({
21
+ const backend = {
22
+ kind: 'vision_analysis',
23
+ requestIdSuffix: ':vision',
21
24
  entryEndpoint: '/v1/chat/completions',
22
- requestId: `${ctx.options.requestId}:vision`,
23
- body: analysisPayload,
24
- metadata: {
25
- routeHint: 'vision',
26
- serverToolFollowup: true,
27
- stream: false
28
- }
29
- });
30
- const visionBody = visionResponse.body && typeof visionResponse.body === 'object'
31
- ? visionResponse.body
32
- : null;
33
- if (!visionBody) {
34
- return null;
35
- }
36
- const visionSummary = extractTextFromChatLike(visionBody);
37
- if (!visionSummary) {
38
- return null;
39
- }
40
- const followupPayload = buildVisionFollowupPayload(captured, visionSummary, ctx.options.entryEndpoint || ctx.adapterContext?.entryEndpoint || '/v1/chat/completions');
41
- if (!followupPayload) {
42
- return null;
43
- }
44
- const execution = {
45
- flowId: FLOW_ID,
46
- followup: {
47
- requestIdSuffix: ':vision_followup',
48
- payload: followupPayload
49
- }
25
+ payload: analysisPayload
50
26
  };
51
27
  return {
52
- chatResponse: ctx.base,
53
- execution
28
+ flowId: FLOW_ID,
29
+ backend,
30
+ finalize: async ({ backendResult }) => {
31
+ if (!backendResult || backendResult.kind !== 'vision_analysis') {
32
+ return null;
33
+ }
34
+ const body = backendResult.response.body && typeof backendResult.response.body === 'object'
35
+ ? backendResult.response.body
36
+ : null;
37
+ if (!body) {
38
+ return null;
39
+ }
40
+ const visionSummary = extractTextFromChatLike(body);
41
+ if (!visionSummary) {
42
+ return null;
43
+ }
44
+ // Fail-closed: if we cannot build followup seed, do not intercept.
45
+ const seed = extractCapturedChatSeed(captured);
46
+ if (!seed) {
47
+ return null;
48
+ }
49
+ return {
50
+ chatResponse: ctx.base,
51
+ execution: {
52
+ flowId: FLOW_ID,
53
+ followup: {
54
+ requestIdSuffix: ':vision_followup',
55
+ entryEndpoint: ctx.entryEndpoint,
56
+ injection: {
57
+ ops: [
58
+ { op: 'inject_vision_summary', summary: visionSummary },
59
+ { op: 'drop_tool_by_name', name: 'vision' }
60
+ ]
61
+ }
62
+ }
63
+ }
64
+ };
65
+ }
54
66
  };
55
67
  };
56
68
  registerServerToolHandler('vision_auto', handler, { trigger: 'auto' });
69
+ export async function executeVisionBackendPlan(args) {
70
+ const plan = args.plan;
71
+ const options = args.options;
72
+ if (!options.reenterPipeline) {
73
+ return { kind: 'vision_analysis', response: {} };
74
+ }
75
+ const response = await reenterServerToolBackend({
76
+ reenterPipeline: options.reenterPipeline,
77
+ entryEndpoint: plan.entryEndpoint,
78
+ requestId: `${options.requestId}${plan.requestIdSuffix}`,
79
+ body: plan.payload,
80
+ providerProtocol: 'openai-chat',
81
+ routeHint: 'vision'
82
+ });
83
+ return { kind: 'vision_analysis', response };
84
+ }
57
85
  function shouldRunVisionFlow(ctx) {
58
86
  const record = ctx.adapterContext;
59
87
  const followupFlag = record.serverToolFollowup === true || record.serverToolFollowup === 'true';
@@ -194,87 +222,3 @@ function buildVisionUserMessage(source) {
194
222
  }
195
223
  return message;
196
224
  }
197
- function buildVisionFollowupPayload(source, summary, entryEndpoint) {
198
- const seed = extractCapturedChatSeed(source);
199
- if (!seed) {
200
- return null;
201
- }
202
- const messages = injectVisionSummary(seed.messages, summary);
203
- const filteredTools = dropToolByFunctionName(seed.tools, 'vision');
204
- return buildEntryAwareFollowupPayload({
205
- entryEndpoint,
206
- model: seed.model,
207
- messages,
208
- ...(filteredTools ? { tools: filteredTools } : {}),
209
- ...(seed.parameters ? { parameters: seed.parameters } : {})
210
- });
211
- }
212
- function injectVisionSummary(source, summary) {
213
- const messages = Array.isArray(source) ? cloneJson(source) : [];
214
- let injected = false;
215
- for (const message of messages) {
216
- if (!message || typeof message !== 'object')
217
- continue;
218
- const content = message.content;
219
- if (!Array.isArray(content))
220
- continue;
221
- const nextParts = [];
222
- let removed = false;
223
- for (const part of content) {
224
- if (part && typeof part === 'object') {
225
- const typeValue = typeof part.type === 'string'
226
- ? part.type.toLowerCase()
227
- : '';
228
- if (typeValue.includes('image')) {
229
- removed = true;
230
- continue;
231
- }
232
- }
233
- nextParts.push(part);
234
- }
235
- if (removed) {
236
- nextParts.push({
237
- type: 'text',
238
- text: `[Vision] ${summary}`
239
- });
240
- message.content = nextParts;
241
- injected = true;
242
- }
243
- }
244
- if (!injected) {
245
- for (let i = messages.length - 1; i >= 0; i -= 1) {
246
- const msg = messages[i];
247
- if (!msg || typeof msg !== 'object')
248
- continue;
249
- const role = typeof msg.role === 'string'
250
- ? msg.role.toLowerCase()
251
- : '';
252
- if (role !== 'user')
253
- continue;
254
- const content = msg.content;
255
- if (Array.isArray(content)) {
256
- content.push({
257
- type: 'text',
258
- text: `[Vision] ${summary}`
259
- });
260
- injected = true;
261
- break;
262
- }
263
- if (typeof content === 'string' && content.length) {
264
- msg.content = `${content}\n[Vision] ${summary}`;
265
- }
266
- else {
267
- msg.content = `[Vision] ${summary}`;
268
- }
269
- injected = true;
270
- break;
271
- }
272
- }
273
- if (!injected) {
274
- messages.push({
275
- role: 'system',
276
- content: `[Vision] ${summary}`
277
- });
278
- }
279
- return messages;
280
- }
@@ -1 +1,7 @@
1
- export {};
1
+ import type { ServerSideToolEngineOptions, ServerToolBackendPlan, ServerToolBackendResult } from '../types.js';
2
+ export declare function executeWebSearchBackendPlan(args: {
3
+ plan: Extract<ServerToolBackendPlan, {
4
+ kind: 'web_search';
5
+ }>;
6
+ options: ServerSideToolEngineOptions;
7
+ }): Promise<ServerToolBackendResult>;
@@ -1,14 +1,15 @@
1
1
  import { buildOpenAIChatFromGeminiResponse } from '../../conversion/codecs/gemini-openai-codec.js';
2
2
  import { registerServerToolHandler } from '../registry.js';
3
3
  import { cloneJson, extractTextFromChatLike } from '../server-side-tools.js';
4
- import { buildEntryAwareFollowupPayload, dropToolByFunctionName, extractCapturedChatSeed } from './followup-request-builder.js';
4
+ import { extractCapturedChatSeed } from './followup-request-builder.js';
5
+ import { reenterServerToolBackend } from '../reenter-backend.js';
5
6
  const FLOW_ID = 'web_search_flow';
6
7
  const handler = async (ctx) => {
7
8
  const toolCall = ctx.toolCall;
8
9
  if (!toolCall) {
9
10
  return null;
10
11
  }
11
- if (!ctx.options.providerInvoker && !ctx.options.reenterPipeline) {
12
+ if (!ctx.capabilities.providerInvoker && !ctx.capabilities.reenterPipeline) {
12
13
  return null;
13
14
  }
14
15
  const webSearchConfig = getWebSearchConfig(ctx.adapterContext);
@@ -28,57 +29,61 @@ const handler = async (ctx) => {
28
29
  return null;
29
30
  }
30
31
  const resultCount = normalizeResultCount(parsedArgs.count);
31
- let chosenEngine;
32
- let chosenResult;
33
- let lastFailure;
34
- for (const engine of engines) {
35
- const backendResult = await executeWebSearchBackend({
36
- ctx,
37
- engine,
38
- query,
39
- recency: parsedArgs.recency,
40
- resultCount
41
- });
42
- if (backendResult.ok) {
43
- chosenEngine = engine;
44
- chosenResult = backendResult;
45
- break;
46
- }
47
- lastFailure = { engine, result: backendResult };
48
- }
49
- if (!chosenEngine || !chosenResult) {
50
- if (!lastFailure) {
51
- return null;
52
- }
53
- chosenEngine = lastFailure.engine;
54
- chosenResult = lastFailure.result;
55
- }
56
- const patched = injectWebSearchToolResult(ctx.base, toolCall, chosenEngine, query, chosenResult);
57
- const followupPayload = buildWebSearchFollowupPayload(ctx.adapterContext, patched, ctx.options.entryEndpoint || ctx.adapterContext?.entryEndpoint || '/v1/chat/completions');
58
- const execution = {
32
+ const recency = typeof parsedArgs.recency === 'string' && parsedArgs.recency.trim() ? parsedArgs.recency.trim() : undefined;
33
+ const backend = {
34
+ kind: 'web_search',
35
+ requestIdSuffix: ':web_search',
36
+ query,
37
+ ...(recency ? { recency } : {}),
38
+ resultCount,
39
+ engines
40
+ };
41
+ return {
59
42
  flowId: FLOW_ID,
60
- followup: followupPayload
61
- ? {
62
- requestIdSuffix: ':web_search_followup',
63
- payload: followupPayload,
64
- metadata: {
65
- // keep minimal; servertool engine will inject the standard followup metadata defaults
66
- serverToolFollowup: true
67
- }
43
+ backend,
44
+ finalize: async ({ backendResult }) => {
45
+ if (!backendResult || backendResult.kind !== 'web_search') {
46
+ return null;
68
47
  }
69
- : undefined,
70
- context: {
71
- web_search: {
72
- engineId: chosenEngine.id,
73
- providerKey: chosenEngine.providerKey,
74
- summary: chosenResult.summary
48
+ const chosen = backendResult.chosenEngine;
49
+ if (!chosen || !chosen.id || !chosen.providerKey) {
50
+ return null;
75
51
  }
52
+ const patched = injectWebSearchToolResult(ctx.base, toolCall, { id: chosen.id, providerKey: chosen.providerKey }, query, backendResult.result);
53
+ const seed = extractCapturedChatSeed(ctx.adapterContext?.capturedChatRequest);
54
+ const assistantMessage = seed ? extractAssistantMessage(patched) : null;
55
+ const toolMessages = seed ? buildToolMessages(patched) : [];
56
+ const canFollowup = Boolean(seed && assistantMessage && toolMessages.length > 0);
57
+ return {
58
+ chatResponse: patched,
59
+ execution: {
60
+ flowId: FLOW_ID,
61
+ ...(canFollowup
62
+ ? {
63
+ followup: {
64
+ requestIdSuffix: ':web_search_followup',
65
+ entryEndpoint: ctx.entryEndpoint,
66
+ injection: {
67
+ ops: [
68
+ { op: 'append_assistant_message' },
69
+ { op: 'append_tool_messages_from_tool_outputs' },
70
+ { op: 'drop_tool_by_name', name: 'web_search' }
71
+ ]
72
+ }
73
+ }
74
+ }
75
+ : {}),
76
+ context: {
77
+ web_search: {
78
+ engineId: chosen.id,
79
+ providerKey: chosen.providerKey,
80
+ summary: backendResult.result.summary
81
+ }
82
+ }
83
+ }
84
+ };
76
85
  }
77
86
  };
78
- return {
79
- chatResponse: patched,
80
- execution
81
- };
82
87
  };
83
88
  registerServerToolHandler('web_search', handler);
84
89
  function parseToolArguments(toolCall) {
@@ -220,19 +225,19 @@ function normalizeResultCount(value) {
220
225
  return 10;
221
226
  }
222
227
  async function executeWebSearchBackend(args) {
223
- const { ctx, engine, query } = args;
228
+ const { options, engine, query } = args;
224
229
  const recency = typeof args.recency === 'string' && args.recency.trim() ? args.recency.trim() : undefined;
225
230
  let summary = '';
226
231
  let hits = [];
227
232
  let ok = true;
228
233
  try {
229
- logServerToolWebSearch(engine, ctx.options.requestId, query);
230
- const requestSuffix = `:web_search:${engine.id}`;
234
+ logServerToolWebSearch(engine, options.requestId, query);
235
+ const requestSuffix = args.requestSuffix;
231
236
  // 对于 iFlow,直接通过 providerInvoker 调用 /chat/retrieve,
232
237
  // 即使 reenterPipeline 可用,也不走 Chat 模型 + tools。
233
- if (isIflowWebSearchEngine(engine) && ctx.options.providerInvoker) {
238
+ if (isIflowWebSearchEngine(engine) && options.providerInvoker) {
234
239
  const backendResult = await executeIflowWebSearchViaProvider({
235
- ctx,
240
+ options,
236
241
  engine,
237
242
  query,
238
243
  recency,
@@ -243,17 +248,15 @@ async function executeWebSearchBackend(args) {
243
248
  hits = backendResult.hits;
244
249
  ok = backendResult.ok;
245
250
  }
246
- else if (ctx.options.reenterPipeline) {
251
+ else if (options.reenterPipeline) {
247
252
  const payload = buildWebSearchReenterPayload(engine, query, recency, args.resultCount);
248
- const followup = await ctx.options.reenterPipeline({
253
+ const followup = await reenterServerToolBackend({
254
+ reenterPipeline: options.reenterPipeline,
249
255
  entryEndpoint: '/v1/chat/completions',
250
- requestId: `${ctx.options.requestId}${requestSuffix}`,
256
+ requestId: `${options.requestId}${requestSuffix}`,
251
257
  body: payload,
252
- metadata: {
253
- routeHint: 'web_search',
254
- serverToolFollowup: true,
255
- stream: false
256
- }
258
+ providerProtocol: 'openai-chat',
259
+ routeHint: 'web_search'
257
260
  });
258
261
  const body = followup.body && typeof followup.body === 'object' ? followup.body : null;
259
262
  if (body) {
@@ -263,7 +266,7 @@ async function executeWebSearchBackend(args) {
263
266
  try {
264
267
  // eslint-disable-next-line no-console
265
268
  console.log('\x1b[38;5;27m[server-tool][web_search][backend_debug]' +
266
- ` requestId=${ctx.options.requestId}${requestSuffix} payload=${JSON.stringify(body).slice(0, 2000)}\x1b[0m`);
269
+ ` requestId=${options.requestId}${requestSuffix} payload=${JSON.stringify(body).slice(0, 2000)}\x1b[0m`);
267
270
  }
268
271
  catch {
269
272
  /* logging best-effort */
@@ -271,9 +274,9 @@ async function executeWebSearchBackend(args) {
271
274
  }
272
275
  }
273
276
  }
274
- else if (ctx.options.providerInvoker) {
277
+ else if (options.providerInvoker) {
275
278
  summary = await executeWebSearchViaProvider({
276
- ctx,
279
+ options,
277
280
  engine,
278
281
  query,
279
282
  recency,
@@ -305,7 +308,7 @@ async function executeWebSearchBackend(args) {
305
308
  try {
306
309
  const preview = summary.length > 120 ? `${summary.slice(0, 117)}...` : summary;
307
310
  // eslint-disable-next-line no-console
308
- console.log(`\x1b[38;5;27m[server-tool][web_search][result] requestId=${ctx.options.requestId} ` +
311
+ console.log(`\x1b[38;5;27m[server-tool][web_search][result] requestId=${options.requestId} ` +
309
312
  `engine=${engine.id} chars=${summary.length} preview=${JSON.stringify(preview)}\x1b[0m`);
310
313
  }
311
314
  catch {
@@ -320,6 +323,46 @@ async function executeWebSearchBackend(args) {
320
323
  }
321
324
  return { summary, hits: finalHits, ok };
322
325
  }
326
+ export async function executeWebSearchBackendPlan(args) {
327
+ const plan = args.plan;
328
+ const options = args.options;
329
+ const engines = Array.isArray(plan.engines) ? plan.engines : [];
330
+ if (!engines.length) {
331
+ return { kind: 'web_search', result: { ok: false, summary: '', hits: [] } };
332
+ }
333
+ let chosenEngine;
334
+ let chosenResult;
335
+ let lastFailure;
336
+ for (const engine of engines) {
337
+ const requestSuffix = `${plan.requestIdSuffix}:${engine.id}`;
338
+ const backendResult = await executeWebSearchBackend({
339
+ options,
340
+ engine,
341
+ query: plan.query,
342
+ recency: plan.recency,
343
+ resultCount: plan.resultCount,
344
+ requestSuffix
345
+ });
346
+ if (backendResult.ok) {
347
+ chosenEngine = engine;
348
+ chosenResult = backendResult;
349
+ break;
350
+ }
351
+ lastFailure = { engine, result: backendResult };
352
+ }
353
+ if (!chosenEngine || !chosenResult) {
354
+ if (!lastFailure) {
355
+ return { kind: 'web_search', result: { ok: false, summary: '', hits: [] } };
356
+ }
357
+ chosenEngine = lastFailure.engine;
358
+ chosenResult = lastFailure.result;
359
+ }
360
+ return {
361
+ kind: 'web_search',
362
+ chosenEngine: { id: chosenEngine.id, providerKey: chosenEngine.providerKey },
363
+ result: { ok: chosenResult.ok, summary: chosenResult.summary, hits: chosenResult.hits }
364
+ };
365
+ }
323
366
  function buildWebSearchReenterPayload(engine, query, recency, resultCount) {
324
367
  const systemPrompt = buildWebSearchSystemPrompt(resultCount);
325
368
  const basePayload = {
@@ -350,8 +393,8 @@ function buildWebSearchReenterPayload(engine, query, recency, resultCount) {
350
393
  };
351
394
  }
352
395
  async function executeWebSearchViaProvider(args) {
353
- const { ctx, engine, query, recency, count, requestSuffix } = args;
354
- if (!ctx.options.providerInvoker) {
396
+ const { options, engine, query, recency, count, requestSuffix } = args;
397
+ if (!options.providerInvoker) {
355
398
  return '';
356
399
  }
357
400
  if (isGeminiWebSearchEngine(engine)) {
@@ -373,14 +416,14 @@ async function executeWebSearchViaProvider(args) {
373
416
  }
374
417
  ]
375
418
  };
376
- const backend = await ctx.options.providerInvoker({
419
+ const backend = await options.providerInvoker({
377
420
  providerKey: engine.providerKey,
378
421
  providerType: undefined,
379
422
  modelId: engine.id,
380
423
  providerProtocol: 'gemini-chat',
381
424
  payload: geminiPayload,
382
425
  entryEndpoint: '/v1/models/gemini:generateContent',
383
- requestId: `${ctx.options.requestId}${requestSuffix}`,
426
+ requestId: `${options.requestId}${requestSuffix}`,
384
427
  routeHint: 'web_search'
385
428
  });
386
429
  const providerResponse = backend.providerResponse && typeof backend.providerResponse === 'object'
@@ -412,14 +455,14 @@ async function executeWebSearchViaProvider(args) {
412
455
  engine: engine.id
413
456
  }
414
457
  };
415
- const backend = await ctx.options.providerInvoker({
458
+ const backend = await options.providerInvoker({
416
459
  providerKey: engine.providerKey,
417
460
  providerType: undefined,
418
461
  modelId: undefined,
419
- providerProtocol: ctx.options.providerProtocol,
462
+ providerProtocol: options.providerProtocol,
420
463
  payload: backendPayload,
421
464
  entryEndpoint: '/v1/chat/completions',
422
- requestId: `${ctx.options.requestId}${requestSuffix}`,
465
+ requestId: `${options.requestId}${requestSuffix}`,
423
466
  routeHint: 'web_search'
424
467
  });
425
468
  const providerResponse = backend.providerResponse && typeof backend.providerResponse === 'object'
@@ -431,8 +474,8 @@ async function executeWebSearchViaProvider(args) {
431
474
  return extractTextFromChatLike(providerResponse);
432
475
  }
433
476
  async function executeIflowWebSearchViaProvider(args) {
434
- const { ctx, engine, query, count, requestSuffix } = args;
435
- if (!ctx.options.providerInvoker) {
477
+ const { options, engine, query, count, requestSuffix } = args;
478
+ if (!options.providerInvoker) {
436
479
  return {
437
480
  summary: '',
438
481
  hits: [],
@@ -457,8 +500,8 @@ async function executeIflowWebSearchViaProvider(args) {
457
500
  };
458
501
  let providerKey = engine.providerKey;
459
502
  try {
460
- const adapter = ctx.adapterContext && typeof ctx.adapterContext === 'object'
461
- ? ctx.adapterContext
503
+ const adapter = options.adapterContext && typeof options.adapterContext === 'object'
504
+ ? options.adapterContext
462
505
  : null;
463
506
  const target = adapter && adapter.target && typeof adapter.target === 'object'
464
507
  ? adapter.target
@@ -481,14 +524,14 @@ async function executeIflowWebSearchViaProvider(args) {
481
524
  routeName: 'web_search'
482
525
  }
483
526
  };
484
- const backend = await ctx.options.providerInvoker({
527
+ const backend = await options.providerInvoker({
485
528
  providerKey,
486
529
  providerType: undefined,
487
530
  modelId: undefined,
488
- providerProtocol: ctx.options.providerProtocol,
531
+ providerProtocol: options.providerProtocol,
489
532
  payload,
490
533
  entryEndpoint: '/v1/chat/retrieve',
491
- requestId: `${ctx.options.requestId}${requestSuffix}`,
534
+ requestId: `${options.requestId}${requestSuffix}`,
492
535
  routeHint: 'web_search'
493
536
  });
494
537
  const providerResponse = backend.providerResponse && typeof backend.providerResponse === 'object'
@@ -569,32 +612,6 @@ function injectWebSearchToolResult(base, toolCall, engine, query, backendResult)
569
612
  ];
570
613
  return cloned;
571
614
  }
572
- function buildWebSearchFollowupPayload(adapterContext, chatResponse, entryEndpoint) {
573
- const captured = adapterContext && typeof adapterContext === 'object'
574
- ? adapterContext.capturedChatRequest
575
- : undefined;
576
- const seed = extractCapturedChatSeed(captured);
577
- if (!seed) {
578
- return null;
579
- }
580
- const assistantMessage = extractAssistantMessage(chatResponse);
581
- if (!assistantMessage) {
582
- return null;
583
- }
584
- const toolMessages = buildToolMessages(chatResponse);
585
- if (!toolMessages.length) {
586
- return null;
587
- }
588
- const reconstructed = [...seed.messages, assistantMessage, ...toolMessages];
589
- const filteredTools = dropToolByFunctionName(seed.tools, 'web_search');
590
- return buildEntryAwareFollowupPayload({
591
- entryEndpoint,
592
- model: seed.model,
593
- messages: reconstructed,
594
- ...(filteredTools ? { tools: filteredTools } : {}),
595
- ...(seed.parameters ? { parameters: seed.parameters } : {})
596
- });
597
- }
598
615
  function extractAssistantMessage(chatResponse) {
599
616
  const choices = Array.isArray(chatResponse.choices)
600
617
  ? chatResponse.choices
@@ -0,0 +1,23 @@
1
+ import type { JsonObject } from '../conversion/hub/types/json.js';
2
+ export declare function reenterServerToolBackend(args: {
3
+ reenterPipeline: (options: {
4
+ entryEndpoint: string;
5
+ requestId: string;
6
+ body: JsonObject;
7
+ metadata?: JsonObject;
8
+ }) => Promise<{
9
+ body?: JsonObject;
10
+ __sse_responses?: unknown;
11
+ format?: string;
12
+ }>;
13
+ entryEndpoint: string;
14
+ requestId: string;
15
+ body: JsonObject;
16
+ providerProtocol: string;
17
+ routeHint?: string;
18
+ metadata?: JsonObject;
19
+ }): Promise<{
20
+ body?: JsonObject;
21
+ __sse_responses?: unknown;
22
+ format?: string;
23
+ }>;
@@ -0,0 +1,18 @@
1
+ export async function reenterServerToolBackend(args) {
2
+ const routeHint = typeof args.routeHint === 'string' && args.routeHint.trim().length ? args.routeHint.trim() : undefined;
3
+ const merged = {
4
+ providerProtocol: args.providerProtocol,
5
+ serverToolFollowup: true,
6
+ stream: false,
7
+ preserveRouteHint: false,
8
+ disableStickyRoutes: true,
9
+ ...(routeHint ? { routeHint } : {}),
10
+ ...(args.metadata ?? {})
11
+ };
12
+ return await args.reenterPipeline({
13
+ entryEndpoint: args.entryEndpoint,
14
+ requestId: args.requestId,
15
+ body: args.body,
16
+ metadata: merged
17
+ });
18
+ }
@@ -1,12 +1,13 @@
1
1
  import type { JsonObject } from '../conversion/hub/types/json.js';
2
2
  import type { ServerSideToolEngineOptions, ServerSideToolEngineResult, ToolCall } from './types.js';
3
- import './handlers/web-search.js';
4
- import './handlers/vision.js';
5
3
  import './handlers/iflow-model-error-retry.js';
6
4
  import './handlers/gemini-empty-reply-continue.js';
7
5
  import './handlers/stop-message-auto.js';
6
+ import './handlers/clock.js';
7
+ import './handlers/clock-auto.js';
8
8
  import './handlers/exec-command-guard.js';
9
9
  import './handlers/apply-patch-guard.js';
10
+ import './handlers/recursive-detection-guard.js';
10
11
  export declare function runServerSideToolEngine(options: ServerSideToolEngineOptions): Promise<ServerSideToolEngineResult>;
11
12
  export declare function extractToolCalls(chatResponse: JsonObject): ToolCall[];
12
13
  export declare function cloneJson<T>(value: T): T;