@jsonstudio/llms 0.6.1172 → 0.6.1354

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 (160) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.d.ts +3 -1
  2. package/dist/conversion/codecs/gemini-openai-codec.js +10 -4
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -1
  4. package/dist/conversion/compat/actions/gemini-web-search.js +5 -2
  5. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +12 -0
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +199 -0
  7. package/dist/conversion/compat/actions/iflow-web-search.d.ts +1 -1
  8. package/dist/conversion/compat/actions/iflow-web-search.js +5 -2
  9. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
  10. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
  11. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +523 -50
  12. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
  13. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  14. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
  16. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +134 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
  18. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
  19. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
  20. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +384 -0
  25. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
  26. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
  27. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
  28. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
  30. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
  31. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
  32. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
  33. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
  34. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
  35. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
  36. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
  37. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
  38. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
  39. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
  40. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
  41. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
  42. package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
  43. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
  44. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
  45. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
  46. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
  47. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
  48. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
  49. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
  50. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
  51. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
  52. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
  53. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
  54. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
  55. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
  56. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
  57. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
  58. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
  59. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
  60. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
  61. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
  62. package/dist/conversion/hub/pipeline/target-utils.js +9 -5
  63. package/dist/conversion/hub/process/chat-process.js +256 -16
  64. package/dist/conversion/hub/response/provider-response.d.ts +8 -0
  65. package/dist/conversion/hub/response/provider-response.js +85 -27
  66. package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
  67. package/dist/conversion/hub/response/response-mappers.js +30 -6
  68. package/dist/conversion/hub/response/response-runtime.js +4 -38
  69. package/dist/conversion/hub/snapshot-recorder.js +5 -1
  70. package/dist/conversion/hub/standardized-bridge.js +23 -15
  71. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
  72. package/dist/conversion/responses/responses-openai-bridge.js +20 -4
  73. package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
  74. package/dist/conversion/shared/gemini-tool-utils.js +580 -108
  75. package/dist/conversion/shared/jsonish.js +1 -1
  76. package/dist/conversion/shared/mcp-injection.js +67 -33
  77. package/dist/conversion/shared/openai-finalizer.js +2 -1
  78. package/dist/conversion/shared/openai-message-normalize.js +76 -21
  79. package/dist/conversion/shared/responses-output-builder.js +6 -0
  80. package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
  81. package/dist/conversion/shared/runtime-metadata.js +23 -0
  82. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  83. package/dist/conversion/shared/text-markup-normalizer.js +284 -4
  84. package/dist/conversion/shared/tool-canonicalizer.js +2 -1
  85. package/dist/conversion/shared/tool-governor.js +3 -3
  86. package/dist/filters/engine.js +5 -5
  87. package/dist/filters/special/request-tool-list-filter.js +194 -60
  88. package/dist/filters/special/request-tools-normalize.js +1 -1
  89. package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
  90. package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
  91. package/dist/filters/special/tool-filter-hooks.js +58 -62
  92. package/dist/guidance/index.js +5 -1
  93. package/dist/http/sse-response.js +6 -6
  94. package/dist/router/virtual-router/bootstrap.js +48 -4
  95. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  96. package/dist/router/virtual-router/engine-health.js +11 -110
  97. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
  98. package/dist/router/virtual-router/engine-selection/alias-selection.js +156 -0
  99. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
  100. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
  101. package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
  102. package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
  103. package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
  104. package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
  105. package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
  106. package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
  107. package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
  108. package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
  109. package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
  110. package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
  111. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
  112. package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
  113. package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
  114. package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
  115. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
  116. package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
  117. package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
  118. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +400 -0
  119. package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
  120. package/dist/router/virtual-router/engine-selection/tier-selection.js +225 -0
  121. package/dist/router/virtual-router/engine-selection.d.ts +4 -30
  122. package/dist/router/virtual-router/engine-selection.js +10 -962
  123. package/dist/router/virtual-router/engine.d.ts +1 -0
  124. package/dist/router/virtual-router/engine.js +55 -10
  125. package/dist/router/virtual-router/routing-instructions.js +6 -1
  126. package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
  127. package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
  128. package/dist/router/virtual-router/types.d.ts +25 -1
  129. package/dist/servertool/clock/config.d.ts +8 -0
  130. package/dist/servertool/clock/config.js +22 -0
  131. package/dist/servertool/clock/log.d.ts +3 -0
  132. package/dist/servertool/clock/log.js +13 -0
  133. package/dist/servertool/clock/task-store.d.ts +1 -1
  134. package/dist/servertool/clock/task-store.js +1 -1
  135. package/dist/servertool/clock/tasks.js +1 -1
  136. package/dist/servertool/engine.js +146 -21
  137. package/dist/servertool/handlers/clock-auto.js +11 -6
  138. package/dist/servertool/handlers/clock.js +36 -10
  139. package/dist/servertool/handlers/followup-request-builder.js +8 -2
  140. package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
  141. package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
  142. package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
  143. package/dist/servertool/handlers/stop-message-auto.js +100 -10
  144. package/dist/servertool/handlers/vision.js +4 -1
  145. package/dist/servertool/handlers/web-search.js +3 -1
  146. package/dist/servertool/pending-session.d.ts +19 -0
  147. package/dist/servertool/pending-session.js +97 -0
  148. package/dist/servertool/reenter-backend.js +5 -3
  149. package/dist/servertool/server-side-tools.js +235 -6
  150. package/dist/servertool/types.d.ts +13 -0
  151. package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
  152. package/dist/sse/shared/chat-serializer.js +2 -2
  153. package/dist/sse/shared/constants.js +1 -1
  154. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
  155. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  156. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  157. package/dist/tools/apply-patch/execution-capturer.js +1 -1
  158. package/dist/tools/exec-command/normalize.js +4 -0
  159. package/dist/tools/exec-command/regression-capturer.js +1 -1
  160. package/package.json +10 -5
