@jsonstudio/llms 0.6.802 → 0.6.954

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 (188) hide show
  1. package/dist/bridge/routecodex-adapter.d.ts +74 -0
  2. package/dist/config-unified/enhanced-path-resolver.d.ts +5 -0
  3. package/dist/config-unified/unified-config.d.ts +26 -0
  4. package/dist/conversion/codec-registry.d.ts +10 -0
  5. package/dist/conversion/codecs/gemini-openai-codec.d.ts +16 -0
  6. package/dist/conversion/codecs/openai-openai-codec.d.ts +12 -0
  7. package/dist/conversion/codecs/responses-openai-codec.d.ts +12 -0
  8. package/dist/conversion/compat/profiles/chat-gemini.json +12 -0
  9. package/dist/conversion/config/config-manager.d.ts +212 -0
  10. package/dist/conversion/hub/config/types.d.ts +26 -0
  11. package/dist/conversion/hub/core/detour-registry.d.ts +9 -0
  12. package/dist/conversion/hub/core/hub-context.d.ts +21 -0
  13. package/dist/conversion/hub/core/index.d.ts +3 -0
  14. package/dist/conversion/hub/core/stage-driver.d.ts +30 -0
  15. package/dist/conversion/hub/format-adapters/anthropic-format-adapter.d.ts +16 -0
  16. package/dist/conversion/hub/format-adapters/chat-format-adapter.d.ts +17 -0
  17. package/dist/conversion/hub/format-adapters/gemini-format-adapter.d.ts +16 -0
  18. package/dist/conversion/hub/format-adapters/index.d.ts +21 -0
  19. package/dist/conversion/hub/hub-feature.d.ts +1 -0
  20. package/dist/conversion/hub/node-support.d.ts +19 -0
  21. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +11 -0
  22. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +7 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline.js +113 -17
  25. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +6 -3
  26. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +4 -0
  27. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +23 -1
  28. package/dist/conversion/hub/pipelines/inbound.d.ts +22 -0
  29. package/dist/conversion/hub/pipelines/outbound.d.ts +22 -0
  30. package/dist/conversion/hub/policy/policy-engine.d.ts +46 -0
  31. package/dist/conversion/hub/policy/policy-engine.js +176 -0
  32. package/dist/conversion/hub/policy/protocol-spec.d.ts +50 -0
  33. package/dist/conversion/hub/policy/protocol-spec.js +105 -0
  34. package/dist/conversion/hub/process/chat-process.d.ts +32 -0
  35. package/dist/conversion/hub/registry.d.ts +28 -0
  36. package/dist/conversion/hub/response/chat-response-utils.d.ts +6 -0
  37. package/dist/conversion/hub/response/provider-response.js +31 -0
  38. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +32 -1
  39. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +7 -0
  40. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +96 -1
  41. package/dist/conversion/hub/semantic-mappers/index.d.ts +4 -0
  42. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +21 -0
  43. package/dist/conversion/hub/standardized-bridge.d.ts +12 -0
  44. package/dist/conversion/hub/types/chat-schema.d.ts +112 -0
  45. package/dist/conversion/hub/types/errors.d.ts +5 -0
  46. package/dist/conversion/hub/types/format-envelope.d.ts +7 -0
  47. package/dist/conversion/hub/types/index.d.ts +6 -0
  48. package/dist/conversion/hub/types/json.d.ts +9 -0
  49. package/dist/conversion/hub/types/node.d.ts +31 -0
  50. package/dist/conversion/responses/responses-openai-bridge.js +263 -10
  51. package/dist/conversion/schema-validator.d.ts +7 -0
  52. package/dist/conversion/shared/args-mapping.d.ts +18 -0
  53. package/dist/conversion/shared/chat-request-filters.d.ts +9 -0
  54. package/dist/conversion/shared/errors.d.ts +1 -1
  55. package/dist/conversion/shared/gemini-tool-utils.js +105 -1
  56. package/dist/conversion/shared/jsonish.d.ts +3 -0
  57. package/dist/conversion/shared/mcp-injection.d.ts +2 -0
  58. package/dist/conversion/shared/media.d.ts +1 -0
  59. package/dist/conversion/shared/openai-message-normalize.d.ts +1 -0
  60. package/dist/conversion/shared/payload-budget.d.ts +13 -0
  61. package/dist/conversion/shared/reasoning-mapping.d.ts +5 -0
  62. package/dist/conversion/shared/responses-request-adapter.d.ts +1 -28
  63. package/dist/conversion/shared/responses-request-adapter.js +1 -430
  64. package/dist/conversion/shared/snapshot-hooks.js +58 -3
  65. package/dist/conversion/shared/tool-governor.js +8 -2
  66. package/dist/conversion/shared/tool-harvester.d.ts +31 -0
  67. package/dist/conversion/shared/tool-mapping.js +10 -29
  68. package/dist/conversion/types.d.ts +33 -0
  69. package/dist/filters/builtin/add-fields-filter.d.ts +8 -0
  70. package/dist/filters/builtin/blacklist-filter.d.ts +8 -0
  71. package/dist/filters/builtin/whitelist-filter.d.ts +8 -0
  72. package/dist/filters/engine.d.ts +16 -0
  73. package/dist/filters/special/request-tool-choice-policy.d.ts +11 -0
  74. package/dist/filters/special/response-finish-invariants.d.ts +11 -0
  75. package/dist/filters/special/response-openai-to-responses-bridge.d.ts +13 -0
  76. package/dist/filters/special/response-tool-arguments-blacklist.d.ts +12 -0
  77. package/dist/filters/special/response-tool-arguments-schema-converge.d.ts +13 -0
  78. package/dist/filters/special/response-tool-arguments-stringify.d.ts +9 -0
  79. package/dist/filters/special/response-tool-arguments-whitelist.d.ts +11 -0
  80. package/dist/filters/special/tool-filter-hooks.d.ts +19 -0
  81. package/dist/filters/special/tool-post-constraints.d.ts +31 -0
  82. package/dist/filters/types.d.ts +68 -0
  83. package/dist/filters/utils/fieldmap-loader.d.ts +2 -0
  84. package/dist/filters/utils/snapshot-writer.d.ts +10 -0
  85. package/dist/guidance/index.d.ts +3 -0
  86. package/dist/guidance/index.js +78 -83
  87. package/dist/http/sse-response.d.ts +22 -0
  88. package/dist/router/virtual-router/bootstrap.d.ts +6 -0
  89. package/dist/router/virtual-router/bootstrap.js +49 -5
  90. package/dist/router/virtual-router/classifier.d.ts +10 -0
  91. package/dist/router/virtual-router/engine-selection.js +98 -11
  92. package/dist/router/virtual-router/engine.js +177 -31
  93. package/dist/router/virtual-router/error-center.d.ts +10 -0
  94. package/dist/router/virtual-router/features.d.ts +3 -0
  95. package/dist/router/virtual-router/routing-instructions.d.ts +23 -1
  96. package/dist/router/virtual-router/routing-instructions.js +120 -30
  97. package/dist/router/virtual-router/types.d.ts +11 -0
  98. package/dist/servertool/engine.js +192 -17
  99. package/dist/servertool/handlers/apply-patch-guard.js +269 -0
  100. package/dist/servertool/handlers/exec-command-guard.js +558 -0
  101. package/dist/servertool/handlers/followup-message-trimmer.d.ts +16 -0
  102. package/dist/servertool/handlers/followup-message-trimmer.js +198 -0
  103. package/dist/servertool/handlers/followup-request-builder.d.ts +17 -0
  104. package/dist/servertool/handlers/followup-request-builder.js +122 -0
  105. package/dist/servertool/handlers/gemini-empty-reply-continue.js +252 -51
  106. package/dist/servertool/handlers/iflow-model-error-retry.js +12 -22
  107. package/dist/servertool/handlers/stop-message-auto.js +237 -75
  108. package/dist/servertool/handlers/vision.js +15 -27
  109. package/dist/servertool/handlers/web-search.js +17 -43
  110. package/dist/servertool/server-side-tools.d.ts +3 -0
  111. package/dist/servertool/server-side-tools.js +3 -0
  112. package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.d.ts +2 -1
  113. package/dist/sse/json-to-sse/chat-json-to-sse-converter.d.ts +80 -0
  114. package/dist/sse/json-to-sse/event-generators/chat.d.ts +55 -0
  115. package/dist/sse/json-to-sse/event-generators/responses.d.ts +99 -0
  116. package/dist/sse/json-to-sse/gemini-json-to-sse-converter.d.ts +2 -1
  117. package/dist/sse/json-to-sse/responses-json-to-sse-converter.d.ts +80 -0
  118. package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.d.ts +1 -1
  119. package/dist/sse/json-to-sse/sequencers/chat-sequencer.d.ts +2 -2
  120. package/dist/sse/json-to-sse/sequencers/gemini-sequencer.d.ts +1 -1
  121. package/dist/sse/json-to-sse/sequencers/responses-sequencer.d.ts +40 -0
  122. package/dist/sse/shared/chat-serializer.d.ts +4 -0
  123. package/dist/sse/shared/constants.d.ts +272 -0
  124. package/dist/sse/shared/serializers/anthropic-event-serializer.d.ts +1 -1
  125. package/dist/sse/shared/serializers/base-serializer.d.ts +158 -0
  126. package/dist/sse/shared/serializers/chat-event-serializer.d.ts +82 -0
  127. package/dist/sse/shared/serializers/gemini-event-serializer.d.ts +1 -1
  128. package/dist/sse/shared/serializers/index.d.ts +2 -1
  129. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +123 -0
  130. package/dist/sse/shared/serializers/types.d.ts +51 -0
  131. package/dist/sse/shared/utils.d.ts +254 -0
  132. package/dist/sse/shared/writer.d.ts +2 -2
  133. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -1
  134. package/dist/sse/sse-to-json/builders/anthropic-response-builder.d.ts +1 -1
  135. package/dist/sse/sse-to-json/builders/response-builder.d.ts +1 -1
  136. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +2 -1
  137. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.d.ts +2 -1
  138. package/dist/sse/sse-to-json/parsers/sse-parser.d.ts +73 -0
  139. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  140. package/dist/sse/types/chat-types.d.ts +1 -1
  141. package/dist/sse/types/responses-types.d.ts +1 -1
  142. package/dist/tools/apply-patch/execution-capturer.d.ts +13 -0
  143. package/dist/tools/apply-patch/execution-capturer.js +158 -0
  144. package/dist/tools/apply-patch/regression-capturer.d.ts +1 -0
  145. package/dist/tools/apply-patch/regression-capturer.js +5 -4
  146. package/dist/tools/apply-patch/structured.js +109 -13
  147. package/dist/tools/apply-patch/validator.js +112 -18
  148. package/dist/tools/tool-registry.d.ts +8 -0
  149. package/dist/tools/tool-registry.js +2 -1
  150. package/package.json +4 -4
  151. package/dist/conversion/compat/actions/apply-patch-format-fixer.js +0 -233
  152. package/dist/conversion/config/compat-profiles.json +0 -38
  153. package/dist/conversion/hub/pipeline/context-limit.d.ts +0 -13
  154. package/dist/conversion/hub/pipeline/context-limit.js +0 -55
  155. package/dist/conversion/hub/response/server-side-tools.d.ts +0 -26
  156. package/dist/conversion/hub/response/server-side-tools.js +0 -383
  157. package/dist/conversion/shared/bridge-conversation-store.d.ts +0 -41
  158. package/dist/conversion/shared/bridge-conversation-store.js +0 -279
  159. package/dist/conversion/shared/bridge-request-adapter.d.ts +0 -28
  160. package/dist/conversion/shared/bridge-request-adapter.js +0 -430
  161. package/dist/conversion/shared/responses-id-utils.js +0 -42
  162. package/dist/conversion/shared/responses-instructions.js +0 -113
  163. package/dist/conversion/shared/responses-message-utils.d.ts +0 -15
  164. package/dist/conversion/shared/responses-message-utils.js +0 -206
  165. package/dist/conversion/shared/responses-metadata.js +0 -1
  166. package/dist/conversion/shared/responses-output-utils.d.ts +0 -7
  167. package/dist/conversion/shared/responses-output-utils.js +0 -108
  168. package/dist/conversion/shared/responses-types.d.ts +0 -33
  169. package/dist/conversion/shared/tool-normalizers.d.ts +0 -4
  170. package/dist/conversion/shared/tool-normalizers.js +0 -84
  171. package/dist/filters/special/request-streaming-to-nonstreaming.d.ts +0 -13
  172. package/dist/filters/special/request-streaming-to-nonstreaming.js +0 -39
  173. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +0 -23
  174. package/dist/filters/special/response-apply-patch-toon-decode.js +0 -460
  175. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +0 -10
  176. package/dist/filters/special/response-tool-arguments-toon-decode.js +0 -154
  177. package/dist/servertool/flow-types.d.ts +0 -40
  178. package/dist/servertool/flow-types.js +0 -1
  179. package/dist/servertool/orchestration-types.d.ts +0 -33
  180. package/dist/servertool/orchestration-types.js +0 -1
  181. package/dist/servertool/vision-tool.d.ts +0 -2
  182. package/dist/servertool/vision-tool.js +0 -185
  183. package/dist/tools/patch-args-normalizer.d.ts +0 -15
  184. package/dist/tools/patch-args-normalizer.js +0 -472
  185. package/dist/utils/toon.d.ts +0 -4
  186. package/dist/utils/toon.js +0 -75
  187. /package/dist/{conversion/compat/actions/apply-patch-format-fixer.d.ts → servertool/handlers/apply-patch-guard.d.ts} +0 -0
  188. /package/dist/{conversion/shared/responses-types.js → servertool/handlers/exec-command-guard.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { Readable } from 'node:stream';
2
- import { isJsonObject } from '../types/json.js';
2
+ import { isJsonObject, jsonClone } from '../types/json.js';
3
3
  import { VirtualRouterEngine } from '../../../router/virtual-router/engine.js';
4
4
  import { providerErrorCenter } from '../../../router/virtual-router/error-center.js';
5
5
  import { defaultSseCodecRegistry } from '../../../sse/index.js';
@@ -25,6 +25,23 @@ import { runReqOutboundStage3Compat } from './stages/req_outbound/req_outbound_s
25
25
  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
+ import { applyHubProviderOutboundPolicy, recordHubPolicyObservation, setHubPolicyRuntimePolicy } from '../policy/policy-engine.js';
29
+ function extractHubPolicyOverride(metadata) {
30
+ const raw = metadata ? metadata.__hubPolicyOverride : undefined;
31
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
32
+ return undefined;
33
+ }
34
+ const obj = raw;
35
+ const mode = typeof obj.mode === 'string' ? obj.mode.trim().toLowerCase() : '';
36
+ const sampleRate = typeof obj.sampleRate === 'number' && Number.isFinite(obj.sampleRate) ? obj.sampleRate : undefined;
37
+ if (mode !== 'off' && mode !== 'observe' && mode !== 'enforce') {
38
+ return undefined;
39
+ }
40
+ return {
41
+ mode: mode,
42
+ ...(sampleRate !== undefined ? { sampleRate } : {})
43
+ };
44
+ }
28
45
  export class HubPipeline {
29
46
  routerEngine;
30
47
  config;
@@ -37,6 +54,7 @@ export class HubPipeline {
37
54
  quotaView: config.quotaView
38
55
  });
