@jsonstudio/llms 0.6.938 → 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 (131) 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 +533 -24
  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_inbound/req_inbound_stage3_context_capture/index.js +6 -3
  17. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
  18. package/dist/conversion/hub/policy/policy-engine.js +41 -9
  19. package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
  20. package/dist/conversion/hub/policy/protocol-spec.js +73 -23
  21. package/dist/conversion/hub/process/chat-process.js +252 -41
  22. package/dist/conversion/hub/response/provider-response.js +175 -2
  23. package/dist/conversion/hub/response/response-runtime.js +1 -1
  24. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
  25. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
  26. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
  27. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -436
  28. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
  29. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -894
  30. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
  31. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
  32. package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
  33. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
  34. package/dist/conversion/responses/responses-openai-bridge.js +14 -2
  35. package/dist/conversion/shared/bridge-message-utils.js +2 -8
  36. package/dist/conversion/shared/bridge-policies.js +5 -105
  37. package/dist/conversion/shared/gemini-tool-utils.js +121 -4
  38. package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
  39. package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
  40. package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
  41. package/dist/conversion/shared/snapshot-hooks.js +166 -3
  42. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  43. package/dist/conversion/shared/text-markup-normalizer.js +345 -9
  44. package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
  45. package/dist/conversion/shared/thought-signature-validator.js +170 -0
  46. package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
  47. package/dist/conversion/shared/tool-argument-repairer.js +56 -0
  48. package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
  49. package/dist/conversion/shared/tool-call-id-manager.js +231 -0
  50. package/dist/conversion/shared/tool-canonicalizer.js +2 -11
  51. package/dist/router/virtual-router/bootstrap.js +54 -5
  52. package/dist/router/virtual-router/engine-selection.js +132 -42
  53. package/dist/router/virtual-router/engine.d.ts +3 -0
  54. package/dist/router/virtual-router/engine.js +142 -33
  55. package/dist/router/virtual-router/health-weighted.d.ts +25 -0
  56. package/dist/router/virtual-router/health-weighted.js +63 -0
  57. package/dist/router/virtual-router/load-balancer.d.ts +2 -0
  58. package/dist/router/virtual-router/load-balancer.js +45 -16
  59. package/dist/router/virtual-router/routing-instructions.js +17 -1
  60. package/dist/router/virtual-router/sticky-session-store.js +136 -24
  61. package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
  62. package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
  63. package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
  64. package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
  65. package/dist/router/virtual-router/types.d.ts +70 -0
  66. package/dist/servertool/clock/config.d.ts +7 -0
  67. package/dist/servertool/clock/config.js +27 -0
  68. package/dist/servertool/clock/daemon.d.ts +3 -0
  69. package/dist/servertool/clock/daemon.js +79 -0
  70. package/dist/servertool/clock/io.d.ts +2 -0
  71. package/dist/servertool/clock/io.js +13 -0
  72. package/dist/servertool/clock/paths.d.ts +4 -0
  73. package/dist/servertool/clock/paths.js +25 -0
  74. package/dist/servertool/clock/session-store.d.ts +3 -0
  75. package/dist/servertool/clock/session-store.js +56 -0
  76. package/dist/servertool/clock/state.d.ts +5 -0
  77. package/dist/servertool/clock/state.js +62 -0
  78. package/dist/servertool/clock/task-store.d.ts +5 -0
  79. package/dist/servertool/clock/task-store.js +4 -0
  80. package/dist/servertool/clock/tasks.d.ts +17 -0
  81. package/dist/servertool/clock/tasks.js +221 -0
  82. package/dist/servertool/clock/types.d.ts +36 -0
  83. package/dist/servertool/clock/types.js +1 -0
  84. package/dist/servertool/engine.d.ts +2 -0
  85. package/dist/servertool/engine.js +164 -8
  86. package/dist/servertool/followup-shadow.d.ts +16 -0
  87. package/dist/servertool/followup-shadow.js +145 -0
  88. package/dist/servertool/handlers/apply-patch-guard.js +1 -265
  89. package/dist/servertool/handlers/clock-auto.d.ts +1 -0
  90. package/dist/servertool/handlers/clock-auto.js +160 -0
  91. package/dist/servertool/handlers/clock.d.ts +1 -0
  92. package/dist/servertool/handlers/clock.js +197 -0
  93. package/dist/servertool/handlers/exec-command-guard.js +7 -555
  94. package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
  95. package/dist/servertool/handlers/followup-request-builder.js +248 -28
  96. package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
  97. package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
  98. package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
  99. package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
  100. package/dist/servertool/handlers/stop-message-auto.js +47 -175
  101. package/dist/servertool/handlers/vision.d.ts +7 -1
  102. package/dist/servertool/handlers/vision.js +61 -117
  103. package/dist/servertool/handlers/web-search.d.ts +7 -1
  104. package/dist/servertool/handlers/web-search.js +122 -105
  105. package/dist/servertool/reenter-backend.d.ts +23 -0
  106. package/dist/servertool/reenter-backend.js +18 -0
  107. package/dist/servertool/server-side-tools.d.ts +3 -2
  108. package/dist/servertool/server-side-tools.js +64 -10
  109. package/dist/servertool/types.d.ts +92 -3
  110. package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
  111. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
  112. package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
  113. package/dist/sse/shared/writer.js +24 -7
  114. package/dist/tools/apply-patch/execution-capturer.js +3 -1
  115. package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
  116. package/dist/tools/apply-patch/json/parse-loose.js +139 -0
  117. package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
  118. package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
  119. package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
  120. package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
  121. package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
  122. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
  123. package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
  124. package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
  125. package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
  126. package/dist/tools/apply-patch/structured/coercion.js +82 -0
  127. package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
  128. package/dist/tools/apply-patch/validation/shared.js +6 -0
  129. package/dist/tools/apply-patch/validator.d.ts +2 -2
  130. package/dist/tools/apply-patch/validator.js +6 -556
  131. package/package.json +1 -1
