@jsonstudio/llms 0.6.802 → 0.6.938

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 (186) 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 +71 -14
  25. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +4 -0
  26. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +23 -1
  27. package/dist/conversion/hub/pipelines/inbound.d.ts +22 -0
  28. package/dist/conversion/hub/pipelines/outbound.d.ts +22 -0
  29. package/dist/conversion/hub/policy/policy-engine.d.ts +46 -0
  30. package/dist/conversion/hub/policy/policy-engine.js +176 -0
  31. package/dist/conversion/hub/policy/protocol-spec.d.ts +50 -0
  32. package/dist/conversion/hub/policy/protocol-spec.js +105 -0
  33. package/dist/conversion/hub/process/chat-process.d.ts +32 -0
  34. package/dist/conversion/hub/registry.d.ts +28 -0
  35. package/dist/conversion/hub/response/chat-response-utils.d.ts +6 -0
  36. package/dist/conversion/hub/response/provider-response.js +31 -0
  37. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +7 -0
  38. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +87 -1
  39. package/dist/conversion/hub/semantic-mappers/index.d.ts +4 -0
  40. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +21 -0
  41. package/dist/conversion/hub/standardized-bridge.d.ts +12 -0
  42. package/dist/conversion/hub/types/chat-schema.d.ts +112 -0
  43. package/dist/conversion/hub/types/errors.d.ts +5 -0
  44. package/dist/conversion/hub/types/format-envelope.d.ts +7 -0
  45. package/dist/conversion/hub/types/index.d.ts +6 -0
  46. package/dist/conversion/hub/types/json.d.ts +9 -0
  47. package/dist/conversion/hub/types/node.d.ts +31 -0
  48. package/dist/conversion/responses/responses-openai-bridge.js +263 -10
  49. package/dist/conversion/schema-validator.d.ts +7 -0
  50. package/dist/conversion/shared/args-mapping.d.ts +18 -0
  51. package/dist/conversion/shared/chat-request-filters.d.ts +9 -0
  52. package/dist/conversion/shared/errors.d.ts +1 -1
  53. package/dist/conversion/shared/gemini-tool-utils.js +61 -0
  54. package/dist/conversion/shared/jsonish.d.ts +3 -0
  55. package/dist/conversion/shared/mcp-injection.d.ts +2 -0
  56. package/dist/conversion/shared/media.d.ts +1 -0
  57. package/dist/conversion/shared/openai-message-normalize.d.ts +1 -0
  58. package/dist/conversion/shared/payload-budget.d.ts +13 -0
  59. package/dist/conversion/shared/reasoning-mapping.d.ts +5 -0
  60. package/dist/conversion/shared/responses-request-adapter.d.ts +1 -28
  61. package/dist/conversion/shared/responses-request-adapter.js +1 -430
  62. package/dist/conversion/shared/snapshot-hooks.js +58 -3
  63. package/dist/conversion/shared/tool-governor.js +8 -2
  64. package/dist/conversion/shared/tool-harvester.d.ts +31 -0
  65. package/dist/conversion/shared/tool-mapping.js +10 -29
  66. package/dist/conversion/types.d.ts +33 -0
  67. package/dist/filters/builtin/add-fields-filter.d.ts +8 -0
  68. package/dist/filters/builtin/blacklist-filter.d.ts +8 -0
  69. package/dist/filters/builtin/whitelist-filter.d.ts +8 -0
  70. package/dist/filters/engine.d.ts +16 -0
  71. package/dist/filters/special/request-tool-choice-policy.d.ts +11 -0
  72. package/dist/filters/special/response-finish-invariants.d.ts +11 -0
  73. package/dist/filters/special/response-openai-to-responses-bridge.d.ts +13 -0
  74. package/dist/filters/special/response-tool-arguments-blacklist.d.ts +12 -0
  75. package/dist/filters/special/response-tool-arguments-schema-converge.d.ts +13 -0
  76. package/dist/filters/special/response-tool-arguments-stringify.d.ts +9 -0
  77. package/dist/filters/special/response-tool-arguments-whitelist.d.ts +11 -0
  78. package/dist/filters/special/tool-filter-hooks.d.ts +19 -0
  79. package/dist/filters/special/tool-post-constraints.d.ts +31 -0
  80. package/dist/filters/types.d.ts +68 -0
  81. package/dist/filters/utils/fieldmap-loader.d.ts +2 -0
  82. package/dist/filters/utils/snapshot-writer.d.ts +10 -0
  83. package/dist/guidance/index.d.ts +3 -0
  84. package/dist/guidance/index.js +78 -83
  85. package/dist/http/sse-response.d.ts +22 -0
  86. package/dist/router/virtual-router/bootstrap.d.ts +6 -0
  87. package/dist/router/virtual-router/bootstrap.js +49 -5
  88. package/dist/router/virtual-router/classifier.d.ts +10 -0
  89. package/dist/router/virtual-router/engine-selection.js +98 -11
  90. package/dist/router/virtual-router/engine.js +177 -31
  91. package/dist/router/virtual-router/error-center.d.ts +10 -0
  92. package/dist/router/virtual-router/features.d.ts +3 -0
  93. package/dist/router/virtual-router/routing-instructions.d.ts +23 -1
  94. package/dist/router/virtual-router/routing-instructions.js +120 -30
  95. package/dist/router/virtual-router/types.d.ts +11 -0
  96. package/dist/servertool/engine.js +189 -16
  97. package/dist/servertool/handlers/apply-patch-guard.js +269 -0
  98. package/dist/servertool/handlers/exec-command-guard.js +558 -0
  99. package/dist/servertool/handlers/followup-message-trimmer.d.ts +16 -0
  100. package/dist/servertool/handlers/followup-message-trimmer.js +198 -0
  101. package/dist/servertool/handlers/followup-request-builder.d.ts +17 -0
  102. package/dist/servertool/handlers/followup-request-builder.js +122 -0
  103. package/dist/servertool/handlers/gemini-empty-reply-continue.js +252 -51
  104. package/dist/servertool/handlers/iflow-model-error-retry.js +12 -22
  105. package/dist/servertool/handlers/stop-message-auto.js +237 -75
  106. package/dist/servertool/handlers/vision.js +15 -27
  107. package/dist/servertool/handlers/web-search.js +17 -43
  108. package/dist/servertool/server-side-tools.d.ts +3 -0
  109. package/dist/servertool/server-side-tools.js +3 -0
  110. package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.d.ts +2 -1
  111. package/dist/sse/json-to-sse/chat-json-to-sse-converter.d.ts +80 -0
  112. package/dist/sse/json-to-sse/event-generators/chat.d.ts +55 -0
  113. package/dist/sse/json-to-sse/event-generators/responses.d.ts +99 -0
  114. package/dist/sse/json-to-sse/gemini-json-to-sse-converter.d.ts +2 -1
  115. package/dist/sse/json-to-sse/responses-json-to-sse-converter.d.ts +80 -0
  116. package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.d.ts +1 -1
  117. package/dist/sse/json-to-sse/sequencers/chat-sequencer.d.ts +2 -2
  118. package/dist/sse/json-to-sse/sequencers/gemini-sequencer.d.ts +1 -1
  119. package/dist/sse/json-to-sse/sequencers/responses-sequencer.d.ts +40 -0
  120. package/dist/sse/shared/chat-serializer.d.ts +4 -0
  121. package/dist/sse/shared/constants.d.ts +272 -0
  122. package/dist/sse/shared/serializers/anthropic-event-serializer.d.ts +1 -1
  123. package/dist/sse/shared/serializers/base-serializer.d.ts +158 -0
  124. package/dist/sse/shared/serializers/chat-event-serializer.d.ts +82 -0
  125. package/dist/sse/shared/serializers/gemini-event-serializer.d.ts +1 -1
  126. package/dist/sse/shared/serializers/index.d.ts +2 -1
  127. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +123 -0
  128. package/dist/sse/shared/serializers/types.d.ts +51 -0
  129. package/dist/sse/shared/utils.d.ts +254 -0
  130. package/dist/sse/shared/writer.d.ts +2 -2
  131. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -1
  132. package/dist/sse/sse-to-json/builders/anthropic-response-builder.d.ts +1 -1
  133. package/dist/sse/sse-to-json/builders/response-builder.d.ts +1 -1
  134. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +2 -1
  135. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.d.ts +2 -1
  136. package/dist/sse/sse-to-json/parsers/sse-parser.d.ts +73 -0
  137. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  138. package/dist/sse/types/chat-types.d.ts +1 -1
  139. package/dist/sse/types/responses-types.d.ts +1 -1
  140. package/dist/tools/apply-patch/execution-capturer.d.ts +13 -0
  141. package/dist/tools/apply-patch/execution-capturer.js +158 -0
  142. package/dist/tools/apply-patch/regression-capturer.d.ts +1 -0
  143. package/dist/tools/apply-patch/regression-capturer.js +5 -4
  144. package/dist/tools/apply-patch/structured.js +109 -13
  145. package/dist/tools/apply-patch/validator.js +112 -18
  146. package/dist/tools/tool-registry.d.ts +8 -0
  147. package/dist/tools/tool-registry.js +2 -1
  148. package/package.json +4 -4
  149. package/dist/conversion/compat/actions/apply-patch-format-fixer.js +0 -233
  150. package/dist/conversion/config/compat-profiles.json +0 -38
  151. package/dist/conversion/hub/pipeline/context-limit.d.ts +0 -13
  152. package/dist/conversion/hub/pipeline/context-limit.js +0 -55
  153. package/dist/conversion/hub/response/server-side-tools.d.ts +0 -26
  154. package/dist/conversion/hub/response/server-side-tools.js +0 -383
  155. package/dist/conversion/shared/bridge-conversation-store.d.ts +0 -41
  156. package/dist/conversion/shared/bridge-conversation-store.js +0 -279
  157. package/dist/conversion/shared/bridge-request-adapter.d.ts +0 -28
  158. package/dist/conversion/shared/bridge-request-adapter.js +0 -430
  159. package/dist/conversion/shared/responses-id-utils.js +0 -42
  160. package/dist/conversion/shared/responses-instructions.js +0 -113
  161. package/dist/conversion/shared/responses-message-utils.d.ts +0 -15
  162. package/dist/conversion/shared/responses-message-utils.js +0 -206
  163. package/dist/conversion/shared/responses-metadata.js +0 -1
  164. package/dist/conversion/shared/responses-output-utils.d.ts +0 -7
  165. package/dist/conversion/shared/responses-output-utils.js +0 -108
  166. package/dist/conversion/shared/responses-types.d.ts +0 -33
  167. package/dist/conversion/shared/tool-normalizers.d.ts +0 -4
  168. package/dist/conversion/shared/tool-normalizers.js +0 -84
  169. package/dist/filters/special/request-streaming-to-nonstreaming.d.ts +0 -13
  170. package/dist/filters/special/request-streaming-to-nonstreaming.js +0 -39
  171. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +0 -23
  172. package/dist/filters/special/response-apply-patch-toon-decode.js +0 -460
  173. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +0 -10
  174. package/dist/filters/special/response-tool-arguments-toon-decode.js +0 -154
  175. package/dist/servertool/flow-types.d.ts +0 -40
  176. package/dist/servertool/flow-types.js +0 -1
  177. package/dist/servertool/orchestration-types.d.ts +0 -33
  178. package/dist/servertool/orchestration-types.js +0 -1
  179. package/dist/servertool/vision-tool.d.ts +0 -2
  180. package/dist/servertool/vision-tool.js +0 -185
  181. package/dist/tools/patch-args-normalizer.d.ts +0 -15
  182. package/dist/tools/patch-args-normalizer.js +0 -472
  183. package/dist/utils/toon.d.ts +0 -4
  184. package/dist/utils/toon.js +0 -75
  185. /package/dist/{conversion/compat/actions/apply-patch-format-fixer.d.ts → servertool/handlers/apply-patch-guard.d.ts} +0 -0
  186. /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,7 @@ 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';
