@jsonstudio/llms 0.6.3541 → 0.6.3685

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 (100) hide show
  1. package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +23 -114
  2. package/dist/conversion/compat/actions/auto-thinking.js +3 -2
  3. package/dist/conversion/compat/actions/deepseek-web-response.js +9 -50
  4. package/dist/conversion/compat/actions/field-mapping.js +2 -153
  5. package/dist/conversion/compat/actions/gemini-cli-request.d.ts +2 -0
  6. package/dist/conversion/compat/actions/gemini-cli-request.js +1 -1
  7. package/dist/conversion/compat/actions/glm-history-image-trim.js +3 -37
  8. package/dist/conversion/compat/actions/glm-image-content.js +3 -32
  9. package/dist/conversion/compat/actions/glm-native-compat.d.ts +6 -0
  10. package/dist/conversion/compat/actions/glm-native-compat.js +34 -0
  11. package/dist/conversion/compat/actions/glm-vision-prompt.js +3 -76
  12. package/dist/conversion/compat/actions/glm-web-search.js +10 -43
  13. package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.js +4 -53
  14. package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.js +5 -141
  15. package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.js +7 -28
  16. package/dist/conversion/compat/actions/iflow-native-compat.d.ts +6 -0
  17. package/dist/conversion/compat/actions/iflow-native-compat.js +36 -0
  18. package/dist/conversion/compat/actions/iflow-response-body-unwrap.js +4 -119
  19. package/dist/conversion/compat/actions/iflow-web-search.js +14 -55
  20. package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.js +3 -104
  21. package/dist/conversion/hub/node-support.js +1 -1
  22. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +9 -1
  23. package/dist/conversion/hub/operation-table/semantic-mappers/archive/chat-mapper.archive.js +5 -0
  24. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +34 -14
  25. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +14 -14
  26. package/dist/conversion/hub/pipeline/hub-pipeline.js +838 -524
  27. package/dist/conversion/hub/pipeline/hub-stage-timing.d.ts +6 -0
  28. package/dist/conversion/hub/pipeline/hub-stage-timing.js +178 -0
  29. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +6 -4
  30. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +46 -0
  31. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-capture-orchestration.d.ts +3 -0
  32. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-capture-orchestration.js +2 -1
  33. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.js +2 -0
  34. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +1 -0
  35. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/responses-context-snapshot.d.ts +3 -2
  36. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/responses-context-snapshot.js +18 -5
  37. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/context-merge.d.ts +1 -2
  38. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/context-merge.js +0 -16
  39. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.d.ts +1 -1
  40. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +30 -12
  41. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.d.ts +1 -0
  42. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +5 -2
  43. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.d.ts +1 -1
  44. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +9 -5
  45. package/dist/conversion/hub/process/chat-process-continue-execution.js +2 -4
  46. package/dist/conversion/hub/process/chat-process-governance-orchestration.js +3 -1
  47. package/dist/conversion/hub/process/chat-process-media.d.ts +1 -0
  48. package/dist/conversion/hub/process/chat-process-media.js +36 -0
  49. package/dist/conversion/hub/process/chat-process-session-usage.d.ts +25 -0
  50. package/dist/conversion/hub/process/chat-process-session-usage.js +246 -0
  51. package/dist/conversion/hub/response/provider-response.js +13 -0
  52. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  53. package/dist/conversion/pipeline/codecs/v2/openai-openai-pipeline.js +0 -4
  54. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +0 -12
  55. package/dist/conversion/responses/responses-openai-bridge/types.d.ts +1 -9
  56. package/dist/conversion/responses/responses-openai-bridge.d.ts +1 -0
  57. package/dist/conversion/responses/responses-openai-bridge.js +51 -24
  58. package/dist/conversion/shared/anthropic-message-utils.js +14 -1
  59. package/dist/conversion/shared/reasoning-normalizer.js +61 -0
  60. package/dist/conversion/shared/tool-governor.js +2 -4
  61. package/dist/native/router_hotpath_napi.node +0 -0
  62. package/dist/quota/quota-state.js +1 -6
  63. package/dist/router/virtual-router/bootstrap/profile-builder.js +1 -0
  64. package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +1 -0
  65. package/dist/router/virtual-router/bootstrap/provider-normalization.js +6 -0
  66. package/dist/router/virtual-router/bootstrap.js +1 -6
  67. package/dist/router/virtual-router/engine/routing-state/store.js +21 -2
  68. package/dist/router/virtual-router/engine-legacy.js +43 -0
  69. package/dist/router/virtual-router/engine-logging.d.ts +3 -0
  70. package/dist/router/virtual-router/engine-logging.js +29 -3
  71. package/dist/router/virtual-router/engine-selection/native-chat-process-governed-filter-semantics.d.ts +1 -0
  72. package/dist/router/virtual-router/engine-selection/native-chat-process-governed-filter-semantics.js +1 -0
  73. package/dist/router/virtual-router/engine-selection/native-compat-action-semantics.d.ts +3 -0
  74. package/dist/router/virtual-router/engine-selection/native-compat-action-semantics.js +72 -0
  75. package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.d.ts +1 -1
  76. package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.js +1 -1
  77. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-inbound-outbound-semantics.d.ts +0 -1
  78. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-inbound-outbound-semantics.js +0 -29
  79. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-process-semantics.d.ts +1 -0
  80. package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.js +6 -2
  81. package/dist/router/virtual-router/engine.js +28 -13
  82. package/dist/router/virtual-router/provider-registry.js +1 -0
  83. package/dist/router/virtual-router/routing-instructions/state.js +44 -2
  84. package/dist/router/virtual-router/routing-instructions/types.d.ts +6 -0
  85. package/dist/router/virtual-router/token-estimator.js +21 -0
  86. package/dist/router/virtual-router/types.d.ts +7 -0
  87. package/dist/servertool/engine.js +3 -34
  88. package/dist/servertool/handlers/followup-request-builder.js +0 -6
  89. package/dist/servertool/handlers/gemini-empty-reply-continue.js +3 -274
  90. package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +0 -3
  91. package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +0 -29
  92. package/dist/servertool/handlers/stop-message-auto.js +11 -9
  93. package/dist/servertool/handlers/vision.js +4 -1
  94. package/dist/servertool/server-side-tools.d.ts +0 -1
  95. package/dist/servertool/server-side-tools.js +67 -5
  96. package/dist/tools/apply-patch/execution-capturer.d.ts +1 -1
  97. package/dist/tools/apply-patch/execution-capturer.js +1 -2
  98. package/dist/tools/apply-patch/regression-capturer.js +2 -1
  99. package/dist/tools/tool-registry.js +1 -2
  100. package/package.json +1 -1