39
56
  this.routerEngine.initialize(config.virtualRouter);
57
+ setHubPolicyRuntimePolicy(config.policy);
40
58
  try {
41
59
  this.unsubscribeProviderErrors = providerErrorCenter.subscribe((event) => {
42
60
  try {
@@ -97,13 +115,38 @@ export class HubPipeline {
97
115
  const formatAdapter = hooks.createFormatAdapter();
98
116
  const semanticMapper = hooks.createSemanticMapper();
99
117
  const rawRequest = this.asJsonObject(normalized.payload);
118
+ // Preserve the client-provided raw tool definitions (schema included) for response-side validation
119
+ // and protocol-correct outbound tool-call formatting. This must reflect the inbound request shape,
120
+ // not the governed/augmented tools.
121
+ try {
122
+ const toolsRaw = Array.isArray(rawRequest?.tools) ? rawRequest.tools : null;
123
+ if (toolsRaw && toolsRaw.length > 0) {
124
+ normalized.metadata = normalized.metadata || {};
125
+ normalized.metadata.clientToolsRaw = jsonClone(toolsRaw);
126
+ }
127
+ }
128
+ catch {
129
+ // best-effort: do not block request handling due to tool snapshot failures
130
+ }
100
131
  if (isCompactionRequest(rawRequest)) {
101
132
  normalized.metadata = normalized.metadata || {};
102
133
  normalized.metadata.compactionRequest = true;
103
134
  }
135
+ const effectivePolicy = normalized.policyOverride ?? this.config.policy;
104
136
  const inboundAdapterContext = this.buildAdapterContext(normalized);
105
- const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint);
137
+ const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint, {
138
+ disableSnapshots: normalized.disableSnapshots === true
139
+ });
106
140
  const inboundStart = Date.now();
141
+ // Phase 0: observe client inbound payload violations (best-effort; no rewrites).
142
+ recordHubPolicyObservation({
143
+ policy: effectivePolicy,
144
+ providerProtocol: this.resolveClientProtocol(normalized.entryEndpoint),
145
+ payload: rawRequest,
146
+ phase: 'client_inbound',
147
+ stageRecorder: inboundRecorder,
148
+ requestId: normalized.id
149
+ });
107
150
  const formatEnvelope = await runReqInboundStage1FormatParse({
108
151
  rawRequest,
109
152
  adapterContext: inboundAdapterContext,
@@ -139,20 +182,26 @@ export class HubPipeline {
139
182
  }
140
183
  }
141
184
  });
185
+ // 将 VirtualRouter 层的 servertool 相关配置注入到 metadata,保证响应侧
186
+ // servertool(第三跳 reenter)也能访问到相同配置,即使当前 route 标记为 passthrough。
187
+ const metaBase = {
188
+ ...(normalized.metadata ?? {})
189
+ };
190
+ const webSearchConfig = this.config.virtualRouter?.webSearch;
191
+ if (webSearchConfig) {
192
+ metaBase.webSearch = webSearchConfig;
193
+ }
194
+ const execCommandGuard = this.config.virtualRouter?.execCommandGuard;
195
+ if (execCommandGuard) {
196
+ metaBase.execCommandGuard = execCommandGuard;
197
+ }
198
+ normalized.metadata = metaBase;
142
199
  let processedRequest;
143
200
  if (normalized.processMode !== 'passthrough') {
144
- const processMetadata = {
145
- ...(normalized.metadata ?? {})
146
- };
147
- const webSearchConfig = this.config.virtualRouter?.webSearch;
148
- if (webSearchConfig) {
149
- processMetadata.webSearch = webSearchConfig;
150
- }
151
- normalized.metadata = processMetadata;
152
201
  const processResult = await runReqProcessStage1ToolGovernance({
153
202
  request: standardizedRequest,
154
203
  rawPayload: rawRequest,
155
- metadata: processMetadata,
204
+ metadata: metaBase,
156
205
  entryEndpoint: normalized.entryEndpoint,
157
206
  requestId: normalized.id,
158
207
  stageRecorder: inboundRecorder
@@ -251,7 +300,9 @@ export class HubPipeline {
251
300
  // Snapshots must be grouped by entry endpoint (client-facing protocol), not by provider protocol.
252
301
  // Otherwise one request would be split across multiple folders (e.g. openai-responses + anthropic-messages),
253
302
  // which breaks codex-samples correlation.
254
- const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint);
303
+ const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint, {
304
+ disableSnapshots: normalized.disableSnapshots === true
305
+ });
255
306
  const outboundStart = Date.now();
256
307
  let providerPayload;
257
308
  const outboundStage1 = await runReqOutboundStage1SemanticMap({
@@ -273,7 +324,20 @@ export class HubPipeline {
273
324
  adapterContext: outboundAdapterContext,
274
325
  stageRecorder: outboundRecorder
275
326
  });
276
- providerPayload = formattedPayload;
327
+ providerPayload = applyHubProviderOutboundPolicy({
328
+ policy: effectivePolicy,
329
+ providerProtocol: outboundProtocol,
330
+ payload: formattedPayload,
331
+ stageRecorder: outboundRecorder,
332
+ requestId: normalized.id
333
+ });
334
+ recordHubPolicyObservation({
335
+ policy: effectivePolicy,
336
+ providerProtocol: outboundProtocol,
337
+ payload: providerPayload,
338
+ stageRecorder: outboundRecorder,
339
+ requestId: normalized.id
340
+ });
277
341
  const outboundEnd = Date.now();
278
342
  nodeResults.push({
279
343
  id: 'req_outbound',
@@ -305,11 +369,13 @@ export class HubPipeline {
305
369
  // 对于 capturedChatRequest,我们只需要一个“可读快照”,不会在后续流程中
306
370
  // 对其做就地修改,因此可以直接使用浅拷贝结构,避免序列化失败导致整段
307
371
  // 逻辑失效。
372
+ // Deep-clone a JSON-safe snapshot for servertool followups.
373
+ // Only capture the canonical Chat payload fields (model/messages/tools/parameters) to keep it serializable.
308
374
  const capturedChatRequest = {
309
375
  model: workingRequest.model,
310
- messages: Array.isArray(workingRequest.messages) ? [...workingRequest.messages] : workingRequest.messages,
311
- tools: workingRequest.tools,
312
- parameters: workingRequest.parameters
376
+ messages: jsonClone(workingRequest.messages),
377
+ tools: workingRequest.tools ? jsonClone(workingRequest.tools) : workingRequest.tools,
378
+ parameters: workingRequest.parameters ? jsonClone(workingRequest.parameters) : workingRequest.parameters
313
379
  };
314
380
  const metadata = {
315
381
  ...normalized.metadata,
@@ -335,6 +401,14 @@ export class HubPipeline {
335
401
  nodeResults
336
402
  };
337
403
  }
404
+ resolveClientProtocol(entryEndpoint) {
405
+ const lowered = String(entryEndpoint || '').toLowerCase();
406
+ if (lowered.includes('/v1/responses'))
407
+ return 'openai-responses';
408
+ if (lowered.includes('/v1/messages'))
409
+ return 'anthropic-messages';
410
+ return 'openai-chat';
411
+ }
338
412
  async execute(request) {
339
413
  const normalized = await this.normalizeRequest(request);
340
414
  const hooks = this.resolveProtocolHooks(normalized.providerProtocol);
@@ -429,6 +503,10 @@ export class HubPipeline {
429
503
  streamingHint,
430
504
  toolCallIdStyle
431
505
  };
506
+ const runtime = metadata.runtime;
507
+ if (runtime && typeof runtime === 'object' && !Array.isArray(runtime)) {
508
+ adapterContext.runtime = jsonClone(runtime);
509
+ }
432
510
  const clientRequestId = typeof metadata.clientRequestId === 'string'
433
511
  ? metadata.clientRequestId.trim()
434
512
  : '';
@@ -457,6 +535,11 @@ export class HubPipeline {
457
535
  adapterContext.serverToolFollowup = metadata
458
536
  .serverToolFollowup;
459
537
  }
538
+ // Preserve raw client tools (schemas) for response-side validation/formatting.
539
+ if (Object.prototype.hasOwnProperty.call(metadata, 'clientToolsRaw')) {
540
+ adapterContext.clientToolsRaw = metadata
541
+ .clientToolsRaw;
542
+ }
460
543
  const sessionId = typeof metadata.sessionId === 'string'
461
544
  ? metadata.sessionId.trim()
462
545
  : '';
@@ -512,7 +595,10 @@ export class HubPipeline {
512
595
  }
513
596
  return adapterContext;
514
597
  }
515
- maybeCreateStageRecorder(context, endpoint) {
598
+ maybeCreateStageRecorder(context, endpoint, options) {
599
+ if (options?.disableSnapshots === true) {
600
+ return undefined;
601
+ }
516
602
  if (!shouldRecordSnapshots()) {
517
603
  return undefined;
518
604
  }
@@ -539,6 +625,14 @@ export class HubPipeline {
539
625
  const metadataRecord = {
540
626
  ...(request.metadata ?? {})
541
627
  };
628
+ const policyOverride = extractHubPolicyOverride(metadataRecord);
629
+ if (Object.prototype.hasOwnProperty.call(metadataRecord, '__hubPolicyOverride')) {
630
+ delete metadataRecord.__hubPolicyOverride;
631
+ }
632
+ const disableSnapshots = metadataRecord.__disableHubSnapshots === true;
633
+ if (Object.prototype.hasOwnProperty.call(metadataRecord, '__disableHubSnapshots')) {
634
+ delete metadataRecord.__disableHubSnapshots;
635
+ }
542
636
  const entryEndpoint = typeof metadataRecord.entryEndpoint === 'string'
543
637
  ? normalizeEndpoint(metadataRecord.entryEndpoint)
544
638
  : endpoint;
@@ -576,6 +670,8 @@ export class HubPipeline {
576
670
  providerProtocol,
577
671
  payload,
578
672
  metadata: normalizedMetadata,
673
+ policyOverride: policyOverride ?? undefined,
674
+ disableSnapshots,
579
675
  processMode,
580
676
  direction,
581
677
  stage,
@@ -312,9 +312,6 @@ function buildApplyPatchDiagnostics(output) {
312
312
  }
313
313
  function appendDiagnosticsToRecord(record) {
314
314
  const name = typeof record.name === 'string' ? record.name.trim() : undefined;
315
- if (name !== 'apply_patch') {
316
- return;
317
- }
318
315
  let text;
319
316
  if (typeof record.output === 'string') {
320
317
  text = record.output;
@@ -329,6 +326,12 @@ function appendDiagnosticsToRecord(record) {
329
326
  if (!diag) {
330
327
  return;
331
328
  }
329
+ // Some providers / compatibility layers omit `name` on tool outputs.
330
+ // When the output text matches apply_patch argument-parse failures, still inject the diagnostics
331
+ // to keep user-visible behavior stable across modes.
332
+ if (name && name !== 'apply_patch') {
333
+ return;
334
+ }
332
335
  const merged = `${text}${diag}`;
333
336
  if (typeof record.output === 'string') {
334
337
  record.output = merged;
@@ -1,5 +1,6 @@
1
1
  import { runHubChatProcess } from '../../../../process/chat-process.js';
2
2
  import { recordStage } from '../../../stages/utils.js';
3
+ import { captureApplyPatchExecutionFailuresFromProcessedRequest } from '../../../../../../tools/apply-patch/execution-capturer.js';
3
4
  export async function runReqProcessStage1ToolGovernance(options) {
4
5
  const result = await runHubChatProcess({
5
6
  request: options.request,
@@ -10,6 +11,9 @@ export async function runReqProcessStage1ToolGovernance(options) {
10
11
  });
11
12
  if (result.processedRequest) {
12
13
  recordStage(options.stageRecorder, 'req_process_stage1_tool_governance', result.processedRequest);
14
+ // Best-effort: capture apply_patch execution failures reported by tool role messages.
15
+ // This is for errorsamples collection only and must not affect runtime behavior.
16
+ captureApplyPatchExecutionFailuresFromProcessedRequest(result.processedRequest);
13
17
  }
14
18
  return result;
15
19
  }
@@ -12,8 +12,10 @@ export function runRespOutboundStage1ClientRemap(options) {
12
12
  });
13
13
  }
14
14
  else {
15
+ const toolsRaw = resolveClientToolsRawFromContext(options.adapterContext);
15
16
  clientPayload = buildResponsesPayloadFromChat(options.payload, {
16
- requestId: options.requestId
17
+ requestId: options.requestId,
18
+ ...(toolsRaw ? { toolsRaw } : {})
17
19
  });
18
20
  }
19
21
  recordStage(options.stageRecorder, 'resp_outbound_stage1_client_remap', clientPayload);
@@ -41,3 +43,23 @@ function resolveAliasMapFromContext(adapterContext) {
41
43
  }
42
44
  return Object.keys(map).length ? map : undefined;
43
45
  }
46
+ function resolveClientToolsRawFromContext(adapterContext) {
47
+ if (!adapterContext) {
48
+ return undefined;
49
+ }
50
+ const candidate = adapterContext.clientToolsRaw;
51
+ if (!candidate || !Array.isArray(candidate)) {
52
+ return undefined;
53
+ }
54
+ const filtered = [];
55
+ for (const entry of candidate) {
56
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
57
+ const type = entry.type;
58
+ if (typeof type !== 'string' || !type.trim()) {
59
+ continue;
60
+ }
61
+ filtered.push(entry);
62
+ }
63
+ }
64
+ return filtered.length ? filtered : undefined;
65
+ }
@@ -0,0 +1,22 @@
1
+ import type { ChatEnvelope, AdapterContext } from '../types/chat-envelope.js';
2
+ import type { FormatAdapter, SemanticMapper, StageRecorder } from '../format-adapters/index.js';
3
+ import type { JsonObject } from '../types/json.js';
4
+ export type InboundStage = 'format_parse' | 'semantic_map_to_chat' | 'inbound_passthrough';
5
+ export interface InboundPassthroughConfig {
6
+ mode: 'chat';
7
+ factory: (raw: JsonObject, ctx: AdapterContext) => Promise<ChatEnvelope> | ChatEnvelope;
8
+ }
9
+ export interface InboundPlan {
10
+ protocol?: string;
11
+ stages: InboundStage[];
12
+ formatAdapter?: Pick<FormatAdapter, 'parseRequest'>;
13
+ semanticMapper?: Pick<SemanticMapper, 'toChat'>;
14
+ passthrough?: InboundPassthroughConfig;
15
+ }
16
+ export interface InboundPipelineOptions {
17
+ rawRequest: JsonObject;
18
+ context: AdapterContext;
19
+ plan: InboundPlan;
20
+ stageRecorder?: StageRecorder;
21
+ }
22
+ export declare function runInboundPipeline(options: InboundPipelineOptions): Promise<ChatEnvelope>;
@@ -0,0 +1,22 @@
1
+ import type { ChatEnvelope, AdapterContext } from '../types/chat-envelope.js';
2
+ import type { FormatAdapter, SemanticMapper, StageRecorder } from '../format-adapters/index.js';
3
+ import type { JsonObject } from '../types/json.js';
4
+ export type OutboundStage = 'semantic_map_from_chat' | 'format_build' | 'outbound_passthrough';
5
+ export interface OutboundPassthroughConfig {
6
+ mode: 'protocol';
7
+ factory: (chat: ChatEnvelope, ctx: AdapterContext) => Promise<JsonObject> | JsonObject;
8
+ }
9
+ export interface OutboundPlan {
10
+ protocol?: string;
11
+ stages: OutboundStage[];
12
+ formatAdapter?: Pick<FormatAdapter, 'buildResponse'>;
13
+ semanticMapper?: Pick<SemanticMapper, 'fromChat'>;
14
+ passthrough?: OutboundPassthroughConfig;
15
+ }
16
+ export interface OutboundPipelineOptions {
17
+ chat: ChatEnvelope;
18
+ context: AdapterContext;
19
+ plan: OutboundPlan;
20
+ stageRecorder?: StageRecorder;
21
+ }
22
+ export declare function runOutboundPipeline(options: OutboundPipelineOptions): Promise<JsonObject>;
@@ -0,0 +1,46 @@
1
+ import type { JsonObject } from '../types/json.js';
2
+ import type { StageRecorder } from '../format-adapters/index.js';
3
+ export type HubPolicyMode = 'off' | 'observe' | 'enforce';
4
+ export interface HubPolicyConfig {
5
+ mode?: HubPolicyMode;
6
+ /**
7
+ * Optional: sampling rate in [0, 1]. When provided, observation is best-effort
8
+ * and may skip recording for some requests.
9
+ */
10
+ sampleRate?: number;
11
+ }
12
+ export interface PolicyObservation {
13
+ phase: 'client_inbound' | 'provider_outbound' | 'provider_inbound' | 'client_outbound';
14
+ providerProtocol: string;
15
+ violations: Array<{
16
+ code: 'unexpected_wrapper' | 'unexpected_field';
17
+ path: string;
18
+ detail?: string;
19
+ }>;
20
+ summary: {
21
+ totalViolations: number;
22
+ unexpectedFieldCount: number;
23
+ };
24
+ }
25
+ export interface ProviderOutboundPolicyApplyResult {
26
+ payload: JsonObject;
27
+ changed: boolean;
28
+ removedTopLevelKeys: string[];
29
+ flattenedWrappers: string[];
30
+ }
31
+ export declare function setHubPolicyRuntimePolicy(policy?: HubPolicyConfig): void;
32
+ export declare function recordHubPolicyObservation(options: {
33
+ policy?: HubPolicyConfig;
34
+ phase?: PolicyObservation['phase'];
35
+ providerProtocol: string;
36
+ payload: JsonObject;
37
+ stageRecorder?: StageRecorder;
38
+ requestId?: string;
39
+ }): void;
40
+ export declare function applyHubProviderOutboundPolicy(options: {
41
+ policy?: HubPolicyConfig;
42
+ providerProtocol: string;
43
+ payload: JsonObject;
44
+ stageRecorder?: StageRecorder;
45
+ requestId?: string;
46
+ }): JsonObject;
@@ -0,0 +1,176 @@
1
+ import { resolveHubProtocolSpec } from './protocol-spec.js';
2
+ let hubPolicyRuntime = undefined;
3
+ export function setHubPolicyRuntimePolicy(policy) {
4
+ hubPolicyRuntime = policy;
5
+ }
6
+ function resolveEffectivePolicy(policy) {
7
+ return policy ?? hubPolicyRuntime;
8
+ }
9
+ function shouldSample(rate) {
10
+ if (rate === undefined)
11
+ return true;
12
+ if (!Number.isFinite(rate))
13
+ return true;
14
+ if (rate <= 0)
15
+ return false;
16
+ if (rate >= 1)
17
+ return true;
18
+ return Math.random() < rate;
19
+ }
20
+ function isJsonRecord(value) {
21
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
22
+ }
23
+ function applyProviderOutboundPolicy(providerProtocol, payload) {
24
+ const removedTopLevelKeys = [];
25
+ const flattenedWrappers = [];
26
+ const spec = resolveHubProtocolSpec(providerProtocol);
27
+ if (!spec.providerOutbound.enforceEnabled) {
28
+ return {
29
+ payload,
30
+ changed: false,
31
+ removedTopLevelKeys,
32
+ flattenedWrappers
33
+ };
34
+ }
35
+ let out = payload;
36
+ const ensureOutClone = () => {
37
+ if (out === payload) {
38
+ out = { ...payload };
39
+ }
40
+ };
41
+ // Reserved/private keys must never be sent upstream.
42
+ for (const key of Object.keys(payload)) {
43
+ if (spec.providerOutbound.reservedKeyPrefixes.some((prefix) => key.startsWith(prefix))) {
44
+ ensureOutClone();
45
+ delete out[key];
46
+ removedTopLevelKeys.push(key);
47
+ }
48
+ }
49
+ // Flatten accidental wrappers that have caused upstream 400s.
50
+ for (const rule of spec.providerOutbound.flattenWrappers) {
51
+ const wrapperKey = rule.wrapperKey;
52
+ if (!wrapperKey || typeof wrapperKey !== 'string') {
53
+ continue;
54
+ }
55
+ if (!isJsonRecord(out[wrapperKey])) {
56
+ continue;
57
+ }
58
+ ensureOutClone();
59
+ const inner = { ...out[wrapperKey] };
60
+ const alias = rule.aliasKeys || {};
61
+ for (const [from, to] of Object.entries(alias)) {
62
+ if (inner[to] === undefined && inner[from] !== undefined) {
63
+ inner[to] = inner[from];
64
+ }
65
+ }
66
+ const allow = Array.isArray(rule.allowKeys) ? new Set(rule.allowKeys) : null;
67
+ const onlyIfMissing = rule.onlyIfTargetMissing !== false;
68
+ for (const [key, value] of Object.entries(inner)) {
69
+ if (allow && !allow.has(key)) {
70
+ continue;
71
+ }
72
+ if (!onlyIfMissing || out[key] === undefined) {
73
+ out[key] = value;
74
+ }
75
+ }
76
+ delete out[wrapperKey];
77
+ flattenedWrappers.push(wrapperKey);
78
+ }
79
+ return {
80
+ payload: out,
81
+ changed: out !== payload || removedTopLevelKeys.length > 0 || flattenedWrappers.length > 0,
82
+ removedTopLevelKeys,
83
+ flattenedWrappers
84
+ };
85
+ }
86
+ function observeProviderOutboundPayload(providerProtocol, payload) {
87
+ const violations = [];
88
+ // V0 (observe-only): detect known "layout anti-patterns" and reserved keys.
89
+ // Do NOT modify payload here.
90
+ const spec = resolveHubProtocolSpec(providerProtocol);
91
+ for (const rule of spec.providerOutbound.forbidWrappers) {
92
+ if (rule.code !== 'forbid_wrapper') {
93
+ continue;
94
+ }
95
+ if (rule.path in payload && isJsonRecord(payload[rule.path])) {
96
+ violations.push({
97
+ code: 'unexpected_wrapper',
98
+ path: rule.path,
99
+ detail: rule.detail
100
+ });
101
+ }
102
+ }
103
+ // Always record unknown private wrapper keys (best-effort, conservative).
104
+ for (const key of Object.keys(payload)) {
105
+ if (spec.providerOutbound.reservedKeyPrefixes.some((prefix) => key.startsWith(prefix))) {
106
+ violations.push({
107
+ code: 'unexpected_field',
108
+ path: key
109
+ });
110
+ }
111
+ }
112
+ const unexpectedFieldCount = violations.filter((v) => v.code === 'unexpected_field').length;
113
+ return {
114
+ phase: 'provider_outbound',
115
+ providerProtocol,
116
+ violations,
117
+ summary: {
118
+ totalViolations: violations.length,
119
+ unexpectedFieldCount
120
+ }
121
+ };
122
+ }
123
+ export function recordHubPolicyObservation(options) {
124
+ if (!options.stageRecorder) {
125
+ return;
126
+ }
127
+ const effectivePolicy = resolveEffectivePolicy(options.policy);
128
+ const mode = effectivePolicy?.mode ?? 'off';
129
+ // Keep observing in enforce mode (best-effort) so operators can monitor
130
+ // violations while gradually turning enforcement on.
131
+ if (mode !== 'observe' && mode !== 'enforce') {
132
+ return;
133
+ }
134
+ if (!shouldSample(effectivePolicy?.sampleRate)) {
135
+ return;
136
+ }
137
+ try {
138
+ const phase = options.phase ?? 'provider_outbound';
139
+ const observation = observeProviderOutboundPayload(options.providerProtocol, options.payload);
140
+ observation.phase = phase;
141
+ if (observation.summary.totalViolations <= 0) {
142
+ return;
143
+ }
144
+ const stage = `hub_policy.observe.${phase}`;
145
+ options.stageRecorder.record(stage, {
146
+ requestId: options.requestId,
147
+ ...observation
148
+ });
149
+ }
150
+ catch {
151
+ // observe-only must never break the pipeline
152
+ }
153
+ }
154
+ export function applyHubProviderOutboundPolicy(options) {
155
+ const effectivePolicy = resolveEffectivePolicy(options.policy);
156
+ const mode = effectivePolicy?.mode ?? 'off';
157
+ if (mode !== 'enforce') {
158
+ return options.payload;
159
+ }
160
+ const result = applyProviderOutboundPolicy(options.providerProtocol, options.payload);
161
+ if (!result.changed) {
162
+ return options.payload;
163
+ }
164
+ try {
165
+ options.stageRecorder?.record('hub_policy.enforce.provider_outbound', {
166
+ requestId: options.requestId,
167
+ providerProtocol: options.providerProtocol,
168
+ removedTopLevelKeys: result.removedTopLevelKeys,
169
+ flattenedWrappers: result.flattenedWrappers
170
+ });
171
+ }
172
+ catch {
173
+ // policy enforcement recording must not break the pipeline
174
+ }
175
+ return result.payload;
176
+ }
@@ -0,0 +1,50 @@
1
+ export type HubProviderProtocol = 'openai-chat' | 'openai-responses' | 'anthropic-messages' | 'gemini-chat';
2
+ export interface ProviderOutboundLayoutRule {
3
+ code: 'forbid_wrapper';
4
+ path: string;
5
+ detail: string;
6
+ }
7
+ export interface ProviderOutboundWrapperFlattenRule {
8
+ wrapperKey: string;
9
+ /**
10
+ * When provided, only these keys from the wrapper are merged into top-level.
11
+ * When omitted, all keys are eligible.
12
+ */
13
+ allowKeys?: string[];
14
+ /**
15
+ * Key remap inside wrapper prior to merging (e.g. max_tokens -> max_output_tokens).
16
+ */
17
+ aliasKeys?: Record<string, string>;
18
+ /**
19
+ * Merge strategy: only write to target when top-level key is undefined.
20
+ */
21
+ onlyIfTargetMissing?: boolean;
22
+ }
23
+ export interface ProviderOutboundPolicySpec {
24
+ /**
25
+ * Whether provider outbound enforcement is enabled for this protocol.
26
+ * Keep this false for protocols not yet migrated, to avoid behavior changes.
27
+ */
28
+ enforceEnabled: boolean;
29
+ /**
30
+ * Reserved/private key prefixes that must not be sent upstream.
31
+ * (Enforced only when enforceEnabled=true.)
32
+ */
33
+ reservedKeyPrefixes: string[];
34
+ /**
35
+ * Wrappers that should not exist in provider outbound payload.
36
+ * observe: treat as violations; enforce: flatten/remove when configured.
37
+ */
38
+ forbidWrappers: ProviderOutboundLayoutRule[];
39
+ /**
40
+ * Best-effort fix for known wrapper anti-patterns by flattening into top-level.
41
+ * (Applied only when enforceEnabled=true.)
42
+ */
43
+ flattenWrappers: ProviderOutboundWrapperFlattenRule[];
44
+ }
45
+ export interface ProtocolSpec {
46
+ id: HubProviderProtocol;
47
+ providerOutbound: ProviderOutboundPolicySpec;
48
+ }
49
+ export declare const HUB_PROTOCOL_SPECS: Record<HubProviderProtocol, ProtocolSpec>;
50
+ export declare function resolveHubProtocolSpec(protocol: string): ProtocolSpec;