@@ -1,8 +1,11 @@
1
1
  import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js';
2
2
  import { ToolGovernanceEngine } from '../tool-governance/index.js';
3
+ import { readRuntimeMetadata } from '../../shared/runtime-metadata.js';
3
4
  import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
4
5
  import { normalizeApplyPatchToolCallsOnRequest } from '../../shared/tool-governor.js';
5
- import { clearClockSession, normalizeClockConfig, reserveDueTasksForRequest, startClockDaemonIfNeeded } from '../../../servertool/clock/task-store.js';
6
+ import { clearClockSession, resolveClockConfig, reserveDueTasksForRequest, startClockDaemonIfNeeded } from '../../../servertool/clock/task-store.js';
7
+ import { logClock } from '../../../servertool/clock/log.js';
8
+ import { clearPendingServerToolInjection, loadPendingServerToolInjection } from '../../../servertool/pending-session.js';
6
9
  import { isJsonObject } from '../types/json.js';
7
10
  import { applyHubOperations } from '../ops/operations.js';
8
11
  const toolGovernanceEngine = new ToolGovernanceEngine();
@@ -83,6 +86,9 @@ async function applyRequestToolGovernance(request, context) {
83
86
  messages: stripHistoricalImageAttachments(merged.messages)
84
87
  };
85
88
  merged = normalizeApplyPatchToolCallsOnRequest(merged);