@@ -6,6 +6,7 @@ import { ProviderRegistry } from './provider-registry.js';
6
6
  import { resolveStopMessageScope } from './engine/routing-state/store.js';
7
7
  import { loadRoutingInstructionStateSync } from './sticky-session-store.js';
8
8
  import { mergeStopMessageFromPersisted } from './stop-message-state-sync.js';
9
+ import { resolveSessionColor } from './engine-logging.js';
9
10
  export class VirtualRouterEngine {
10
11
  nativeProxy;
11
12
  registry;
@@ -72,6 +73,7 @@ export class VirtualRouterEngine {
72
73
  parseLog?.scopedTypes.some((type) => type === 'stopMessageSet' || type === 'stopMessageMode' || type === 'stopMessageClear'));
73
74
  emitVirtualRouterHitLog(parsed, {
74
75
  requestId: metadata.requestId,
76
+ sessionId: resolveVirtualRouterLogSessionId(metadata),
75
77
  stopScope,
76
78
  stopState,
77
79
  forceStopStatusLabel
@@ -176,6 +178,12 @@ function extractNativeErrorMessage(error) {
176
178
  if (error instanceof Error) {
177
179
  return error.message;
178
180
  }
181
+ if (error && typeof error === 'object') {
182
+ const message = error.message;
183
+ if (typeof message === 'string') {
184
+ return message;
185
+ }
186
+ }
179
187
  return '';
180
188
  }
181
189
  function parseVirtualRouterErrorMessage(message) {
@@ -274,30 +282,37 @@ function emitVirtualRouterHitLog(result, options) {
274
282
  const prefixColor = '\x1b[38;5;208m';
275
283
  const timeColor = '\x1b[90m';
276
284
  const stopColor = '\x1b[38;5;214m';
277
- const routeColorMap = {
278
- multimodal: '\x1b[38;5;45m',
279
- tools: '\x1b[38;5;214m',
280
- thinking: '\x1b[34m',
281
- coding: '\x1b[35m',
282
- longcontext: '\x1b[38;5;141m',
283
- web_search: '\x1b[32m',
284
- search: '\x1b[38;5;34m',
285
- vision: '\x1b[38;5;207m',
286
- background: '\x1b[90m'
287
- };
288
285
  const now = new Date();
289
286
  const timestamp = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
290
287
  const routeLabel = result.decision.poolId
291
288
  ? `${result.decision.routeName}/${result.decision.poolId}`
292
289
  : result.decision.routeName;
293
- const routeColor = routeColorMap[result.decision.routeName] ?? '\x1b[36m';
290
+ const routeColor = resolveSessionColor(options?.sessionId);
294
291
  const providerKey = result.decision.providerKey || result.target.providerKey;
295
292
  const modelSuffix = result.target.modelId ? `.${result.target.modelId}` : '';
296
293
  const reason = result.decision.reasoning ? ` reason=${result.decision.reasoning}` : '';
297
294
  const stopStatusLabel = formatStopMessageStatusLabel(options?.stopState ?? null, options?.stopScope, Boolean(options?.forceStopStatusLabel));
298
295
  const requestId = typeof options?.requestId === 'string' ? options.requestId : '';
299
296
  const requestLabel = requestId && !requestId.includes('unknown') ? ` req=${requestId}` : '';
300
- console.log(`${prefixColor}[virtual-router-hit]${reset} ${timeColor}${timestamp}${reset}${requestLabel} ${routeColor}${routeLabel} -> ${providerKey}${modelSuffix}${reason}${reset}${stopStatusLabel ? ` ${stopColor}${stopStatusLabel}${reset}` : ''}`);
297
+ const sessionId = typeof options?.sessionId === 'string' ? options.sessionId.trim() : '';
298
+ const sessionLabel = sessionId ? ` sid=${sessionId}` : '';
299
+ console.log(`${prefixColor}[virtual-router-hit]${reset} ${timeColor}${timestamp}${reset}${requestLabel}${sessionLabel} ${routeColor}${routeLabel} -> ${providerKey}${modelSuffix}${reason}${reset}${stopStatusLabel ? ` ${stopColor}${stopStatusLabel}${reset}` : ''}`);
300
+ }
301
+ function resolveVirtualRouterLogSessionId(metadata) {
302
+ const candidates = [
303
+ metadata.sessionId,
304
+ metadata.clientTmuxSessionId,
305
+ metadata.client_tmux_session_id,
306
+ metadata.tmuxSessionId,
307
+ metadata.tmux_session_id,
308
+ metadata.conversationId
309
+ ];
310
+ for (const value of candidates) {
311
+ if (typeof value === 'string' && value.trim()) {
312
+ return value.trim();
313
+ }
314
+ }
315
+ return undefined;
301
316
  }
302
317
  function formatStopMessageStatusLabel(snapshot, scope, forceShow) {
303
318
  const scopeLabel = scope && scope.trim() ? scope.trim() : 'none';
@@ -97,6 +97,7 @@ export class ProviderRegistry {
97
97
  providerType: profile.providerType,
98
98
  endpoint: profile.endpoint,
99
99
  auth: profile.auth,
100
+ ...(profile.enabled !== undefined ? { enabled: profile.enabled } : {}),
100
101
  outboundProfile: profile.outboundProfile,
101
102
  compatibilityProfile: profile.compatibilityProfile,
102
103
  runtimeKey: profile.runtimeKey,
@@ -27,7 +27,13 @@ export function applyRoutingInstructions(instructions, currentState) {
27
27
  : undefined,
28
28
  preCommandSource: currentState.preCommandSource,
29
29
  preCommandScriptPath: currentState.preCommandScriptPath,
30
- preCommandUpdatedAt: currentState.preCommandUpdatedAt
30
+ preCommandUpdatedAt: currentState.preCommandUpdatedAt,
31
+ chatProcessInputTokens: currentState.chatProcessInputTokens,
32
+ chatProcessMessageCount: currentState.chatProcessMessageCount,
33
+ chatProcessToolsSignature: currentState.chatProcessToolsSignature,
34
+ chatProcessParametersSignature: currentState.chatProcessParametersSignature,
35
+ chatProcessBoundarySignature: currentState.chatProcessBoundarySignature,
36
+ chatProcessUpdatedAt: currentState.chatProcessUpdatedAt
31
37
  };
32
38
  let allowReset = false;
33
39
  let disableReset = false;
@@ -185,6 +191,18 @@ export function serializeRoutingInstructionState(state) {
185
191
  provider,
186
192
  models: Array.from(models)
187
193
  })),
194
+ ...(typeof state.chatProcessInputTokens === 'number' && Number.isFinite(state.chatProcessInputTokens)
195
+ ? { chatProcessInputTokens: state.chatProcessInputTokens }
196
+ : {}),
197
+ ...(typeof state.chatProcessMessageCount === 'number' && Number.isFinite(state.chatProcessMessageCount)
198
+ ? { chatProcessMessageCount: state.chatProcessMessageCount }
199
+ : {}),
200
+ ...(state.chatProcessToolsSignature ? { chatProcessToolsSignature: state.chatProcessToolsSignature } : {}),
201
+ ...(state.chatProcessParametersSignature ? { chatProcessParametersSignature: state.chatProcessParametersSignature } : {}),
202
+ ...(state.chatProcessBoundarySignature ? { chatProcessBoundarySignature: state.chatProcessBoundarySignature } : {}),
203
+ ...(typeof state.chatProcessUpdatedAt === 'number' && Number.isFinite(state.chatProcessUpdatedAt)
204
+ ? { chatProcessUpdatedAt: state.chatProcessUpdatedAt }
205
+ : {}),
188
206
  ...serializeStopMessageState(state),
189
207
  ...serializePreCommandState(state)
190
208
  };
@@ -208,7 +226,13 @@ export function deserializeRoutingInstructionState(data) {
208
226
  stopMessageAiHistory: undefined,
209
227
  preCommandSource: undefined,
210
228
  preCommandScriptPath: undefined,
211
- preCommandUpdatedAt: undefined
229
+ preCommandUpdatedAt: undefined,
230
+ chatProcessInputTokens: undefined,
231
+ chatProcessMessageCount: undefined,
232
+ chatProcessToolsSignature: undefined,
233
+ chatProcessParametersSignature: undefined,
234
+ chatProcessBoundarySignature: undefined,
235
+ chatProcessUpdatedAt: undefined
212
236
  };
213
237
  if (data.forcedTarget && typeof data.forcedTarget === 'object') {
214
238
  state.forcedTarget = data.forcedTarget;
@@ -241,5 +265,23 @@ export function deserializeRoutingInstructionState(data) {
241
265
  }
242
266
  deserializeStopMessageState(data, state);
243
267
  deserializePreCommandState(data, state);
268
+ if (typeof data.chatProcessInputTokens === 'number' && Number.isFinite(data.chatProcessInputTokens)) {
269
+ state.chatProcessInputTokens = data.chatProcessInputTokens;
270
+ }
271
+ if (typeof data.chatProcessMessageCount === 'number' && Number.isFinite(data.chatProcessMessageCount)) {
272
+ state.chatProcessMessageCount = data.chatProcessMessageCount;
273
+ }
274
+ if (typeof data.chatProcessToolsSignature === 'string' && data.chatProcessToolsSignature.trim()) {
275
+ state.chatProcessToolsSignature = data.chatProcessToolsSignature.trim();
276
+ }
277
+ if (typeof data.chatProcessParametersSignature === 'string' && data.chatProcessParametersSignature.trim()) {
278
+ state.chatProcessParametersSignature = data.chatProcessParametersSignature.trim();
279
+ }
280
+ if (typeof data.chatProcessBoundarySignature === 'string' && data.chatProcessBoundarySignature.trim()) {
281
+ state.chatProcessBoundarySignature = data.chatProcessBoundarySignature.trim();
282
+ }
283
+ if (typeof data.chatProcessUpdatedAt === 'number' && Number.isFinite(data.chatProcessUpdatedAt)) {
284
+ state.chatProcessUpdatedAt = data.chatProcessUpdatedAt;
285
+ }
244
286
  return state;
245
287
  }
@@ -67,4 +67,10 @@ export interface RoutingInstructionState {
67
67
  preCommandSource?: string;
68
68
  preCommandScriptPath?: string;
69
69
  preCommandUpdatedAt?: number;
70
+ chatProcessInputTokens?: number;
71
+ chatProcessMessageCount?: number;
72
+ chatProcessToolsSignature?: string;
73
+ chatProcessParametersSignature?: string;
74
+ chatProcessBoundarySignature?: string;
75
+ chatProcessUpdatedAt?: number;
70
76
  }
@@ -1,5 +1,9 @@
1
1
  import { countRequestTokens } from './token-counter.js';
2
2
  export function computeRequestTokens(request, fallbackText) {
3
+ const metadataEstimate = readEstimatedTokensFromMetadata(request);
4
+ if (metadataEstimate !== undefined) {
5
+ return metadataEstimate;
6
+ }
3
7
  try {
4
8
  return countRequestTokens(request);
5
9
  }
@@ -7,6 +11,23 @@ export function computeRequestTokens(request, fallbackText) {
7
11
  return fallbackEstimateTokens(fallbackText, request.messages?.length ?? 0);
8
12
  }
9
13
  }
14
+ function readEstimatedTokensFromMetadata(request) {
15
+ const metadata = request?.metadata;
16
+ if (!metadata || typeof metadata !== 'object') {
17
+ return undefined;
18
+ }
19
+ const candidates = [
20
+ metadata.estimatedInputTokens,
21
+ metadata.estimatedTokens,
22
+ metadata.estimated_tokens
23
+ ];
24
+ for (const candidate of candidates) {
25
+ if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) {
26
+ return Math.max(1, Math.round(candidate));
27
+ }
28
+ }
29
+ return undefined;
30
+ }
10
31
  function fallbackEstimateTokens(text, messageCount) {
11
32
  if (!text) {
12
33
  return Math.max(32, Math.max(messageCount, 1) * 16);
@@ -17,6 +17,11 @@ export interface RoutePoolLoadBalancingPolicy {
17
17
  * Optional pool-local weights. Keys may target runtime keys, provider.model groups, or provider ids.
18
18
  */
19
19
  weights?: Record<string, number>;
20
+ responsesResume?: {
21
+ previousRequestId?: string;
22
+ restoredFromResponseId?: string;
23
+ [key: string]: unknown;
24
+ };
20
25
  }
21
26
  export interface RoutePoolTier {
22
27
  id: string;
@@ -69,6 +74,7 @@ export interface ProviderProfile {
69
74
  providerType: string;
70
75
  endpoint: string;
71
76
  auth: ProviderAuthConfig;
77
+ enabled?: boolean;
72
78
  outboundProfile: string;
73
79
  compatibilityProfile?: string;
74
80
  runtimeKey?: string;
@@ -95,6 +101,7 @@ export interface ProviderRuntimeProfile {
95
101
  endpoint: string;
96
102
  headers?: Record<string, string>;
97
103
  auth: ProviderAuthConfig;
104
+ enabled?: boolean;
98
105
  outboundProfile: string;
99
106
  compatibilityProfile?: string;
100
107
  modelId?: string;
@@ -189,7 +189,7 @@ function isEmptyClientResponsePayload(payload) {
189
189
  return false;
190
190
  }
191
191
  // OpenAI Responses: requires_action (function_call output) is a meaningful response and must not be
192
- // treated as "empty". Some auto-followup servertools (stop_message_flow / empty_reply_continue)
192
+ // treated as "empty". Some auto-followup servertools (for example stop_message_flow)
193
193
  // previously misclassified this as empty because there is no output_text/content yet.
194
194
  const requiredAction = payload.required_action;
195
195
  if (requiredAction && typeof requiredAction === 'object') {
@@ -314,7 +314,6 @@ export async function runServerToolOrchestration(options) {
314
314
  continue_execution_flow: 'continue_execution',
315
315
  review_flow: 'review',
316
316
  stop_message_flow: 'stop_message_auto',
317
- empty_reply_continue: 'empty_reply_continue',
318
317
  apply_patch_guard: 'apply_patch_guard',
319
318
  exec_command_guard: 'exec_command_guard',
320
319
  iflow_model_error_retry: 'iflow_model_error_retry',
@@ -636,11 +635,10 @@ export async function runServerToolOrchestration(options) {
636
635
  const isClockHoldFlow = engineResult.execution.flowId === 'clock_hold_flow';
637
636
  const isContinueExecutionFlow = engineResult.execution.flowId === 'continue_execution_flow';
638
637
  const isReviewFlow = engineResult.execution.flowId === 'review_flow';
639
- const isEmptyReplyContinue = engineResult.execution.flowId === 'empty_reply_continue';
640
638
  const isApplyPatchGuard = engineResult.execution.flowId === 'apply_patch_guard';
641
639
  const isExecCommandGuard = engineResult.execution.flowId === 'exec_command_guard';
642
640
  const isErrorAutoFlow = engineResult.execution.flowId === 'iflow_model_error_retry';
643
- const applyAutoLimit = isErrorAutoFlow || isEmptyReplyContinue || isApplyPatchGuard || isExecCommandGuard;
641
+ const applyAutoLimit = isErrorAutoFlow || isApplyPatchGuard || isExecCommandGuard;
644
642
  // ServerTool followups must not inherit or inject any routeHint; always route fresh.
645
643
  const preserveRouteHint = false;
646
644
  const followupPlan = engineResult.execution.followup;
@@ -757,8 +755,6 @@ export async function runServerToolOrchestration(options) {
757
755
  // - do not inherit sticky target
758
756
  // - record original entry endpoint for downstream formatting/debug
759
757
  rt.preserveRouteHint = preserveRouteHint;
760
- // Use empty string (falsy) to avoid VirtualRouter calling `.trim()` on non-string values.
761
- metadata.routeHint = '';
762
758
  rt.disableStickyRoutes = true;
763
759
  rt.serverToolOriginalEntryEndpoint =
764
760
  (typeof options.entryEndpoint === 'string' && options.entryEndpoint.trim().length
@@ -774,7 +770,7 @@ export async function runServerToolOrchestration(options) {
774
770
  metadata.__shadowCompareForcedProviderKey = providerKey;
775
771
  }
776
772
  }
777
- const retryEmptyFollowupOnce = isStopMessageFlow || isEmptyReplyContinue;
773
+ const retryEmptyFollowupOnce = isStopMessageFlow;
778
774
  const maxAttempts = retryEmptyFollowupOnce ? 2 : 1;
779
775
  const followupRequestId = buildFollowupRequestId(options.requestId, engineResult.execution.followup.requestIdSuffix);
780
776
  const clientInjectOnlyRaw = metadata.clientInjectOnly;
@@ -1072,7 +1068,6 @@ export async function runServerToolOrchestration(options) {
1072
1068
  replayRt.serverToolLoopState = replayLoopState;
1073
1069
  }
1074
1070
  replayMetadata.__hubEntry = 'chat_process';
1075
- replayMetadata.routeHint = '';
1076
1071
  replayRt.preserveRouteHint = false;
1077
1072
  replayRt.disableStickyRoutes = true;
1078
1073
  replayRt.serverToolOriginalEntryEndpoint =
@@ -1374,17 +1369,6 @@ function resolveCapturedChatRequest(adapterContext) {
1374
1369
  if (direct && typeof direct === 'object' && !Array.isArray(direct)) {
1375
1370
  return direct;
1376
1371
  }
1377
- const rt = readRuntimeMetadata(record);
1378
- const runtimeCaptured = rt && typeof rt === 'object' && !Array.isArray(rt)
1379
- ? rt.capturedChatRequest
1380
- : undefined;
1381
- if (runtimeCaptured && typeof runtimeCaptured === 'object' && !Array.isArray(runtimeCaptured)) {
1382
- return runtimeCaptured;
1383
- }
1384
- const originalRequest = record.originalRequest;
1385
- if (originalRequest && typeof originalRequest === 'object' && !Array.isArray(originalRequest)) {
1386
- return originalRequest;
1387
- }
1388
1372
  return null;
1389
1373
  }
1390
1374
  function buildStopMessageLoopPayload(adapterContext) {
@@ -1546,21 +1530,6 @@ function buildFollowupRequestId(baseRequestId, suffix) {
1546
1530
  }
1547
1531
  return normalized.endsWith(suffixText) ? normalized : `${normalized}${suffixText}`;
1548
1532
  }
1549
- function getStopMessageSource(adapterContext) {
1550
- if (!adapterContext || typeof adapterContext !== 'object') {
1551
- return undefined;
1552
- }
1553
- const rt = readRuntimeMetadata(adapterContext);
1554
- const raw = rt?.stopMessageState;
1555
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1556
- return undefined;
1557
- }
1558
- const record = raw;
1559
- const source = typeof record.stopMessageSource === 'string' && record.stopMessageSource.trim()
1560
- ? record.stopMessageSource.trim()
1561
- : '';
1562
- return source || undefined;
1563
- }
1564
1533
  function isAdapterClientDisconnected(adapterContext) {
1565
1534
  if (!adapterContext || typeof adapterContext !== 'object') {
1566
1535
  return false;
@@ -1,5 +1,4 @@
1
1
  import { buildChatRequestFromResponses, captureResponsesContext } from '../../conversion/responses/responses-openai-bridge.js';
2
- import { stripHistoricalImageAttachments } from '../../conversion/hub/process/chat-process-media.js';
3
2
  import { cloneJson } from '../server-side-tools.js';
4
3
  import { trimOpenAiMessagesForFollowup } from './followup-message-trimmer.js';
5
4
  function extractResponsesTopLevelParameters(record) {
@@ -423,11 +422,6 @@ export function buildServerToolFollowupChatPayloadFromInjection(args) {
423
422
  return null;
424
423
  }
425
424
  let messages = Array.isArray(seed.messages) ? cloneJson(seed.messages) : [];
426
- // ServerTool followups must enter marker/routing/chat-process analysis with the same
427
- // historical-media invariants as normal chat-process requests:
428
- // only the latest live user turn may keep inline image payloads; earlier user turns
429
- // are scrubbed to placeholders before any followup ops append new assistant/user items.
430
- messages = stripHistoricalImageAttachments(messages);
431
425
  const ops = Array.isArray(args.injection?.ops) ? args.injection.ops : [];
432
426
  // Followup is a normal request hop: inherit tool schema from the captured request and
433
427
  // let compat/tool-governance apply standard sanitization rules.
@@ -1,274 +1,3 @@
1
- import { registerServerToolHandler } from '../registry.js';
2
- import { isCompactionRequest } from './compaction-detect.js';
3
- import { extractCapturedChatSeed } from './followup-request-builder.js';
4
- import { ensureRuntimeMetadata, readRuntimeMetadata } from '../../conversion/runtime-metadata.js';
5
- const FLOW_ID = 'empty_reply_continue';
6
- const MAX_TOOL_HINTS = 24;
7
- const handler = async (ctx) => {
8
- if (!ctx.capabilities.reenterPipeline) {
9
- return null;
10
- }
11
- // 避免在 followup 请求里再次触发,防止循环。
12
- const rt = readRuntimeMetadata(ctx.adapterContext);
13
- const followupRaw = rt?.serverToolFollowup;
14
- if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
15
- return null;
16
- }
17
- if (hasCompactionFlag(rt)) {
18
- return null;
19
- }
20
- // 通用空回复续跑:只要是 /v1/responses 链路且满足空回复判定,不区分 provider family / protocol。
21
- const entryEndpoint = (ctx.entryEndpoint || '').toLowerCase();
22
- if (!entryEndpoint.includes('/v1/responses')) {
23
- return null;
24
- }
25
- // 支持两种客户端协议形状:
26
- // - OpenAI Chat: choices[0].message.content
27
- // - OpenAI Responses: output/output_text/status
28
- const base = ctx.base;
29
- const emptyDecision = decideEmptyReply(base);
30
- if (!emptyDecision.shouldTrigger) {
31
- return null;
32
- }
33
- // 统计连续空回复次数,超过上限后不再自动续写,而是返回一个可重试错误。
34
- const previousCountRaw = rt?.emptyReplyContinueCount ?? rt?.geminiEmptyReplyCount;
35
- const previousCount = typeof previousCountRaw === 'number' && Number.isFinite(previousCountRaw) && previousCountRaw >= 0
36
- ? previousCountRaw
37
- : 0;
38
- const nextCount = previousCount + 1;
39
- const captured = getCapturedRequest(ctx.adapterContext);
40
- if (!captured) {
41
- return null;
42
- }
43
- if (isCompactionRequest(captured)) {
44
- return null;
45
- }
46
- const seed = extractCapturedChatSeed(captured);
47
- if (!seed) {
48
- return null;
49
- }
50
- const continueText = buildContinueUserText(seed);
51
- // 超过最多 3 次空回复:返回一个 HTTP_HANDLER_ERROR 形状的错误,交由上层错误中心处理。
52
- if (nextCount > 3) {
53
- const errorChat = {
54
- id: base.id,
55
- object: base.object,
56
- model: base.model,
57
- error: {
58
- message: 'fetch failed: empty_reply_continue exceeded max empty replies',
59
- code: 'HTTP_HANDLER_ERROR',
60
- type: 'servertool_empty_reply'
61
- }
62
- };
63
- return {
64
- flowId: FLOW_ID,
65
- finalize: async () => ({
66
- chatResponse: errorChat,
67
- execution: {
68
- flowId: FLOW_ID
69
- }
70
- })
71
- };
72
- }
73
- return {
74
- flowId: FLOW_ID,
75
- finalize: async () => ({
76
- chatResponse: ctx.base,
77
- execution: {
78
- flowId: FLOW_ID,
79
- followup: {
80
- requestIdSuffix: ':continue',
81
- entryEndpoint: ctx.entryEndpoint,
82
- injection: {
83
- ops: [
84
- { op: 'preserve_tools' },
85
- { op: 'append_assistant_message', required: false },
86
- { op: 'append_user_text', text: continueText }
87
- ]
88
- },
89
- metadata: (() => {
90
- const meta = {};
91
- const runtime = ensureRuntimeMetadata(meta);
92
- runtime.emptyReplyContinueCount = nextCount;
93
- // Backward compatibility for old snapshots/guards.
94
- runtime.geminiEmptyReplyCount = nextCount;
95
- return meta;
96
- })()
97
- }
98
- }
99
- })
100
- };
101
- };
102
- registerServerToolHandler('empty_reply_continue', handler, { trigger: 'auto', hook: { phase: 'default', priority: 20 } });
103
- function decideEmptyReply(base) {
104
- // 1) OpenAI Chat shape
105
- const choices = Array.isArray(base.choices) ? base.choices : [];
106
- if (choices.length > 0) {
107
- const firstRaw = choices[0];
108
- if (!firstRaw || typeof firstRaw !== 'object') {
109
- return { shouldTrigger: false };
110
- }
111
- const first = firstRaw;
112
- const finishReasonRaw = typeof first.finish_reason === 'string' && first.finish_reason.trim()
113
- ? first.finish_reason.trim()
114
- : '';
115
- const finishReason = finishReasonRaw.toLowerCase();
116
- const isStop = finishReason === 'stop';
117
- const isMaxTokens = finishReason === 'length'; // 映射自 Gemini 的 MAX_TOKENS
118
- if (!isStop && !isMaxTokens) {
119
- return { shouldTrigger: false };
120
- }
121
- const message = first.message && typeof first.message === 'object' && !Array.isArray(first.message)
122
- ? first.message
123
- : null;
124
- if (!message) {
125
- return { shouldTrigger: false };
126
- }
127
- const contentRaw = message.content;
128
- const contentText = typeof contentRaw === 'string' ? contentRaw.trim() : '';
129
- // 对于 finish_reason=stop,仅在真正“空回复”时触发;
130
- // 对于 finish_reason=length(MAX_TOKENS 截断),允许已有内容,视为需要自动续写。
131
- if (isStop && contentText.length > 0) {
132
- return { shouldTrigger: false };
133
- }
134
- const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
135
- if (toolCalls.length > 0) {
136
- return { shouldTrigger: false };
137
- }
138
- return { shouldTrigger: true };
139
- }
140
- // 2) OpenAI Responses shape
141
- const statusRaw = typeof base.status === 'string' ? base.status.trim().toLowerCase() : '';
142
- if (statusRaw && statusRaw !== 'completed') {
143
- return { shouldTrigger: false };
144
- }
145
- if (base.required_action && typeof base.required_action === 'object') {
146
- return { shouldTrigger: false };
147
- }
148
- const outputText = extractResponsesOutputText(base);
149
- if (outputText.length > 0) {
150
- return { shouldTrigger: false };
151
- }
152
- const outputRaw = Array.isArray(base.output) ? base.output : [];
153
- if (outputRaw.some((item) => hasToolLikeOutput(item))) {
154
- return { shouldTrigger: false };
155
- }
156
- // 允许 output 为空或仅包含空消息:视作空回复,触发自动续写。
157
- return { shouldTrigger: true };
158
- }
159
- function extractResponsesOutputText(base) {
160
- const raw = base.output_text;
161
- if (typeof raw === 'string') {
162
- return raw.trim();
163
- }
164
- if (Array.isArray(raw)) {
165
- const texts = raw
166
- .map((entry) => (typeof entry === 'string' ? entry : ''))
167
- .filter((entry) => entry.trim().length > 0);
168
- if (texts.length > 0) {
169
- return texts.join('\n').trim();
170
- }
171
- }
172
- const output = Array.isArray(base.output) ? (base.output) : [];
173
- const chunks = [];
174
- for (const item of output) {
175
- if (!item || typeof item !== 'object' || Array.isArray(item))
176
- continue;
177
- if (typeof item.type !== 'string')
178
- continue;
179
- const type = String(item.type).trim().toLowerCase();
180
- if (type !== 'message')
181
- continue;
182
- const content = Array.isArray(item.content) ? (item.content) : [];
183
- for (const part of content) {
184
- if (!part || typeof part !== 'object' || Array.isArray(part))
185
- continue;
186
- const pType = typeof part.type === 'string'
187
- ? String(part.type).trim().toLowerCase()
188
- : '';
189
- if (pType === 'output_text' || pType === 'text' || pType === 'input_text') {
190
- const text = typeof part.text === 'string' ? String(part.text) : '';
191
- if (text.trim().length)
192
- chunks.push(text.trim());
193
- continue;
194
- }
195
- const fallback = typeof part.content === 'string'
196
- ? String(part.content).trim()
197
- : '';
198
- if (fallback.length)
199
- chunks.push(fallback);
200
- }
201
- }
202
- return chunks.join('\n').trim();
203
- }
204
- function hasToolLikeOutput(value) {
205
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
206
- return false;
207
- }
208
- const typeRaw = value.type;
209
- const type = typeof typeRaw === 'string' ? typeRaw.trim().toLowerCase() : '';
210
- if (!type) {
211
- return false;
212
- }
213
- return (type === 'tool_call' ||
214
- type === 'tool_use' ||
215
- type === 'function_call' ||
216
- type.includes('tool'));
217
- }
218
- function getCapturedRequest(adapterContext) {
219
- if (!adapterContext || typeof adapterContext !== 'object') {
220
- return null;
221
- }
222
- const captured = adapterContext.capturedChatRequest;
223
- if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
224
- return null;
225
- }
226
- return captured;
227
- }
228
- function hasCompactionFlag(rt) {
229
- const flag = rt && typeof rt === 'object' && !Array.isArray(rt) ? rt.compactionRequest : undefined;
230
- if (flag === true) {
231
- return true;
232
- }
233
- if (typeof flag === 'string' && flag.trim().toLowerCase() === 'true') {
234
- return true;
235
- }
236
- return false;
237
- }
238
- function buildContinueUserText(seed) {
239
- const toolNames = collectToolNames(seed);
240
- if (!toolNames.length) {
241
- return '继续执行';
242
- }
243
- const shown = toolNames.slice(0, MAX_TOOL_HINTS);
244
- const omitted = toolNames.length - shown.length;
245
- const listText = shown.join(', ');
246
- const tail = omitted > 0 ? `(其余 ${omitted} 个省略)` : '';
247
- return `继续执行。可用工具列表:${listText}${tail}。请优先调用这些工具,不要返回空回复。`;
248
- }
249
- function collectToolNames(seed) {
250
- const tools = Array.isArray(seed.tools) ? seed.tools : [];
251
- if (!tools.length) {
252
- return [];
253
- }
254
- const names = [];
255
- const seen = new Set();
256
- for (const tool of tools) {
257
- if (!tool || typeof tool !== 'object' || Array.isArray(tool))
258
- continue;
259
- const fnNode = tool.function;
260
- const fnName = fnNode && typeof fnNode === 'object' && !Array.isArray(fnNode)
261
- ? normalizeToolName(fnNode.name)
262
- : '';
263
- const fallbackName = normalizeToolName(tool.name);
264
- const resolvedName = fnName || fallbackName;
265
- if (!resolvedName || seen.has(resolvedName))
266
- continue;
267
- seen.add(resolvedName);
268
- names.push(resolvedName);
269
- }
270
- return names;
271
- }
272
- function normalizeToolName(value) {
273
- return typeof value === 'string' && value.trim().length > 0 ? value.trim() : '';
274
- }
1
+ // empty_reply_continue has been retired intentionally.
2
+ // This file remains as a tombstone so stale imports fail closed.
3
+ export {};
@@ -7,19 +7,16 @@ export declare function resolveStickyKey(record: {
7
7
  sessionId?: unknown;
8
8
  conversationId?: unknown;
9
9
  metadata?: unknown;
10
- originalRequest?: unknown;
11
10
  [key: string]: unknown;
12
11
  }, runtimeMetadata?: unknown): string | undefined;
13
12
  export declare function persistStopMessageState(stickyKey: string | undefined, state: RoutingInstructionState): void;
14
13
  export declare function resolveStopMessageSessionScope(record: {
15
14
  sessionId?: unknown;
16
15
  metadata?: unknown;
17
- originalRequest?: unknown;
18
16
  [key: string]: unknown;
19
17
  }, runtimeMetadata?: unknown): string | undefined;
20
18
  export declare function resolveBdWorkingDirectoryForRecord(record: {
21
19
  metadata?: unknown;
22
- originalRequest?: unknown;
23
20
  [key: string]: unknown;
24
21
  }, runtimeMetadata: unknown): string | undefined;
25
22
  export declare function readServerToolFollowupFlowId(runtimeMetadata: unknown): string;