@jsonstudio/llms 0.6.954 → 0.6.1164

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 (130) 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 +54 -5
  51. package/dist/router/virtual-router/engine-selection.js +132 -42
  52. package/dist/router/virtual-router/engine.d.ts +3 -0
  53. package/dist/router/virtual-router/engine.js +142 -33
  54. package/dist/router/virtual-router/health-weighted.d.ts +25 -0
  55. package/dist/router/virtual-router/health-weighted.js +63 -0
  56. package/dist/router/virtual-router/load-balancer.d.ts +2 -0
  57. package/dist/router/virtual-router/load-balancer.js +45 -16
  58. package/dist/router/virtual-router/routing-instructions.js +17 -1
  59. package/dist/router/virtual-router/sticky-session-store.js +136 -24
  60. package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
  61. package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
  62. package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
  63. package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
  64. package/dist/router/virtual-router/types.d.ts +70 -0
  65. package/dist/servertool/clock/config.d.ts +7 -0
  66. package/dist/servertool/clock/config.js +27 -0
  67. package/dist/servertool/clock/daemon.d.ts +3 -0
  68. package/dist/servertool/clock/daemon.js +79 -0
  69. package/dist/servertool/clock/io.d.ts +2 -0
  70. package/dist/servertool/clock/io.js +13 -0
  71. package/dist/servertool/clock/paths.d.ts +4 -0
  72. package/dist/servertool/clock/paths.js +25 -0
  73. package/dist/servertool/clock/session-store.d.ts +3 -0
  74. package/dist/servertool/clock/session-store.js +56 -0
  75. package/dist/servertool/clock/state.d.ts +5 -0
  76. package/dist/servertool/clock/state.js +62 -0
  77. package/dist/servertool/clock/task-store.d.ts +5 -0
  78. package/dist/servertool/clock/task-store.js +4 -0
  79. package/dist/servertool/clock/tasks.d.ts +17 -0
  80. package/dist/servertool/clock/tasks.js +221 -0
  81. package/dist/servertool/clock/types.d.ts +36 -0
  82. package/dist/servertool/clock/types.js +1 -0
  83. package/dist/servertool/engine.d.ts +2 -0
  84. package/dist/servertool/engine.js +161 -7
  85. package/dist/servertool/followup-shadow.d.ts +16 -0
  86. package/dist/servertool/followup-shadow.js +145 -0
  87. package/dist/servertool/handlers/apply-patch-guard.js +1 -265
  88. package/dist/servertool/handlers/clock-auto.d.ts +1 -0
  89. package/dist/servertool/handlers/clock-auto.js +160 -0
  90. package/dist/servertool/handlers/clock.d.ts +1 -0
  91. package/dist/servertool/handlers/clock.js +197 -0
  92. package/dist/servertool/handlers/exec-command-guard.js +7 -555
  93. package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
  94. package/dist/servertool/handlers/followup-request-builder.js +248 -28
  95. package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
  96. package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
  97. package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
  98. package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
  99. package/dist/servertool/handlers/stop-message-auto.js +47 -175
  100. package/dist/servertool/handlers/vision.d.ts +7 -1
  101. package/dist/servertool/handlers/vision.js +61 -117
  102. package/dist/servertool/handlers/web-search.d.ts +7 -1
  103. package/dist/servertool/handlers/web-search.js +122 -105
  104. package/dist/servertool/reenter-backend.d.ts +23 -0
  105. package/dist/servertool/reenter-backend.js +18 -0
  106. package/dist/servertool/server-side-tools.d.ts +3 -2
  107. package/dist/servertool/server-side-tools.js +64 -10
  108. package/dist/servertool/types.d.ts +92 -3
  109. package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
  110. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
  111. package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
  112. package/dist/sse/shared/writer.js +24 -7
  113. package/dist/tools/apply-patch/execution-capturer.js +3 -1
  114. package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
  115. package/dist/tools/apply-patch/json/parse-loose.js +139 -0
  116. package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
  117. package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
  118. package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
  119. package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
  120. package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
  121. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
  122. package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
  123. package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
  124. package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
  125. package/dist/tools/apply-patch/structured/coercion.js +82 -0
  126. package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
  127. package/dist/tools/apply-patch/validation/shared.js +6 -0
  128. package/dist/tools/apply-patch/validator.d.ts +2 -2
  129. package/dist/tools/apply-patch/validator.js +6 -556
  130. package/package.json +1 -1
@@ -4,6 +4,7 @@ import type { JsonObject } from '../types/json.js';
4
4
  import type { VirtualRouterConfig, RoutingDecision, RoutingDiagnostics, TargetMetadata, VirtualRouterHealthStore, ProviderQuotaView } from '../../../router/virtual-router/types.js';
5
5
  import { type HubProcessNodeResult } from '../process/chat-process.js';
6
6
  import { type HubPolicyConfig } from '../policy/policy-engine.js';