89
+ // Mixed-tools support: if previous response executed servertools alongside client tools,
90
+ // inject servertool tool_call + tool_result messages after the client's tool results on next request.
91
+ merged = await maybeInjectPendingServerToolResultsAfterClientTools(merged, metadata);
86
92
  if (containsImageAttachment(merged.messages)) {
87
93
  if (!merged.metadata) {
88
94
  merged.metadata = {
@@ -106,7 +112,7 @@ async function applyRequestToolGovernance(request, context) {
106
112
  // Server-side web_search tool injection (config-driven, best-effort).
107
113
  merged = applyHubOperations(merged, buildWebSearchOperations(merged, metadata));
108
114
  // Server-side clock tool + scheduled reminders injection (config-driven, best-effort).
109
- merged = applyHubOperations(merged, buildClockOperations(metadata));
115
+ merged = applyHubOperations(merged, buildClockOperations(merged, metadata));
110
116
  merged = await maybeInjectClockRemindersAndApplyDirectives(merged, metadata, context.requestId);
111
117
  const { request: sanitized, summary } = toolGovernanceEngine.governRequest(merged, providerProtocol);
112
118
  if (summary.applied) {
@@ -118,7 +124,191 @@ async function applyRequestToolGovernance(request, context) {
118
124
  }
119
125
  };
120
126
  }
121
- return sanitized;
127
+ return pruneUndeclaredToolHistory(sanitized);
128
+ }
129
+ function resolveSessionIdForPending(metadata, request) {
130
+ const candidate = readString(metadata.sessionId) ?? readString(request.metadata?.sessionId);
131
+ return candidate && candidate.trim() ? candidate.trim() : null;
132
+ }
133
+ function pruneUndeclaredToolHistory(request) {
134
+ const tools = Array.isArray(request.tools) ? request.tools : [];
135
+ const allowedToolNames = new Set();
136
+ for (const tool of tools) {
137
+ const name = tool && typeof tool === 'object' && tool.function && typeof tool.function.name === 'string'
138
+ ? String(tool.function.name).trim()
139
+ : '';
140
+ if (name) {
141
+ allowedToolNames.add(name);
142
+ }
143
+ }
144
+ // If no tools are declared, do not carry any tool calls/results in history.
145
+ const hasAnyTools = allowedToolNames.size > 0;
146
+ let prunedToolCalls = 0;
147
+ let prunedToolResults = 0;
148
+ const prunedToolNames = new Set();
149
+ const keptToolCallIds = new Set();
150
+ const nextMessages = [];
151
+ for (const msg of request.messages) {
152
+ if (!msg || typeof msg !== 'object')
153
+ continue;
154
+ if (msg.role === 'assistant' && Array.isArray(msg.tool_calls) && msg.tool_calls.length) {
155
+ if (!hasAnyTools) {
156
+ prunedToolCalls += msg.tool_calls.length;
157
+ for (const tc of msg.tool_calls) {
158
+ const n = tc && tc.function && typeof tc.function.name === 'string' ? tc.function.name.trim() : '';
159
+ if (n)
160
+ prunedToolNames.add(n);
161
+ }
162
+ // Drop tool_calls when no tools are declared.
163
+ const keptContent = msg.content;
164
+ const hasContent = typeof keptContent === 'string'
165
+ ? keptContent.trim().length > 0
166
+ : Array.isArray(keptContent)
167
+ ? keptContent.length > 0
168
+ : false;
169
+ if (hasContent) {
170
+ nextMessages.push({ ...msg, tool_calls: [] });
171
+ }
172
+ continue;
173
+ }
174
+ const keptCalls = [];
175
+ for (const tc of msg.tool_calls) {
176
+ if (!tc || typeof tc !== 'object')
177
+ continue;
178
+ const name = tc.function && typeof tc.function.name === 'string' ? tc.function.name.trim() : '';
179
+ if (!name || !allowedToolNames.has(name)) {
180
+ prunedToolCalls += 1;
181
+ if (name)
182
+ prunedToolNames.add(name);
183
+ continue;
184
+ }
185
+ keptCalls.push(tc);
186
+ if (typeof tc.id === 'string' && tc.id.trim()) {
187
+ keptToolCallIds.add(tc.id.trim());
188
+ }
189
+ }
190
+ const keptContent = msg.content;
191
+ const hasContent = typeof keptContent === 'string'
192
+ ? keptContent.trim().length > 0
193
+ : Array.isArray(keptContent)
194
+ ? keptContent.length > 0
195
+ : false;
196
+ if (!hasContent && keptCalls.length === 0) {
197
+ continue;
198
+ }
199
+ if (keptCalls.length === msg.tool_calls.length) {
200
+ nextMessages.push(msg);
201
+ }
202
+ else {
203
+ nextMessages.push({ ...msg, tool_calls: keptCalls.length ? keptCalls : undefined });
204
+ }
205
+ continue;
206
+ }
207
+ if (msg.role === 'tool') {
208
+ const name = typeof msg.name === 'string' ? msg.name.trim() : '';
209
+ const toolCallId = typeof msg.tool_call_id === 'string' ? msg.tool_call_id.trim() : '';
210
+ if (!hasAnyTools) {
211
+ prunedToolResults += 1;
212
+ if (name)
213
+ prunedToolNames.add(name);
214
+ continue;
215
+ }
216
+ if (name && !allowedToolNames.has(name)) {
217
+ prunedToolResults += 1;
218
+ prunedToolNames.add(name);
219
+ continue;
220
+ }
221
+ // If the result references a tool_call_id that no longer exists, drop it to avoid
222
+ // provider-side strict pairing errors (Gemini/Anthropic tool pairing).
223
+ if (toolCallId && !keptToolCallIds.has(toolCallId)) {
224
+ prunedToolResults += 1;
225
+ if (name)
226
+ prunedToolNames.add(name);
227
+ continue;
228
+ }
229
+ nextMessages.push(msg);
230
+ continue;
231
+ }
232
+ nextMessages.push(msg);
233
+ }
234
+ if (prunedToolCalls === 0 && prunedToolResults === 0) {
235
+ return request;
236
+ }
237
+ return {
238
+ ...request,
239
+ messages: nextMessages,
240
+ metadata: {
241
+ ...request.metadata,
242
+ toolHistoryPrune: {
243
+ prunedToolCalls,
244
+ prunedToolResults,
245
+ prunedToolNames: Array.from(prunedToolNames).sort()
246
+ }
247
+ }
248
+ };
249
+ }
250
+ function extractToolCallIdsFromToolMessages(messages) {
251
+ const ids = new Set();
252
+ for (const msg of messages) {
253
+ if (!msg || typeof msg !== 'object')
254
+ continue;
255
+ if (msg.role !== 'tool')
256
+ continue;
257
+ const toolCallId = typeof msg.tool_call_id === 'string'
258
+ ? String(msg.tool_call_id).trim()
259
+ : typeof msg.toolCallId === 'string'
260
+ ? String(msg.toolCallId).trim()
261
+ : '';
262
+ if (toolCallId)
263
+ ids.add(toolCallId);
264
+ }
265
+ return ids;
266
+ }
267
+ function findLastToolMessageIndex(messages) {
268
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
269
+ const msg = messages[i];
270
+ if (msg && typeof msg === 'object' && msg.role === 'tool') {
271
+ return i;
272
+ }
273
+ }
274
+ return -1;
275
+ }
276
+ async function maybeInjectPendingServerToolResultsAfterClientTools(request, metadata) {
277
+ const sessionId = resolveSessionIdForPending(metadata, request);
278
+ if (!sessionId) {
279
+ return request;
280
+ }
281
+ const pending = await loadPendingServerToolInjection(sessionId);
282
+ if (!pending) {
283
+ return request;
284
+ }
285
+ const afterIds = Array.isArray(pending.afterToolCallIds) ? pending.afterToolCallIds : [];
286
+ if (!afterIds.length) {
287
+ return request;
288
+ }
289
+ const messages = Array.isArray(request.messages) ? request.messages : [];
290
+ const toolIds = extractToolCallIdsFromToolMessages(messages);
291
+ const ready = afterIds.every((id) => toolIds.has(id));
292
+ if (!ready) {
293
+ return request;
294
+ }
295
+ const insertAt = findLastToolMessageIndex(messages);
296
+ if (insertAt < 0) {
297
+ return request;
298
+ }
299
+ const inject = Array.isArray(pending.messages) ? pending.messages : [];
300
+ if (!inject.length) {
301
+ return request;
302
+ }
303
+ const nextMessages = messages.slice();
304
+ nextMessages.splice(insertAt + 1, 0, ...inject);
305
+ try {
306
+ await clearPendingServerToolInjection(sessionId);
307
+ }
308
+ catch {
309
+ // best-effort
310
+ }
311
+ return { ...request, messages: nextMessages };
122
312
  }
123
313
  function buildProcessedRequest(request) {
124
314
  const timestamp = Date.now();
@@ -423,13 +613,38 @@ function buildToolChoiceOperations(toolChoice) {
423
613
  }
424
614
  ];
425
615
  }