28
29
  export class HubPipeline {
29
30
  routerEngine;
30
31
  config;
@@ -37,6 +38,7 @@ export class HubPipeline {
37
38
  quotaView: config.quotaView
38
39
  });
39
40
  this.routerEngine.initialize(config.virtualRouter);
41
+ setHubPolicyRuntimePolicy(config.policy);
40
42
  try {
41
43
  this.unsubscribeProviderErrors = providerErrorCenter.subscribe((event) => {
42
44
  try {
@@ -97,6 +99,19 @@ export class HubPipeline {
97
99
  const formatAdapter = hooks.createFormatAdapter();
98
100
  const semanticMapper = hooks.createSemanticMapper();
99
101
  const rawRequest = this.asJsonObject(normalized.payload);
102
+ // Preserve the client-provided raw tool definitions (schema included) for response-side validation
103
+ // and protocol-correct outbound tool-call formatting. This must reflect the inbound request shape,
104
+ // not the governed/augmented tools.
105
+ try {
106
+ const toolsRaw = Array.isArray(rawRequest?.tools) ? rawRequest.tools : null;
107
+ if (toolsRaw && toolsRaw.length > 0) {
108
+ normalized.metadata = normalized.metadata || {};
109
+ normalized.metadata.clientToolsRaw = jsonClone(toolsRaw);
110
+ }
111
+ }
112
+ catch {
113
+ // best-effort: do not block request handling due to tool snapshot failures
114
+ }
100
115
  if (isCompactionRequest(rawRequest)) {
101
116
  normalized.metadata = normalized.metadata || {};
102
117
  normalized.metadata.compactionRequest = true;
@@ -104,6 +119,14 @@ export class HubPipeline {
104
119
  const inboundAdapterContext = this.buildAdapterContext(normalized);
105
120
  const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint);
106
121
  const inboundStart = Date.now();
122
+ // Phase 0: observe client inbound payload violations (best-effort; no rewrites).
123
+ recordHubPolicyObservation({
124
+ providerProtocol: this.resolveClientProtocol(normalized.entryEndpoint),
125
+ payload: rawRequest,
126
+ phase: 'client_inbound',
127
+ stageRecorder: inboundRecorder,
128
+ requestId: normalized.id
129
+ });
107
130
  const formatEnvelope = await runReqInboundStage1FormatParse({
108
131
  rawRequest,
109
132
  adapterContext: inboundAdapterContext,
@@ -139,20 +162,26 @@ export class HubPipeline {
139
162
  }
140
163
  }
141
164
  });
165
+ // 将 VirtualRouter 层的 servertool 相关配置注入到 metadata,保证响应侧
166
+ // servertool(第三跳 reenter)也能访问到相同配置,即使当前 route 标记为 passthrough。
167
+ const metaBase = {
168
+ ...(normalized.metadata ?? {})
169
+ };
170
+ const webSearchConfig = this.config.virtualRouter?.webSearch;
171
+ if (webSearchConfig) {
172
+ metaBase.webSearch = webSearchConfig;
173
+ }
174
+ const execCommandGuard = this.config.virtualRouter?.execCommandGuard;
175
+ if (execCommandGuard) {
176
+ metaBase.execCommandGuard = execCommandGuard;
177
+ }
178
+ normalized.metadata = metaBase;
142
179
  let processedRequest;
143
180
  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
181
  const processResult = await runReqProcessStage1ToolGovernance({
153
182
  request: standardizedRequest,
154
183
  rawPayload: rawRequest,
155
- metadata: processMetadata,
184
+ metadata: metaBase,
156
185
  entryEndpoint: normalized.entryEndpoint,
157
186
  requestId: normalized.id,
158
187
  stageRecorder: inboundRecorder
@@ -273,7 +302,20 @@ export class HubPipeline {
273
302
  adapterContext: outboundAdapterContext,
274
303
  stageRecorder: outboundRecorder
275
304
  });
276
- providerPayload = formattedPayload;
305
+ providerPayload = applyHubProviderOutboundPolicy({
306
+ policy: this.config.policy,
307
+ providerProtocol: outboundProtocol,
308
+ payload: formattedPayload,
309
+ stageRecorder: outboundRecorder,
310
+ requestId: normalized.id
311
+ });
312
+ recordHubPolicyObservation({
313
+ policy: this.config.policy,
314
+ providerProtocol: outboundProtocol,
315
+ payload: providerPayload,
316
+ stageRecorder: outboundRecorder,
317
+ requestId: normalized.id
318
+ });
277
319
  const outboundEnd = Date.now();
278
320
  nodeResults.push({
279
321
  id: 'req_outbound',
@@ -305,11 +347,13 @@ export class HubPipeline {
305
347
  // 对于 capturedChatRequest,我们只需要一个“可读快照”,不会在后续流程中
306
348
  // 对其做就地修改,因此可以直接使用浅拷贝结构,避免序列化失败导致整段
307
349
  // 逻辑失效。
350
+ // Deep-clone a JSON-safe snapshot for servertool followups.
351
+ // Only capture the canonical Chat payload fields (model/messages/tools/parameters) to keep it serializable.
308
352
  const capturedChatRequest = {
309
353
  model: workingRequest.model,
310
- messages: Array.isArray(workingRequest.messages) ? [...workingRequest.messages] : workingRequest.messages,
311
- tools: workingRequest.tools,
312
- parameters: workingRequest.parameters
354
+ messages: jsonClone(workingRequest.messages),
355
+ tools: workingRequest.tools ? jsonClone(workingRequest.tools) : workingRequest.tools,
356
+ parameters: workingRequest.parameters ? jsonClone(workingRequest.parameters) : workingRequest.parameters
313
357
  };
314
358
  const metadata = {
315
359
  ...normalized.metadata,
@@ -335,6 +379,14 @@ export class HubPipeline {
335
379
  nodeResults
336
380
  };
337
381
  }
382
+ resolveClientProtocol(entryEndpoint) {
383
+ const lowered = String(entryEndpoint || '').toLowerCase();
384
+ if (lowered.includes('/v1/responses'))
385
+ return 'openai-responses';
386
+ if (lowered.includes('/v1/messages'))
387
+ return 'anthropic-messages';
388
+ return 'openai-chat';
389
+ }
338
390
  async execute(request) {
339
391
  const normalized = await this.normalizeRequest(request);
340
392
  const hooks = this.resolveProtocolHooks(normalized.providerProtocol);
@@ -457,6 +509,11 @@ export class HubPipeline {
457
509
  adapterContext.serverToolFollowup = metadata
458
510
  .serverToolFollowup;
459
511
  }
512
+ // Preserve raw client tools (schemas) for response-side validation/formatting.
513
+ if (Object.prototype.hasOwnProperty.call(metadata, 'clientToolsRaw')) {
514
+ adapterContext.clientToolsRaw = metadata
515
+ .clientToolsRaw;
516
+ }
460
517
  const sessionId = typeof metadata.sessionId === 'string'
461
518
  ? metadata.sessionId.trim()
462
519
  : '';
@@ -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;
@@ -0,0 +1,105 @@
1
+ const RESPONSES_SPEC = {
2
+ id: 'openai-responses',
3
+ providerOutbound: {
4
+ enforceEnabled: true,
5
+ forbidWrappers: [
6
+ {
7
+ code: 'forbid_wrapper',
8
+ path: 'parameters',
9
+ detail: 'Responses provider payload must not contain a top-level parameters wrapper (expects flattened fields).'
10
+ },
11
+ {
12
+ code: 'forbid_wrapper',
13
+ path: 'request',
14
+ detail: 'Responses provider payload must not contain a nested request wrapper.'
15
+ }
16
+ ],
17
+ reservedKeyPrefixes: ['__', '_'],
18
+ flattenWrappers: [
19
+ {
20
+ wrapperKey: 'request',
21
+ onlyIfTargetMissing: true
22
+ },
23
+ {
24
+ wrapperKey: 'parameters',
25
+ onlyIfTargetMissing: true,
26
+ aliasKeys: {
27
+ max_tokens: 'max_output_tokens'
28
+ },
29
+ allowKeys: [
30
+ 'temperature',
31
+ 'top_p',
32
+ 'max_output_tokens',
33
+ 'seed',
34
+ 'logit_bias',
35
+ 'user',
36
+ 'parallel_tool_calls',
37
+ 'tool_choice',
38
+ 'response_format',
39
+ 'stream',
40
+ 'stop',
41
+ 'stop_sequences',
42
+ 'modalities',
43
+ 'top_k'
44
+ ]
45
+ }
46
+ ]
47
+ }
48
+ };
49
+ const DEFAULT_SPEC = {
50
+ id: 'openai-chat',
51
+ providerOutbound: {
52
+ enforceEnabled: false,
53
+ forbidWrappers: [
54
+ {
55
+ code: 'forbid_wrapper',
56
+ path: 'parameters',
57
+ detail: 'OpenAI Chat provider payload must not contain a top-level parameters wrapper (expects flattened fields).'
58
+ },
59
+ {
60
+ code: 'forbid_wrapper',
61
+ path: 'request',
62
+ detail: 'OpenAI Chat provider payload must not contain a nested request wrapper.'
63
+ }
64
+ ],
65
+ reservedKeyPrefixes: ['__', '_'],
66
+ flattenWrappers: []
67
+ }
68
+ };
69
+ export const HUB_PROTOCOL_SPECS = {
70
+ 'openai-chat': DEFAULT_SPEC,
71
+ 'openai-responses': RESPONSES_SPEC,
72
+ 'anthropic-messages': {
73
+ id: 'anthropic-messages',
74
+ providerOutbound: {
75
+ enforceEnabled: false,
76
+ forbidWrappers: [
77
+ {
78
+ code: 'forbid_wrapper',
79
+ path: 'parameters',
80
+ detail: 'Anthropic Messages provider payload must not contain a top-level parameters wrapper.'
81
+ },
82
+ {
83
+ code: 'forbid_wrapper',
84
+ path: 'request',
85
+ detail: 'Anthropic Messages provider payload must not contain a nested request wrapper.'
86
+ }
87
+ ],
88
+ reservedKeyPrefixes: ['__', '_'],
89
+ flattenWrappers: []
90
+ }
91
+ },
92
+ 'gemini-chat': {
93
+ id: 'gemini-chat',
94
+ providerOutbound: {
95
+ enforceEnabled: false,
96
+ forbidWrappers: [],
97
+ reservedKeyPrefixes: ['__', '_'],
98
+ flattenWrappers: []
99
+ }
100
+ }
101
+ };
102
+ export function resolveHubProtocolSpec(protocol) {
103
+ const normalized = (protocol || '').trim().toLowerCase();
104
+ return HUB_PROTOCOL_SPECS[normalized] ?? DEFAULT_SPEC;
105
+ }
@@ -0,0 +1,32 @@
1
+ import type { ProcessedRequest, StandardizedRequest } from '../types/standardized.js';
2
+ import type { JsonObject } from '../types/json.js';
3
+ export interface HubChatProcessOptions {
4
+ request: StandardizedRequest;
5
+ requestId: string;
6
+ entryEndpoint: string;
7
+ rawPayload: Record<string, unknown>;
8
+ metadata: Record<string, unknown>;
9
+ }
10
+ export interface HubChatProcessResult {
11
+ processedRequest?: ProcessedRequest;
12
+ nodeResult: HubProcessNodeResult;
13
+ }
14
+ export interface HubProcessNodeResult {
15
+ success: boolean;
16
+ metadata: {
17
+ node: string;
18
+ executionTime: number;
19
+ startTime: number;
20
+ endTime: number;
21
+ dataProcessed?: {
22
+ messages: number;
23
+ tools: number;
24
+ };
25
+ };
26
+ error?: {
27
+ code?: string;
28
+ message: string;
29
+ details?: JsonObject;
30
+ };
31
+ }
32
+ export declare function runHubChatProcess(options: HubChatProcessOptions): Promise<HubChatProcessResult>;