@@ -26,6 +26,86 @@ 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
+ }
77
+ function extractHubPolicyOverride(metadata) {
78
+ const raw = metadata ? metadata.__hubPolicyOverride : undefined;
79
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
80
+ return undefined;
81
+ }
82
+ const obj = raw;
83
+ const mode = typeof obj.mode === 'string' ? obj.mode.trim().toLowerCase() : '';
84
+ const sampleRate = typeof obj.sampleRate === 'number' && Number.isFinite(obj.sampleRate) ? obj.sampleRate : undefined;
85
+ if (mode !== 'off' && mode !== 'observe' && mode !== 'enforce') {
86
+ return undefined;
87
+ }
88
+ return {
89
+ mode: mode,
90
+ ...(sampleRate !== undefined ? { sampleRate } : {})
91
+ };
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
+ }
29
109
  export class HubPipeline {
30
110
  routerEngine;
31
111
  config;
@@ -104,6 +184,11 @@ export class HubPipeline {
104
184
  // not the governed/augmented tools.
105
185
  try {
106
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
+ }
107
192
  if (toolsRaw && toolsRaw.length > 0) {
108
193
  normalized.metadata = normalized.metadata || {};
109
194
  normalized.metadata.clientToolsRaw = jsonClone(toolsRaw);
@@ -116,11 +201,16 @@ export class HubPipeline {
116
201
  normalized.metadata = normalized.metadata || {};
117
202
  normalized.metadata.compactionRequest = true;
118
203
  }
204
+ const effectivePolicy = normalized.policyOverride ?? this.config.policy;
205
+ const shadowCompareBaselineMode = normalized.shadowCompare?.baselineMode;
119
206
  const inboundAdapterContext = this.buildAdapterContext(normalized);
120
- const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint);
207
+ const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint, {
208
+ disableSnapshots: normalized.disableSnapshots === true
209
+ });
121
210
  const inboundStart = Date.now();
122
211
  // Phase 0: observe client inbound payload violations (best-effort; no rewrites).
123
212
  recordHubPolicyObservation({
213
+ policy: effectivePolicy,
124
214
  providerProtocol: this.resolveClientProtocol(normalized.entryEndpoint),
125
215
  payload: rawRequest,
126
216
  phase: 'client_inbound',
@@ -146,6 +236,15 @@ export class HubPipeline {
146
236
  stageRecorder: inboundRecorder
147
237
  });
148
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
+ }
149
248
  const inboundEnd = Date.now();
150
249
  const nodeResults = [];
151
250
  nodeResults.push({
@@ -175,6 +274,10 @@ export class HubPipeline {
175
274
  if (execCommandGuard) {
176
275
  metaBase.execCommandGuard = execCommandGuard;
177
276
  }
277
+ const clockConfig = this.config.virtualRouter?.clock;
278
+ if (clockConfig) {
279
+ metaBase.clock = clockConfig;
280
+ }
178
281
  normalized.metadata = metaBase;
179
282
  let processedRequest;
180
283
  if (normalized.processMode !== 'passthrough') {
@@ -187,11 +290,22 @@ export class HubPipeline {
187
290
  stageRecorder: inboundRecorder
188
291
  });
189
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
+ }
190
304
  if (processResult.nodeResult) {
191
305
  nodeResults.push(this.convertProcessNodeResult('req_process_stage1_tool_governance', processResult.nodeResult));
192
306
  }
193
307
  }
194
- const workingRequest = processedRequest ?? standardizedRequest;
308
+ let workingRequest = processedRequest ?? standardizedRequest;
195
309
  // 使用与 VirtualRouter 一致的 tiktoken 计数逻辑,对标准化请求进行一次
196
310
  // 上下文 token 估算,供后续 usage 归一化与统计使用。
197
311
  try {
@@ -247,6 +361,12 @@ export class HubPipeline {
247
361
  const stopMessageState = this.routerEngine.getStopMessageState(metadataInput);
248
362
  if (stopMessageState && normalized.metadata && typeof normalized.metadata === 'object') {
249
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
+ ]);
250
370
  }
251
371
  // Emit virtual router hit log for debugging (orange [virtual-router] ...)
252
372
  try {
@@ -262,7 +382,7 @@ export class HubPipeline {
262
382
  // logging must not break routing
263
383
  }
264
384
  const outboundStream = this.resolveOutboundStreamIntent(routing.target?.streaming);
265
- this.applyOutboundStreamPreference(workingRequest, outboundStream);
385
+ workingRequest = this.applyOutboundStreamPreference(workingRequest, outboundStream);
266
386
  const outboundAdapterContext = this.buildAdapterContext(normalized, routing.target);
267
387
  if (routing.target?.compatibilityProfile) {
268
388
  outboundAdapterContext.compatibilityProfile = routing.target.compatibilityProfile;
@@ -280,7 +400,9 @@ export class HubPipeline {
280
400
  // Snapshots must be grouped by entry endpoint (client-facing protocol), not by provider protocol.
281
401
  // Otherwise one request would be split across multiple folders (e.g. openai-responses + anthropic-messages),
282
402
  // which breaks codex-samples correlation.
283
- const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint);
403
+ const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint, {
404
+ disableSnapshots: normalized.disableSnapshots === true
405
+ });
284
406
  const outboundStart = Date.now();
285
407
  let providerPayload;
286
408
  const outboundStage1 = await runReqOutboundStage1SemanticMap({
@@ -302,15 +424,59 @@ export class HubPipeline {
302
424
  adapterContext: outboundAdapterContext,
303
425
  stageRecorder: outboundRecorder
304
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
+ });
305
464
  providerPayload = applyHubProviderOutboundPolicy({
306
- policy: this.config.policy,
465
+ policy: effectivePolicy,
307
466
  providerProtocol: outboundProtocol,
308
467
  payload: formattedPayload,
309
468
  stageRecorder: outboundRecorder,
310
469
  requestId: normalized.id
311
470
  });
471
+ providerPayload = applyProviderOutboundToolSurface({
472
+ config: this.config.toolSurface,
473
+ providerProtocol: outboundProtocol,
474
+ payload: providerPayload,
475
+ stageRecorder: outboundRecorder,
476
+ requestId: normalized.id
477
+ });
312
478
  recordHubPolicyObservation({
313
- policy: this.config.policy,
479
+ policy: effectivePolicy,
314
480
  providerProtocol: outboundProtocol,
315
481
  payload: providerPayload,
316
482
  stageRecorder: outboundRecorder,
@@ -365,7 +531,17 @@ export class HubPipeline {
365
531
  processMode: normalized.processMode,
366
532
  routeHint: normalized.routeHint,
367
533
  target: routing.target,
368
- ...(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
+ : {})
369
545
  };
370
546
  return {
371
547
  requestId: normalized.id,
@@ -387,8 +563,310 @@ export class HubPipeline {
387
563
  return 'anthropic-messages';
388
564
  return 'openai-chat';
389
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
+ }
390
865
  async execute(request) {
391
866
  const normalized = await this.normalizeRequest(request);
867
+ if (normalized.direction === 'request' && normalized.hubEntryMode === 'chat_process') {
868
+ return await this.executeChatProcessEntryPipeline(normalized);
869
+ }
392
870
  const hooks = this.resolveProtocolHooks(normalized.providerProtocol);
393
871
  if (!hooks) {
394
872
  throw new Error(`Unsupported provider protocol for hub pipeline: ${normalized.providerProtocol}`);
@@ -481,6 +959,10 @@ export class HubPipeline {
481
959
  streamingHint,
482
960
  toolCallIdStyle
483
961
  };
962
+ const runtime = metadata.runtime;
963
+ if (runtime && typeof runtime === 'object' && !Array.isArray(runtime)) {
964
+ adapterContext.runtime = jsonClone(runtime);
965
+ }
484
966
  const clientRequestId = typeof metadata.clientRequestId === 'string'
485
967
  ? metadata.clientRequestId.trim()
486
968
  : '';
@@ -514,6 +996,12 @@ export class HubPipeline {
514
996
  adapterContext.clientToolsRaw = metadata
515
997
  .clientToolsRaw;
516
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
+ }
517
1005
  const sessionId = typeof metadata.sessionId === 'string'
518
1006
  ? metadata.sessionId.trim()
519
1007
  : '';
@@ -569,7 +1057,10 @@ export class HubPipeline {
569
1057
  }
570
1058
  return adapterContext;
571
1059
  }
572
- maybeCreateStageRecorder(context, endpoint) {
1060
+ maybeCreateStageRecorder(context, endpoint, options) {
1061
+ if (options?.disableSnapshots === true) {
1062
+ return undefined;
1063
+ }
573
1064
  if (!shouldRecordSnapshots()) {
574
1065
  return undefined;
575
1066
  }
@@ -596,6 +1087,27 @@ export class HubPipeline {
596
1087
  const metadataRecord = {
597
1088
  ...(request.metadata ?? {})
598
1089
  };
1090
+ const policyOverride = extractHubPolicyOverride(metadataRecord);
1091
+ if (Object.prototype.hasOwnProperty.call(metadataRecord, '__hubPolicyOverride')) {
1092
+ delete metadataRecord.__hubPolicyOverride;
1093
+ }
1094
+ const shadowCompare = extractHubShadowCompareConfig(metadataRecord);
1095
+ if (Object.prototype.hasOwnProperty.call(metadataRecord, '__hubShadowCompare')) {
1096
+ delete metadataRecord.__hubShadowCompare;
1097
+ }
1098
+ const disableSnapshots = metadataRecord.__disableHubSnapshots === true;
1099
+ if (Object.prototype.hasOwnProperty.call(metadataRecord, '__disableHubSnapshots')) {
1100
+ delete metadataRecord.__disableHubSnapshots;
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
+ }
599
1111
  const entryEndpoint = typeof metadataRecord.entryEndpoint === 'string'
600
1112
  ? normalizeEndpoint(metadataRecord.entryEndpoint)
601
1113
  : endpoint;
@@ -633,11 +1145,15 @@ export class HubPipeline {
633
1145
  providerProtocol,
634
1146
  payload,
635
1147
  metadata: normalizedMetadata,
1148
+ policyOverride: policyOverride ?? undefined,
1149
+ shadowCompare: shadowCompare ?? undefined,
1150
+ disableSnapshots,
636
1151
  processMode,
637
1152
  direction,
638
1153
  stage,
639
1154
  stream,
640
- routeHint
1155
+ routeHint,
1156
+ ...(hubEntryMode ? { hubEntryMode } : {})
641
1157
  };
642
1158
  }
643
1159
  convertProcessNodeResult(id, result) {
@@ -733,25 +1249,18 @@ export class HubPipeline {
733
1249
  }
734
1250
  applyOutboundStreamPreference(request, stream) {
735
1251
  if (!request || typeof request !== 'object') {
736
- return;
1252
+ return request;
737
1253
  }
738
- const parameters = request.parameters || {};
739
- const nextParameters = { ...parameters };
1254
+ const ops = [];
740
1255
  if (typeof stream === 'boolean') {
741
- nextParameters.stream = stream;
742
- }
743
- else if ('stream' in nextParameters) {
744
- delete nextParameters.stream;
1256
+ ops.push({ op: 'set_request_parameter_fields', fields: { stream } });
1257
+ ops.push({ op: 'set_request_metadata_fields', fields: { outboundStream: stream } });
745
1258
  }
746
- request.parameters = nextParameters;
747
- if (request.metadata && typeof request.metadata === 'object') {
748
- if (typeof stream === 'boolean') {
749
- request.metadata.outboundStream = stream;
750
- }
751
- else if ('outboundStream' in request.metadata) {
752
- delete request.metadata.outboundStream;
753
- }
1259
+ else {
1260
+ ops.push({ op: 'unset_request_parameter_keys', keys: ['stream'] });
1261
+ ops.push({ op: 'unset_request_metadata_keys', keys: ['outboundStream'] });
754
1262
  }
1263
+ return applyHubOperations(request, ops);
755
1264
  }
756
1265
  }
757
1266
  function normalizeToolCallIdStyleCandidate(value) {