616
+ function isGeminiAntigravityLike(metadata) {
617
+ const providerProtocol = readString(metadata.providerProtocol) ?? readString(metadata.provider) ?? '';
618
+ if (!providerProtocol.toLowerCase().includes('gemini')) {
619
+ return false;
620
+ }
621
+ const rt = readRuntimeMetadata(metadata);
622
+ const providerId = readString(metadata.providerId) ??
623
+ readString(rt?.providerId) ??
624
+ '';
625
+ const providerKey = readString(metadata.providerKey) ??
626
+ readString(rt?.providerKey) ??
627
+ '';
628
+ const loweredId = providerId.toLowerCase();
629
+ const loweredKey = providerKey.toLowerCase();
630
+ return (loweredId.includes('antigravity') ||
631
+ loweredId === 'gemini-cli' ||
632
+ loweredKey.startsWith('antigravity.') ||
633
+ loweredKey.startsWith('gemini-cli.'));
634
+ }
426
635
  function buildWebSearchOperations(request, metadata) {
636
+ const rt = readRuntimeMetadata(metadata);
637
+ // gcli2api-style: do not force-inject server-side web_search tools for Gemini/Antigravity.
638
+ // Tools must be present only when supplied by the client payload/config, not auto-appended.
639
+ if (isGeminiAntigravityLike(metadata)) {
640
+ return [];
641
+ }
427
642
  // ServerTool 二/三跳(serverToolFollowup=true)不再注入 web_search 工具,
428
643
  // 以避免在 web_search 流程内部形成循环命中。
429
- if (metadata.serverToolFollowup === true) {
644
+ if (rt?.serverToolFollowup === true) {
430
645
  return [];
431
646
  }
432
- const rawConfig = metadata.webSearch;
647
+ const rawConfig = rt?.webSearch;
433
648
  if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
434
649
  return [];
435
650
  }
@@ -535,12 +750,20 @@ function buildWebSearchOperations(request, metadata) {
535
750
  });
536
751
  return ops;
537
752
  }
538
- function buildClockOperations(metadata) {
539
- const rawConfig = metadata.clock;
540
- const clockConfig = normalizeClockConfig(rawConfig);
753
+ function buildClockOperations(request, metadata) {
754
+ const rt = readRuntimeMetadata(metadata);
755
+ // gcli2api-style: do not force-inject server-side clock tools for Gemini/Antigravity.
756
+ if (isGeminiAntigravityLike(metadata)) {
757
+ return [];
758
+ }
759
+ const rawConfig = rt?.clock;
760
+ const clockConfig = resolveClockConfig(rawConfig);
541
761
  if (!clockConfig) {
542
762
  return [];
543
763
  }
764
+ const sessionId = readString(metadata.sessionId);
765
+ const hasSessionId = Boolean(sessionId && sessionId.trim());
766
+ logClock('inject_schema', { hasSessionId });
544
767
  const parameters = {
545
768
  type: 'object',
546
769
  properties: {
@@ -568,12 +791,13 @@ function buildClockOperations(metadata) {
568
791
  description: 'Optional suggested tool name (hint only).'
569
792
  },
570
793
  arguments: {
571
- type: 'object',
572
- description: 'Optional suggested tool arguments (hint only).',
573
- additionalProperties: true
794
+ type: 'string',
795
+ description: 'Optional suggested tool arguments as a JSON string (hint only). Use "{}" when unsure.'
574
796
  }
575
797
  },
576
- required: ['dueAt', 'task'],
798
+ // For strict tool schemas (OpenAI Responses), required must include every key in properties.
799
+ // We keep tool/arguments as hints but allow empty string when not used.
800
+ required: ['dueAt', 'task', 'tool', 'arguments'],
577
801
  additionalProperties: false
578
802
  }
579
803
  },
@@ -582,7 +806,9 @@ function buildClockOperations(metadata) {
582
806
  description: 'For cancel: taskId to remove.'
583
807
  }
584
808
  },