7
+ import { type HubToolSurfaceConfig } from '../tool-surface/tool-surface-engine.js';
7
8
  export interface HubPipelineConfig {
8
9
  virtualRouter: VirtualRouterConfig;
9
10
  /**
@@ -11,6 +12,12 @@ export interface HubPipelineConfig {
11
12
  * Must remain config-driven and default to off.
12
13
  */
13
14
  policy?: HubPolicyConfig;
15
+ /**
16
+ * Optional: tool surface rollout controls.
17
+ * - shadow: compute & record diffs, do not modify outbound payload
18
+ * - enforce: rewrite outbound payload to match canonical tool surface
19
+ */
20
+ toolSurface?: HubToolSurfaceConfig;
14
21
  /**
15
22
  * 可选:供 VirtualRouterEngine 使用的健康状态持久化存储。
16
23
  * 当提供时,VirtualRouterEngine 将在初始化时恢复上一次快照,并在 cooldown/熔断变化时调用 persistSnapshot。
@@ -79,6 +86,8 @@ export declare class HubPipeline {
79
86
  dispose(): void;
80
87
  private executeRequestStagePipeline;
81
88
  private resolveClientProtocol;
89
+ private coerceStandardizedRequestFromPayload;
90
+ private executeChatProcessEntryPipeline;
82
91
  execute(request: HubPipelineRequest): Promise<HubPipelineResult>;
83
92
  private captureAnthropicAliasMap;
84
93
  private shouldCaptureAnthropicAlias;
@@ -26,6 +26,54 @@ import { extractSessionIdentifiersFromMetadata } from './session-identifiers.js'
26
26
  import { computeRequestTokens } from '../../../router/virtual-router/token-estimator.js';
27
27
  import { isCompactionRequest } from '../../shared/compaction-detect.js';
28
28
  import { applyHubProviderOutboundPolicy, recordHubPolicyObservation, setHubPolicyRuntimePolicy } from '../policy/policy-engine.js';
29
+ import { applyProviderOutboundToolSurface } from '../tool-surface/tool-surface-engine.js';
30
+ import { applyHubOperations } from '../ops/operations.js';
31
+ function isTruthyEnv(value) {
32
+ const v = typeof value === 'string' ? value.trim().toLowerCase() : '';
33
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
34
+ }
35
+ function resolveApplyPatchToolModeFromEnv() {
36
+ const rawMode = String(process.env.RCC_APPLY_PATCH_TOOL_MODE || process.env.ROUTECODEX_APPLY_PATCH_TOOL_MODE || '')
37
+ .trim()
38
+ .toLowerCase();
39
+ if (rawMode === 'freeform')
40
+ return 'freeform';
41
+ if (rawMode === 'schema' || rawMode === 'json_schema')
42
+ return 'schema';
43
+ const freeformFlag = process.env.RCC_APPLY_PATCH_FREEFORM || process.env.ROUTECODEX_APPLY_PATCH_FREEFORM;
44
+ if (isTruthyEnv(freeformFlag))
45
+ return 'freeform';
46
+ return undefined;
47
+ }
48
+ function resolveApplyPatchToolModeFromTools(toolsRaw) {
49
+ if (!Array.isArray(toolsRaw) || toolsRaw.length === 0) {
50
+ return undefined;
51
+ }
52
+ for (const entry of toolsRaw) {
53
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
54
+ continue;
55
+ const record = entry;
56
+ const type = typeof record.type === 'string' ? record.type.trim().toLowerCase() : '';
57
+ if (type && type !== 'function')
58
+ continue;
59
+ const fn = record.function && typeof record.function === 'object' && !Array.isArray(record.function)
60
+ ? record.function
61
+ : undefined;
62
+ const name = typeof fn?.name === 'string' ? fn.name.trim().toLowerCase() : '';
63
+ if (name !== 'apply_patch')
64
+ continue;
65
+ const format = typeof record.format === 'string'
66
+ ? record.format.trim().toLowerCase()
67
+ : typeof fn?.format === 'string'
68
+ ? String(fn.format).trim().toLowerCase()
69
+ : '';
70
+ if (format === 'freeform')
71
+ return 'freeform';
72
+ // If apply_patch is present without explicit freeform marker, default to schema mode.
73
+ return 'schema';
74
+ }
75
+ return undefined;
76
+ }
29
77
  function extractHubPolicyOverride(metadata) {
30
78
  const raw = metadata ? metadata.__hubPolicyOverride : undefined;
31
79
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
@@ -42,6 +90,22 @@ function extractHubPolicyOverride(metadata) {
42
90
  ...(sampleRate !== undefined ? { sampleRate } : {})
43
91
  };
44
92
  }
93
+ function extractHubShadowCompareConfig(metadata) {
94
+ const raw = metadata ? metadata.__hubShadowCompare : undefined;
95
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
96
+ return undefined;
97
+ }
98
+ const obj = raw;
99
+ const modeCandidate = typeof obj.baselineMode === 'string'
100
+ ? obj.baselineMode.trim().toLowerCase()
101
+ : typeof obj.mode === 'string'
102
+ ? obj.mode.trim().toLowerCase()
103
+ : '';
104
+ if (modeCandidate !== 'off' && modeCandidate !== 'observe' && modeCandidate !== 'enforce') {
105
+ return undefined;
106
+ }
107
+ return { baselineMode: modeCandidate };
108
+ }
45
109
  export class HubPipeline {
46
110
  routerEngine;
47
111
  config;
@@ -120,6 +184,11 @@ export class HubPipeline {
120
184
  // not the governed/augmented tools.
121
185
  try {
122
186
  const toolsRaw = Array.isArray(rawRequest?.tools) ? rawRequest.tools : null;
187
+ const applyPatchToolMode = resolveApplyPatchToolModeFromEnv() ?? resolveApplyPatchToolModeFromTools(toolsRaw);
188
+ if (applyPatchToolMode) {
189
+ normalized.metadata = normalized.metadata || {};
190
+ normalized.metadata.applyPatchToolMode = applyPatchToolMode;
191
+ }
123
192
  if (toolsRaw && toolsRaw.length > 0) {
124
193
  normalized.metadata = normalized.metadata || {};
125
194
  normalized.metadata.clientToolsRaw = jsonClone(toolsRaw);
@@ -133,6 +202,7 @@ export class HubPipeline {
133
202
  normalized.metadata.compactionRequest = true;
134
203
  }
135
204
  const effectivePolicy = normalized.policyOverride ?? this.config.policy;
205
+ const shadowCompareBaselineMode = normalized.shadowCompare?.baselineMode;
136
206
  const inboundAdapterContext = this.buildAdapterContext(normalized);
137
207
  const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint, {
138
208
  disableSnapshots: normalized.disableSnapshots === true
@@ -166,6 +236,15 @@ export class HubPipeline {
166
236
  stageRecorder: inboundRecorder
167
237
  });
168
238
  const standardizedRequest = inboundStage2.standardizedRequest;
239
+ try {
240
+ const mode = String(normalized.metadata?.applyPatchToolMode || '').trim().toLowerCase();
241
+ if (mode === 'freeform' || mode === 'schema') {
242
+ standardizedRequest.metadata.applyPatchToolMode = mode;
243
+ }
244
+ }
245
+ catch {
246
+ // best-effort: do not block request handling due to metadata propagation failures
247
+ }
169
248
  const inboundEnd = Date.now();
170
249
  const nodeResults = [];
171
250
  nodeResults.push({
@@ -195,6 +274,10 @@ export class HubPipeline {
195
274
  if (execCommandGuard) {
196
275
  metaBase.execCommandGuard = execCommandGuard;
197
276
  }
277
+ const clockConfig = this.config.virtualRouter?.clock;
278
+ if (clockConfig) {
279
+ metaBase.clock = clockConfig;
280
+ }
198
281
  normalized.metadata = metaBase;
199
282
  let processedRequest;
200
283
  if (normalized.processMode !== 'passthrough') {
@@ -207,11 +290,22 @@ export class HubPipeline {
207
290
  stageRecorder: inboundRecorder
208
291
  });
209
292
  processedRequest = processResult.processedRequest;
293
+ // Surface request-side clock reservation into pipeline metadata so response conversion
294
+ // can commit delivery only after a successful response is produced.
295
+ try {
296
+ const reservation = processedRequest?.metadata?.__clockReservation;
297
+ if (reservation && typeof reservation === 'object') {
298
+ metaBase.__clockReservation = reservation;
299
+ }
300
+ }
301
+ catch {
302
+ // best-effort: do not block request handling due to metadata propagation failures
303
+ }
210
304
  if (processResult.nodeResult) {
211
305
  nodeResults.push(this.convertProcessNodeResult('req_process_stage1_tool_governance', processResult.nodeResult));
212
306
  }
213
307
  }
214
- const workingRequest = processedRequest ?? standardizedRequest;
308
+ let workingRequest = processedRequest ?? standardizedRequest;
215
309
  // 使用与 VirtualRouter 一致的 tiktoken 计数逻辑,对标准化请求进行一次
216
310
  // 上下文 token 估算,供后续 usage 归一化与统计使用。
217
311
  try {
@@ -267,6 +361,12 @@ export class HubPipeline {
267
361
  const stopMessageState = this.routerEngine.getStopMessageState(metadataInput);
268
362
  if (stopMessageState && normalized.metadata && typeof normalized.metadata === 'object') {
269
363
  normalized.metadata.stopMessageState = stopMessageState;
364
+ // Phase 4: model stopMessage as an operation-derived request metadata signal.
365
+ // Response-side servertools can read it from AdapterContext, while request-side
366
+ // diagnostics can read it from StandardizedRequest.metadata.
367
+ workingRequest = applyHubOperations(workingRequest, [
368
+ { op: 'set_request_metadata_fields', fields: { stopMessageState } }
369
+ ]);
270
370
  }
271
371
  // Emit virtual router hit log for debugging (orange [virtual-router] ...)
272
372
  try {
@@ -282,7 +382,7 @@ export class HubPipeline {
282
382
  // logging must not break routing
283
383
  }
284
384
  const outboundStream = this.resolveOutboundStreamIntent(routing.target?.streaming);
285
- this.applyOutboundStreamPreference(workingRequest, outboundStream);
385
+ workingRequest = this.applyOutboundStreamPreference(workingRequest, outboundStream);
286
386
  const outboundAdapterContext = this.buildAdapterContext(normalized, routing.target);
287
387
  if (routing.target?.compatibilityProfile) {
288
388
  outboundAdapterContext.compatibilityProfile = routing.target.compatibilityProfile;
@@ -324,6 +424,43 @@ export class HubPipeline {
324
424
  adapterContext: outboundAdapterContext,
325
425
  stageRecorder: outboundRecorder
326
426
  });
427
+ let shadowBaselineProviderPayload;
428
+ if (shadowCompareBaselineMode) {
429
+ const baselinePolicy = {
430
+ ...(effectivePolicy ?? {}),
431
+ mode: shadowCompareBaselineMode
432
+ };
433
+ // Compute a baseline provider payload in the *same execution*, without recording
434
+ // snapshots/diffs and without re-running the full pipeline. This avoids side effects
435
+ // (conversation store, followup captures, etc.) that a second execute() would trigger.
436
+ const baselineFormatted = typeof globalThis.structuredClone === 'function'
437
+ ? globalThis.structuredClone(formattedPayload)
438
+ : jsonClone(formattedPayload);
439
+ let baselinePayload = applyHubProviderOutboundPolicy({
440
+ policy: baselinePolicy,
441
+ providerProtocol: outboundProtocol,
442
+ payload: baselineFormatted,
443
+ stageRecorder: undefined,
444
+ requestId: normalized.id
445
+ });
446
+ baselinePayload = applyProviderOutboundToolSurface({
447
+ config: this.config.toolSurface,
448
+ providerProtocol: outboundProtocol,
449
+ payload: baselinePayload,
450
+ stageRecorder: undefined,
451
+ requestId: normalized.id
452
+ });
453
+ shadowBaselineProviderPayload = baselinePayload;
454
+ }
455
+ // Phase 0/1: observe provider outbound payload violations before any enforcement rewrites.
456
+ // This provides black-box visibility into what the pipeline would have sent upstream.
457
+ recordHubPolicyObservation({
458
+ policy: effectivePolicy,
459
+ providerProtocol: outboundProtocol,
460
+ payload: formattedPayload,
461
+ stageRecorder: outboundRecorder,
462
+ requestId: normalized.id
463
+ });
327
464
  providerPayload = applyHubProviderOutboundPolicy({
328
465
  policy: effectivePolicy,
329
466
  providerProtocol: outboundProtocol,
@@ -331,6 +468,13 @@ export class HubPipeline {
331
468
  stageRecorder: outboundRecorder,
332
469
  requestId: normalized.id
333
470
  });
471
+ providerPayload = applyProviderOutboundToolSurface({
472
+ config: this.config.toolSurface,
473
+ providerProtocol: outboundProtocol,
474
+ payload: providerPayload,
475
+ stageRecorder: outboundRecorder,
476
+ requestId: normalized.id
477
+ });
334
478
  recordHubPolicyObservation({
335
479
  policy: effectivePolicy,
336
480
  providerProtocol: outboundProtocol,
@@ -387,7 +531,17 @@ export class HubPipeline {
387
531
  processMode: normalized.processMode,
388
532
  routeHint: normalized.routeHint,
389
533
  target: routing.target,
390
- ...(typeof outboundStream === 'boolean' ? { providerStream: outboundStream } : {})
534
+ ...(typeof outboundStream === 'boolean' ? { providerStream: outboundStream } : {}),
535
+ ...(shadowBaselineProviderPayload
536
+ ? {
537
+ hubShadowCompare: {
538
+ baselineMode: shadowCompareBaselineMode,
539
+ candidateMode: (effectivePolicy?.mode ?? 'off'),
540
+ providerProtocol: outboundProtocol,
541
+ baselineProviderPayload: shadowBaselineProviderPayload
542
+ }
543
+ }
544
+ : {})
391
545
  };
392
546
  return {
393
547
  requestId: normalized.id,
@@ -409,8 +563,310 @@ export class HubPipeline {
409
563
  return 'anthropic-messages';
410
564
  return 'openai-chat';
411
565
  }
566
+ coerceStandardizedRequestFromPayload(payload, normalized) {
567
+ const model = typeof payload.model === 'string' && payload.model.trim().length ? payload.model.trim() : '';
568
+ if (!model) {
569
+ throw new Error('[HubPipeline] outbound stage requires payload.model');
570
+ }
571
+ const messages = Array.isArray(payload.messages) ? payload.messages : null;
572
+ if (!messages) {
573
+ throw new Error('[HubPipeline] outbound stage requires payload.messages[]');
574
+ }
575
+ const tools = Array.isArray(payload.tools) ? payload.tools : undefined;
576
+ const parameters = payload.parameters && typeof payload.parameters === 'object' && !Array.isArray(payload.parameters)
577
+ ? payload.parameters
578
+ : {};
579
+ const metadataFromPayload = payload.metadata && typeof payload.metadata === 'object' && !Array.isArray(payload.metadata)
580
+ ? payload.metadata
581
+ : undefined;
582
+ const standardizedRequest = {
583
+ model,
584
+ messages,
585
+ ...(tools ? { tools } : {}),
586
+ parameters,
587
+ metadata: {
588
+ originalEndpoint: normalized.entryEndpoint,
589
+ ...(metadataFromPayload ? metadataFromPayload : {}),
590
+ requestId: normalized.id,
591
+ stream: normalized.stream,
592
+ processMode: normalized.processMode,
593
+ ...(normalized.routeHint ? { routeHint: normalized.routeHint } : {})
594
+ }
595
+ };
596
+ // Keep rawPayload minimal and JSON-safe; chat-process only needs the OpenAI-chat-like surface here.
597
+ const rawPayload = {
598
+ model,
599
+ messages,
600
+ ...(tools ? { tools } : {}),
601
+ ...(parameters && Object.keys(parameters).length ? { parameters } : {})
602
+ };
603
+ return { standardizedRequest, rawPayload };
604
+ }
605
+ async executeChatProcessEntryPipeline(normalized) {
606
+ const hooks = this.resolveProtocolHooks(normalized.providerProtocol);
607
+ if (!hooks) {
608
+ throw new Error(`Unsupported provider protocol for hub pipeline: ${normalized.providerProtocol}`);
609
+ }
610
+ const nodeResults = [];
611
+ nodeResults.push({
612
+ id: 'req_inbound',
613
+ success: true,
614
+ metadata: {
615
+ node: 'req_inbound',
616
+ skipped: true,
617
+ reason: 'stage=outbound',
618
+ dataProcessed: {}
619
+ }
620
+ });
621
+ const rawPayloadInput = this.asJsonObject(normalized.payload);
622
+ const { standardizedRequest: standardizedRequestBase, rawPayload } = this.coerceStandardizedRequestFromPayload(rawPayloadInput, normalized);
623
+ // Keep metadata injection consistent with the inbound path: servertool/web_search config must be available
624
+ // to chat-process/tool governance even when request enters at outbound stage.
625
+ const metaBase = {
626
+ ...(normalized.metadata ?? {})
627
+ };
628
+ const webSearchConfig = this.config.virtualRouter?.webSearch;
629
+ if (webSearchConfig) {
630
+ metaBase.webSearch = webSearchConfig;
631
+ }
632
+ const execCommandGuard = this.config.virtualRouter?.execCommandGuard;
633
+ if (execCommandGuard) {
634
+ metaBase.execCommandGuard = execCommandGuard;
635
+ }
636
+ const clockConfig = this.config.virtualRouter?.clock;
637
+ if (clockConfig) {
638
+ metaBase.clock = clockConfig;
639
+ }
640
+ normalized.metadata = metaBase;
641
+ const standardizedRequest = standardizedRequestBase;
642
+ try {
643
+ const mode = String(metaBase?.applyPatchToolMode || '').trim().toLowerCase();
644
+ if (mode === 'freeform' || mode === 'schema') {
645
+ standardizedRequest.metadata.applyPatchToolMode = mode;
646
+ }
647
+ }
648
+ catch {
649
+ // ignore
650
+ }
651
+ const adapterContext = this.buildAdapterContext(normalized);
652
+ const stageRecorder = this.maybeCreateStageRecorder(adapterContext, normalized.entryEndpoint, {
653
+ disableSnapshots: normalized.disableSnapshots === true
654
+ });
655
+ let processedRequest;
656
+ if (normalized.processMode !== 'passthrough') {
657
+ const processResult = await runReqProcessStage1ToolGovernance({
658
+ request: standardizedRequest,
659
+ rawPayload,
660
+ metadata: metaBase,
661
+ entryEndpoint: normalized.entryEndpoint,
662
+ requestId: normalized.id,
663
+ stageRecorder
664
+ });
665
+ processedRequest = processResult.processedRequest;
666
+ // Surface request-side clock reservation into pipeline metadata so response conversion
667
+ // can commit delivery only after a successful response is produced.
668
+ try {
669
+ const reservation = processedRequest?.metadata?.__clockReservation;
670
+ if (reservation && typeof reservation === 'object') {
671
+ metaBase.__clockReservation = reservation;
672
+ }
673
+ }
674
+ catch {
675
+ // best-effort
676
+ }
677
+ if (processResult.nodeResult) {
678
+ nodeResults.push(this.convertProcessNodeResult('req_process_stage1_tool_governance', processResult.nodeResult));
679
+ }
680
+ }
681
+ let workingRequest = processedRequest ?? standardizedRequest;
682
+ // Token estimate for stats/diagnostics (best-effort).
683
+ try {
684
+ const estimatedTokens = computeRequestTokens(workingRequest, '');
685
+ if (typeof estimatedTokens === 'number' && Number.isFinite(estimatedTokens) && estimatedTokens > 0) {
686
+ normalized.metadata = normalized.metadata || {};
687
+ normalized.metadata.estimatedInputTokens = estimatedTokens;
688
+ }
689
+ }
690
+ catch {
691
+ // ignore
692
+ }
693
+ const normalizedMeta = normalized.metadata;
694
+ const responsesResume = normalizedMeta && typeof normalizedMeta.responsesResume === 'object'
695
+ ? normalizedMeta.responsesResume
696
+ : undefined;
697
+ const stdMetadata = workingRequest?.metadata;
698
+ const hasImageAttachment = (stdMetadata?.hasImageAttachment === true || stdMetadata?.hasImageAttachment === 'true') ||
699
+ (normalizedMeta?.hasImageAttachment === true || normalizedMeta?.hasImageAttachment === 'true');
700
+ const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
701
+ stdMetadata?.serverToolRequired === true;
702
+ const sessionIdentifiers = extractSessionIdentifiersFromMetadata(normalized.metadata);
703
+ if (sessionIdentifiers.sessionId && normalized.metadata && typeof normalized.metadata === 'object') {
704
+ normalized.metadata.sessionId = sessionIdentifiers.sessionId;
705
+ }
706
+ if (sessionIdentifiers.conversationId && normalized.metadata && typeof normalized.metadata === 'object') {
707
+ normalized.metadata.conversationId = sessionIdentifiers.conversationId;
708
+ }
709
+ const metadataInput = {
710
+ requestId: normalized.id,
711
+ entryEndpoint: normalized.entryEndpoint,
712
+ processMode: normalized.processMode,
713
+ stream: normalized.stream,
714
+ direction: normalized.direction,
715
+ providerProtocol: normalized.providerProtocol,
716
+ routeHint: normalized.routeHint,
717
+ stage: normalized.stage,
718
+ responsesResume: responsesResume,
719
+ ...(serverToolRequired ? { serverToolRequired: true } : {}),
720
+ ...(sessionIdentifiers.sessionId ? { sessionId: sessionIdentifiers.sessionId } : {}),
721
+ ...(sessionIdentifiers.conversationId ? { conversationId: sessionIdentifiers.conversationId } : {})
722
+ };
723
+ const routing = runReqProcessStage2RouteSelect({
724
+ routerEngine: this.routerEngine,
725
+ request: workingRequest,
726
+ metadataInput,
727
+ normalizedMetadata: normalized.metadata,
728
+ stageRecorder
729
+ });
730
+ const stopMessageState = this.routerEngine.getStopMessageState(metadataInput);
731
+ if (stopMessageState && normalized.metadata && typeof normalized.metadata === 'object') {
732
+ normalized.metadata.stopMessageState = stopMessageState;
733
+ workingRequest = applyHubOperations(workingRequest, [
734
+ { op: 'set_request_metadata_fields', fields: { stopMessageState } }
735
+ ]);
736
+ }
737
+ // Emit virtual router hit log for debugging (same as inbound path).
738
+ try {
739
+ const routeName = routing.decision?.routeName;
740
+ const providerKey = routing.target?.providerKey;
741
+ const modelId = workingRequest.model;
742
+ const logger = (normalized.metadata && normalized.metadata.logger);
743
+ if (logger && typeof logger.logVirtualRouterHit === 'function' && routeName && providerKey) {
744
+ logger.logVirtualRouterHit(routeName, providerKey, typeof modelId === 'string' ? modelId : undefined);
745
+ }
746
+ }
747
+ catch {
748
+ // ignore
749
+ }
750
+ const outboundStream = this.resolveOutboundStreamIntent(routing.target?.streaming);
751
+ workingRequest = this.applyOutboundStreamPreference(workingRequest, outboundStream);
752
+ const outboundAdapterContext = this.buildAdapterContext(normalized, routing.target);
753
+ if (routing.target?.compatibilityProfile) {
754
+ outboundAdapterContext.compatibilityProfile = routing.target.compatibilityProfile;
755
+ }
756
+ const outboundProtocol = outboundAdapterContext.providerProtocol;
757
+ const protocolSwitch = outboundProtocol !== normalized.providerProtocol;
758
+ const outboundHooks = protocolSwitch ? this.resolveProtocolHooks(outboundProtocol) : hooks;
759
+ if (!outboundHooks) {
760
+ throw new Error(`[HubPipeline] Unsupported provider protocol for hub pipeline: ${outboundProtocol}`);
761
+ }
762
+ const outboundSemanticMapper = protocolSwitch ? outboundHooks.createSemanticMapper() : hooks.createSemanticMapper();
763
+ const outboundFormatAdapter = protocolSwitch ? outboundHooks.createFormatAdapter() : hooks.createFormatAdapter();
764
+ const outboundContextMetadataKey = protocolSwitch ? outboundHooks.contextMetadataKey : hooks.contextMetadataKey;
765
+ const outboundContextSnapshot = undefined;
766
+ const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint, {
767
+ disableSnapshots: normalized.disableSnapshots === true
768
+ });
769
+ const outboundStart = Date.now();
770
+ let providerPayload;
771
+ const outboundStage1 = await runReqOutboundStage1SemanticMap({
772
+ request: workingRequest,
773
+ adapterContext: outboundAdapterContext,
774
+ semanticMapper: outboundSemanticMapper,
775
+ contextSnapshot: outboundContextSnapshot,
776
+ contextMetadataKey: outboundContextMetadataKey,
777
+ stageRecorder: outboundRecorder
778
+ });
779
+ let formattedPayload = await runReqOutboundStage2FormatBuild({
780
+ formatEnvelope: outboundStage1.formatEnvelope,
781
+ adapterContext: outboundAdapterContext,
782
+ formatAdapter: outboundFormatAdapter,
783
+ stageRecorder: outboundRecorder
784
+ });
785
+ formattedPayload = await runReqOutboundStage3Compat({
786
+ payload: formattedPayload,
787
+ adapterContext: outboundAdapterContext,
788
+ stageRecorder: outboundRecorder
789
+ });
790
+ // Phase 0/1: observe + enforce provider outbound policy and tool surface (same as inbound path).
791
+ const effectivePolicy = normalized.policyOverride ?? this.config.policy;
792
+ recordHubPolicyObservation({
793
+ policy: effectivePolicy,
794
+ providerProtocol: outboundProtocol,
795
+ payload: formattedPayload,
796
+ stageRecorder: outboundRecorder,
797
+ requestId: normalized.id
798
+ });
799
+ providerPayload = applyHubProviderOutboundPolicy({
800
+ policy: effectivePolicy,
801
+ providerProtocol: outboundProtocol,
802
+ payload: formattedPayload,
803
+ stageRecorder: outboundRecorder,
804
+ requestId: normalized.id
805
+ });
806
+ providerPayload = applyProviderOutboundToolSurface({
807
+ config: this.config.toolSurface,
808
+ providerProtocol: outboundProtocol,
809
+ payload: providerPayload,
810
+ stageRecorder: outboundRecorder,
811
+ requestId: normalized.id
812
+ });
813
+ recordHubPolicyObservation({
814
+ policy: effectivePolicy,
815
+ providerProtocol: outboundProtocol,
816
+ payload: providerPayload,
817
+ stageRecorder: outboundRecorder,
818
+ requestId: normalized.id
819
+ });
820
+ const outboundEnd = Date.now();
821
+ nodeResults.push({
822
+ id: 'req_outbound',
823
+ success: true,
824
+ metadata: {
825
+ node: 'req_outbound',
826
+ executionTime: outboundEnd - outboundStart,
827
+ startTime: outboundStart,
828
+ endTime: outboundEnd,
829
+ dataProcessed: {
830
+ messages: workingRequest.messages.length,
831
+ tools: workingRequest.tools?.length ?? 0
832
+ }
833
+ }
834
+ });
835
+ const capturedChatRequest = {
836
+ model: workingRequest.model,
837
+ messages: jsonClone(workingRequest.messages),
838
+ tools: workingRequest.tools ? jsonClone(workingRequest.tools) : workingRequest.tools,
839
+ parameters: workingRequest.parameters ? jsonClone(workingRequest.parameters) : workingRequest.parameters
840
+ };
841
+ const metadata = {
842
+ ...normalized.metadata,
843
+ ...(hasImageAttachment ? { hasImageAttachment: true } : {}),
844
+ capturedChatRequest,
845
+ entryEndpoint: normalized.entryEndpoint,
846
+ providerProtocol: outboundProtocol,
847
+ stream: normalized.stream,
848
+ processMode: normalized.processMode,
849
+ routeHint: normalized.routeHint,
850
+ target: routing.target,
851
+ ...(typeof outboundStream === 'boolean' ? { providerStream: outboundStream } : {})
852
+ };
853
+ return {
854
+ requestId: normalized.id,
855
+ providerPayload,
856
+ standardizedRequest,
857
+ processedRequest,
858
+ routingDecision: routing.decision,
859
+ routingDiagnostics: routing.diagnostics,
860
+ target: routing.target,
861
+ metadata,
862
+ nodeResults
863
+ };
864
+ }
412
865
  async execute(request) {
413
866
  const normalized = await this.normalizeRequest(request);
867
+ if (normalized.direction === 'request' && normalized.hubEntryMode === 'chat_process') {
868
+ return await this.executeChatProcessEntryPipeline(normalized);
869
+ }
414
870
  const hooks = this.resolveProtocolHooks(normalized.providerProtocol);
415
871
  if (!hooks) {
416
872
  throw new Error(`Unsupported provider protocol for hub pipeline: ${normalized.providerProtocol}`);
@@ -540,6 +996,12 @@ export class HubPipeline {
540
996
  adapterContext.clientToolsRaw = metadata
541
997
  .clientToolsRaw;
542
998
  }
999
+ const applyPatchToolMode = typeof metadata.applyPatchToolMode === 'string'
1000
+ ? metadata.applyPatchToolMode.trim().toLowerCase()
1001
+ : '';
1002
+ if (applyPatchToolMode === 'freeform' || applyPatchToolMode === 'schema') {
1003
+ adapterContext.applyPatchToolMode = applyPatchToolMode;
1004
+ }
543
1005
  const sessionId = typeof metadata.sessionId === 'string'
544
1006
  ? metadata.sessionId.trim()
545
1007
  : '';
@@ -629,10 +1091,23 @@ export class HubPipeline {
629
1091
  if (Object.prototype.hasOwnProperty.call(metadataRecord, '__hubPolicyOverride')) {
630
1092
  delete metadataRecord.__hubPolicyOverride;
631
1093
  }
1094
+ const shadowCompare = extractHubShadowCompareConfig(metadataRecord);
1095
+ if (Object.prototype.hasOwnProperty.call(metadataRecord, '__hubShadowCompare')) {
1096
+ delete metadataRecord.__hubShadowCompare;
1097
+ }
632
1098
  const disableSnapshots = metadataRecord.__disableHubSnapshots === true;
633
1099
  if (Object.prototype.hasOwnProperty.call(metadataRecord, '__disableHubSnapshots')) {
634
1100
  delete metadataRecord.__disableHubSnapshots;
635
1101
  }
1102
+ const hubEntryRaw = typeof metadataRecord.__hubEntry === 'string'
1103
+ ? String(metadataRecord.__hubEntry).trim().toLowerCase()
1104
+ : '';
1105
+ const hubEntryMode = hubEntryRaw === 'chat_process' || hubEntryRaw === 'chat-process' || hubEntryRaw === 'chatprocess'
1106
+ ? 'chat_process'
1107
+ : undefined;
1108
+ if (Object.prototype.hasOwnProperty.call(metadataRecord, '__hubEntry')) {
1109
+ delete metadataRecord.__hubEntry;
1110
+ }
636
1111
  const entryEndpoint = typeof metadataRecord.entryEndpoint === 'string'
637
1112
  ? normalizeEndpoint(metadataRecord.entryEndpoint)
638
1113
  : endpoint;
@@ -671,12 +1146,14 @@ export class HubPipeline {
671
1146
  payload,
672
1147
  metadata: normalizedMetadata,
673
1148
  policyOverride: policyOverride ?? undefined,
1149
+ shadowCompare: shadowCompare ?? undefined,
674
1150
  disableSnapshots,
675
1151
  processMode,
676
1152
  direction,
677
1153
  stage,
678
1154
  stream,
679
- routeHint
1155
+ routeHint,
1156
+ ...(hubEntryMode ? { hubEntryMode } : {})
680
1157
  };
681
1158
  }
682
1159
  convertProcessNodeResult(id, result) {
@@ -772,25 +1249,18 @@ export class HubPipeline {
772
1249
  }
773
1250
  applyOutboundStreamPreference(request, stream) {
774
1251
  if (!request || typeof request !== 'object') {
775
- return;
1252
+ return request;
776
1253
  }
777
- const parameters = request.parameters || {};
778
- const nextParameters = { ...parameters };
1254
+ const ops = [];
779
1255
  if (typeof stream === 'boolean') {
780
- nextParameters.stream = stream;
781
- }
782
- else if ('stream' in nextParameters) {
783
- delete nextParameters.stream;
1256
+ ops.push({ op: 'set_request_parameter_fields', fields: { stream } });
1257
+ ops.push({ op: 'set_request_metadata_fields', fields: { outboundStream: stream } });
784
1258
  }
785
- request.parameters = nextParameters;
786
- if (request.metadata && typeof request.metadata === 'object') {
787
- if (typeof stream === 'boolean') {
788
- request.metadata.outboundStream = stream;
789
- }
790
- else if ('outboundStream' in request.metadata) {
791
- delete request.metadata.outboundStream;
792
- }
1259
+ else {
1260
+ ops.push({ op: 'unset_request_parameter_keys', keys: ['stream'] });
1261
+ ops.push({ op: 'unset_request_metadata_keys', keys: ['outboundStream'] });
793
1262
  }
1263
+ return applyHubOperations(request, ops);
794
1264
  }
795
1265
  }
796
1266
  function normalizeToolCallIdStyleCandidate(value) {