585
- required: ['action'],
809
+ // For strict tool schemas (OpenAI Responses), required must include every key in properties.
810
+ // Fields unused for certain actions can be set to empty string / empty list.
811
+ required: ['action', 'items', 'taskId'],
586
812
  additionalProperties: false
587
813
  };
588
814
  const clockTool = {
@@ -595,7 +821,10 @@ function buildClockOperations(metadata) {
595
821
  }
596
822
  };
597
823
  return [
598
- { op: 'set_request_metadata_fields', fields: { clockEnabled: true, serverToolRequired: true } },
824
+ {
825
+ op: 'set_request_metadata_fields',
826
+ fields: { clockEnabled: true, ...(hasSessionId ? { serverToolRequired: true } : {}) }
827
+ },
599
828
  { op: 'append_tool_if_missing', toolName: 'clock', tool: clockTool }
600
829
  ];
601
830
  }
@@ -657,8 +886,13 @@ function findLastUserMessageIndex(messages) {
657
886
  return -1;
658
887
  }
659
888
  async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, requestId) {
660
- const rawConfig = metadata.clock;
661
- const clockConfig = normalizeClockConfig(rawConfig);
889
+ const rt = readRuntimeMetadata(metadata);
890
+ // Do not inject reminders or apply clock directives during internal servertool followup hops.
891
+ if (rt?.serverToolFollowup === true) {
892
+ return request;
893
+ }
894
+ const rawConfig = rt?.clock;
895
+ const clockConfig = resolveClockConfig(rawConfig);
662
896
  if (!clockConfig) {
663
897
  return request;
664
898
  }
@@ -687,6 +921,7 @@ async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, re
687
921
  if (sessionId) {
688
922
  try {
689
923
  await clearClockSession(sessionId);
924
+ logClock('cleared', { sessionId });
690
925
  }
691
926
  catch {
692
927
  // best-effort: user directive should not crash request
@@ -707,6 +942,11 @@ async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, re
707
942
  if (!reservation || typeof injectText !== 'string' || !injectText.trim()) {
708
943
  return request;
709
944
  }
945
+ logClock('inject_due', {
946
+ sessionId,
947
+ reservationId: reservation.reservationId,
948
+ taskIds: Array.isArray(reservation.taskIds) ? reservation.taskIds.length : 0
949
+ });
710
950
  const baseMetadata = request.metadata && typeof request.metadata === 'object'
711
951
  ? request.metadata
712
952
  : {
@@ -10,6 +10,14 @@ export interface ProviderResponseConversionOptions {
10
10
  context: AdapterContext;
11
11
  entryEndpoint: string;
12
12
  wantsStream: boolean;
13
+ /**
14
+ * Canonical chat semantics from the request-side chat_process output.
15
+ * This is the only allowed carrier for mappable cross-protocol semantics
16
+ * (tool alias maps, client tool schemas, responses resume, etc.).
17
+ *
18
+ * Must not be stuffed into metadata/AdapterContext.
19
+ */
20
+ requestSemantics?: JsonObject;
13
21
  stageRecorder?: StageRecorder;
14
22
  providerInvoker?: ProviderInvoker;
15
23
  /**
@@ -14,6 +14,8 @@ import { runRespProcessStage2Finalize } from '../pipeline/stages/resp_process/re
14
14
  import { runRespOutboundStage1ClientRemap } from '../pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js';
15
15
  import { runRespOutboundStage2SseStream } from '../pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js';
16
16
  import { recordResponsesResponse } from '../../shared/responses-conversation-store.js';
17
+ import { ProviderProtocolError } from '../../shared/errors.js';
18
+ import { readRuntimeMetadata } from '../../shared/runtime-metadata.js';
17
19
  import { runServerToolOrchestration } from '../../../servertool/engine.js';
18
20
  import { commitClockReservation, normalizeClockConfig } from '../../../servertool/clock/task-store.js';
19
21
  const PROVIDER_RESPONSE_REGISTRY = {
@@ -35,7 +37,8 @@ const PROVIDER_RESPONSE_REGISTRY = {
35
37
  }
36
38
  };
37
39
  function isServerToolFollowup(context) {
38
- const raw = context.serverToolFollowup;
40
+ const rt = readRuntimeMetadata(context);
41
+ const raw = rt?.serverToolFollowup;
39
42
  if (raw === true) {
40
43
  return true;
41
44
  }
@@ -85,7 +88,8 @@ function coerceClockReservation(value) {
85
88
  }
86
89
  async function maybeCommitClockReservationFromContext(context) {
87
90
  try {
88
- const clockConfig = normalizeClockConfig(context.clock);
91
+ const rt = readRuntimeMetadata(context);
92
+ const clockConfig = normalizeClockConfig(rt?.clock);
89
93
  if (!clockConfig) {
90
94
  return;
91
95
  }
@@ -114,6 +118,42 @@ function detectProviderResponseShape(payload) {
114
118
  return 'gemini-chat';
115
119
  return 'unknown';
116
120
  }
121
+ function isCanonicalChatCompletion(payload) {
122
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
123
+ return false;
124
+ }
125
+ const obj = payload;
126
+ const choices = Array.isArray(obj.choices) ? obj.choices : [];
127
+ if (!choices.length)
128
+ return false;
129
+ const first = choices[0] && typeof choices[0] === 'object' && !Array.isArray(choices[0]) ? choices[0] : null;
130
+ if (!first)
131
+ return false;
132
+ const msg = first.message;
133
+ return Boolean(msg && typeof msg === 'object' && !Array.isArray(msg));
134
+ }
135
+ async function coerceClientPayloadToCanonicalChatCompletionOrThrow(options) {
136
+ if (isCanonicalChatCompletion(options.payload)) {
137
+ return options.payload;
138
+ }
139
+ const detected = detectProviderResponseShape(options.payload);
140
+ if (detected === 'unknown') {
141
+ throw new ProviderProtocolError(`[hub_response] Non-canonical response payload at ${options.scope}`, {
142
+ code: 'MALFORMED_RESPONSE',
143
+ details: { detected }
144
+ });
145
+ }
146
+ const plan = PROVIDER_RESPONSE_REGISTRY[detected];
147
+ const mapper = plan.createMapper();
148
+ const coerced = await mapper.toChatCompletion({ payload: options.payload }, options.adapterContext, { requestSemantics: options.requestSemantics });
149
+ if (isCanonicalChatCompletion(coerced)) {
150
+ return coerced;
151
+ }
152
+ throw new ProviderProtocolError(`[hub_response] Failed to canonicalize response payload at ${options.scope}`, {
153
+ code: 'MALFORMED_RESPONSE',
154
+ details: { detected }
155
+ });
156
+ }
117
157
  function summarizeToolCallsFromProviderResponse(payload) {
118
158
  try {
119
159
  if (!isJsonRecord(payload))
@@ -169,6 +209,7 @@ function stripInternalPolicyDebugFields(payload) {
169
209
  delete target.__responses_reasoning;
170
210
  delete target.__responses_payload_snapshot;
171
211
  delete target.__responses_passthrough;
212
+ delete target.anthropicToolNameMap;
172
213
  }
173
214
  function supportsSseProtocol(protocol) {
174
215
  return protocol === 'openai-chat' || protocol === 'openai-responses' || protocol === 'anthropic-messages' || protocol === 'gemini-chat';
@@ -212,23 +253,20 @@ function resolveChatReasoningMode(_entryEndpoint) {
212
253
  return 'keep';
213
254
  }
214
255
  export async function convertProviderResponse(options) {
215
- const clientProtocol = resolveClientProtocol(options.entryEndpoint);
256
+ const isFollowup = isServerToolFollowup(options.context);
257
+ // ServerTool followups are internal hops. They must return canonical OpenAI-chat-like payloads
258
+ // to re-enter the hub chat process deterministically (tools harvesting, governance, etc.).
259
+ // Client protocol remapping happens only on the outermost request.
260
+ const clientProtocol = isFollowup ? 'openai-chat' : resolveClientProtocol(options.entryEndpoint);
216
261
  const hasServerToolSupport = Boolean(options.providerInvoker) || Boolean(options.reenterPipeline);
217
- // 是否跳过 ServerTool 编排:
218
- // - 仅在当前 Provider 完全不支持 ServerTool(没有 invoker/reenterPipeline)时跳过;
219
- // - 对于 serverToolFollowup=true 的二/三跳请求,也允许再次进入 ServerTool 流程,
220
- // 由具体 handler(例如 gemini_empty_reply_continue / iflow_model_error_retry 等)
221
- // 自行通过 serverToolFollowup 标记决定是否生效。
222
- const skipServerTools = !hasServerToolSupport;
223
- // 对于由 server-side 工具触发的内部跳转(二跳/三跳),统一禁用 SSE 聚合输出,
224
- // 始终返回完整的 ChatCompletion JSON,便于在 llms 内部直接解析,而不是拿到
225
- // __sse_responses 可读流。
226
- const wantsStream = isServerToolFollowup(options.context) ? false : options.wantsStream;
262
+ // ServerTool followup hop: always return a fully materialized ChatCompletion JSON (non-SSE).
263
+ // This is an internal hop used by the servertool engine, and it must be deterministic.
264
+ const wantsStream = isFollowup ? false : options.wantsStream;
227
265
  try {
228
266
  // eslint-disable-next-line no-console
229
267
  console.log(`\x1b[38;5;33m[servertool][orchestrator][debug] requestId=${options.context.requestId} ` +
230
268
  `protocol=${options.providerProtocol} endpoint=${options.entryEndpoint} ` +
231
- `skipServerTools=${skipServerTools} hasInvoker=${Boolean(options.providerInvoker)} ` +
269
+ `serverTools=${hasServerToolSupport} hasInvoker=${Boolean(options.providerInvoker)} ` +
232
270
  `hasReenter=${Boolean(options.reenterPipeline)}\x1b[0m`);
233
271
  }
234
272
  catch {
@@ -255,17 +293,6 @@ export async function convertProviderResponse(options) {
255
293
  formatAdapter,
256
294
  stageRecorder: options.stageRecorder
257
295
  });
258
- if (options.providerProtocol === 'openai-responses') {
259
- try {
260
- recordResponsesResponse({
261
- requestId: options.context.requestId,
262
- response: formatEnvelope.payload
263
- });
264
- }
265
- catch {
266
- // ignore conversation capture errors
267
- }
268
- }
269
296
  formatEnvelope.payload = runRespInboundStageCompatResponse({
270
297
  payload: formatEnvelope.payload,
271
298
  adapterContext: options.context,
@@ -311,14 +338,15 @@ export async function convertProviderResponse(options) {
311
338
  adapterContext: options.context,
312
339
  formatEnvelope,
313
340
  mapper,
341
+ requestSemantics: options.requestSemantics,
314
342
  stageRecorder: options.stageRecorder
315
343
  });
316
344
  // 记录语义映射后的 ChatCompletion,便于回放 server-side 工具流程。
317
- recordStage(options.stageRecorder, 'resp_inbound_stage3_semantic_map.chat', chatResponse);
345
+ recordStage(options.stageRecorder, 'chat_process.resp.stage4.semantic_map_to_chat.chat', chatResponse);
318
346
  // 检查是否需要进行 ServerTool 编排
319
347
  // 使用新的 ChatEnvelope 级别的 servertool 实现
320
348
  let effectiveChatResponse = chatResponse;
321
- if (!skipServerTools && options.reenterPipeline) {
349
+ if (hasServerToolSupport) {
322
350
  try {
323
351
  // eslint-disable-next-line no-console
324
352
  console.log(`\x1b[38;5;33m[servertool][orchestrator] start requestId=${options.context.requestId} ` +
@@ -348,6 +376,12 @@ export async function convertProviderResponse(options) {
348
376
  /* logging best-effort */
349
377
  }
350
378
  effectiveChatResponse = orchestration.chat;
379
+ recordStage(options.stageRecorder, 'chat_process.resp.stage5.servertool_orchestration', {
380
+ executed: true,
381
+ flowId: orchestration.flowId,
382
+ inputShape: detectProviderResponseShape(chatResponse),
383
+ outputShape: detectProviderResponseShape(effectiveChatResponse)
384
+ });
351
385
  }
352
386
  else {
353
387
  try {
@@ -358,8 +392,20 @@ export async function convertProviderResponse(options) {
358
392
  catch {
359
393
  /* logging best-effort */
360
394
  }
395
+ recordStage(options.stageRecorder, 'chat_process.resp.stage5.servertool_orchestration', {
396
+ executed: false,
397
+ inputShape: detectProviderResponseShape(chatResponse)
398
+ });
361
399
  }
362
400
  }
401
+ // Hard gate: response-side chat_process requires an OpenAI-chat-like surface (choices[].message).
402
+ // ServerTool followups must never replace the canonical chat completion with a client-protocol shape.
403
+ effectiveChatResponse = await coerceClientPayloadToCanonicalChatCompletionOrThrow({
404
+ payload: effectiveChatResponse,
405
+ adapterContext: options.context,
406
+ requestSemantics: options.requestSemantics,
407
+ scope: 'chat_process.response.entry'
408
+ });
363
409
  // 如果没有执行 servertool,继续原来的处理流程
364
410
  const governanceResult = await runRespProcessStage1ToolGovernance({
365
411
  payload: effectiveChatResponse,
@@ -382,10 +428,22 @@ export async function convertProviderResponse(options) {
382
428
  clientProtocol,
383
429
  requestId: clientFacingRequestId,
384
430
  adapterContext: options.context,
431
+ requestSemantics: options.requestSemantics,
385
432
  stageRecorder: options.stageRecorder
386
433
  });
387
434
  applyModelOverride(clientPayload, displayModel);
388
435
  stripInternalPolicyDebugFields(clientPayload);
436
+ if (clientProtocol === 'openai-responses') {
437
+ try {
438
+ recordResponsesResponse({
439
+ requestId: options.context.requestId,
440
+ response: clientPayload
441
+ });
442
+ }
443
+ catch {
444
+ // ignore conversation capture errors
445
+ }
446
+ }
389
447
  // Phase 2 (shadow): response tool surface mismatch detection (client outbound).
390
448
  try {
391
449
  if (options.stageRecorder && isToolSurfaceShadowEnabled()) {
@@ -1,9 +1,12 @@
1
1
  import type { FormatEnvelope } from '../types/format-envelope.js';
2
2
  import type { AdapterContext } from '../types/chat-envelope.js';
3
3
  import type { JsonObject } from '../types/json.js';
4
+ import type { ChatSemantics } from '../types/chat-envelope.js';
4
5
  export type ChatCompletionLike = JsonObject;
5
6
  export interface ResponseMapper {
6
- toChatCompletion(format: FormatEnvelope, ctx: AdapterContext): Promise<ChatCompletionLike> | ChatCompletionLike;
7
+ toChatCompletion(format: FormatEnvelope, ctx: AdapterContext, options?: {
8
+ requestSemantics?: ChatSemantics | JsonObject;
9
+ }): Promise<ChatCompletionLike> | ChatCompletionLike;
7
10
  }
8
11
  export declare class OpenAIChatResponseMapper implements ResponseMapper {
9
12
  toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext): ChatCompletionLike;
@@ -12,8 +15,12 @@ export declare class ResponsesResponseMapper implements ResponseMapper {
12
15
  toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext): ChatCompletionLike;
13
16
  }
14
17
  export declare class AnthropicResponseMapper implements ResponseMapper {
15
- toChatCompletion(format: FormatEnvelope, ctx: AdapterContext): ChatCompletionLike;
18
+ toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext, options?: {
19
+ requestSemantics?: ChatSemantics | JsonObject;
20
+ }): ChatCompletionLike;
16
21
  }
17
22
  export declare class GeminiResponseMapper implements ResponseMapper {
18
- toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext): ChatCompletionLike;
23
+ toChatCompletion(format: FormatEnvelope, ctx: AdapterContext, options?: {
24
+ requestSemantics?: ChatSemantics | JsonObject;
25
+ }): ChatCompletionLike;
19
26
  }
@@ -12,18 +12,25 @@ export class ResponsesResponseMapper {
12
12
  }
13
13
  }
14
14
  export class AnthropicResponseMapper {
15
- toChatCompletion(format, ctx) {
16
- const aliasMap = extractAnthropicAliasMap(ctx);
15
+ toChatCompletion(format, _ctx, options) {
16
+ const aliasMap = extractToolNameAliasMapFromSemantics(options?.requestSemantics);
17
17
  return buildOpenAIChatFromAnthropicMessage(format.payload ?? {}, { aliasMap });
18
18
  }
19
19
  }
20
20
  export class GeminiResponseMapper {
21
- toChatCompletion(format, _ctx) {
22
- return buildOpenAIChatFromGeminiResponse(format.payload ?? {});
21
+ toChatCompletion(format, ctx, options) {
22
+ const aliasMap = extractToolNameAliasMapFromSemantics(options?.requestSemantics) ?? extractGeminiToolNameAliasMapFromContext(ctx);
23
+ return buildOpenAIChatFromGeminiResponse(format.payload ?? {}, { aliasMap });
23
24
  }
24
25
  }
25
- function extractAnthropicAliasMap(ctx) {
26
- const candidate = ctx?.anthropicToolNameMap;
26
+ function extractToolNameAliasMapFromSemantics(semantics) {
27
+ if (!semantics || typeof semantics !== 'object' || Array.isArray(semantics)) {
28
+ return undefined;
29
+ }
30
+ const toolsNode = semantics.tools;
31
+ const candidate = toolsNode && typeof toolsNode === 'object' && !Array.isArray(toolsNode)
32
+ ? toolsNode.toolNameAliasMap
33
+ : undefined;
27
34
  if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
28
35
  return undefined;
29
36
  }
@@ -41,3 +48,20 @@ function extractAnthropicAliasMap(ctx) {
41
48
  }
42
49
  return Object.keys(normalized).length ? normalized : undefined;
43
50
  }
51
+ function extractGeminiToolNameAliasMapFromContext(ctx) {
52
+ const candidate = ctx?.geminiToolNameAliasMap;
53
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
54
+ return undefined;
55
+ }
56
+ const normalized = {};
57
+ for (const [key, value] of Object.entries(candidate)) {
58
+ if (typeof key !== 'string' || typeof value !== 'string')
59
+ continue;
60
+ const trimmedKey = key.trim();
61
+ const trimmedValue = value.trim();
62
+ if (!trimmedKey.length || !trimmedValue.length)
63
+ continue;
64
+ normalized[trimmedKey] = trimmedValue;
65
+ }
66
+ return Object.keys(normalized).length ? normalized : undefined;
67
+ }