@jsonstudio/llms 0.6.1164 → 0.6.1354

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 (164) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.d.ts +3 -1
  2. package/dist/conversion/codecs/gemini-openai-codec.js +10 -4
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -1
  4. package/dist/conversion/compat/actions/gemini-web-search.js +5 -2
  5. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +12 -0
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +199 -0
  7. package/dist/conversion/compat/actions/iflow-web-search.d.ts +1 -1
  8. package/dist/conversion/compat/actions/iflow-web-search.js +5 -2
  9. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
  10. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
  11. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +523 -50
  12. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
  13. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  14. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
  16. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +134 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
  18. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
  19. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
  20. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +384 -0
  25. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
  26. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
  27. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
  28. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
  30. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
  31. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
  32. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
  33. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
  34. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
  35. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
  36. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
  37. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
  38. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
  39. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
  40. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
  41. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
  42. package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
  43. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
  44. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
  45. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
  46. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
  47. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
  48. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
  49. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
  50. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
  51. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
  52. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
  53. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
  54. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
  55. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
  56. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
  57. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
  58. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
  59. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
  60. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
  61. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
  62. package/dist/conversion/hub/pipeline/target-utils.js +9 -5
  63. package/dist/conversion/hub/process/chat-process.js +256 -16
  64. package/dist/conversion/hub/response/provider-response.d.ts +8 -0
  65. package/dist/conversion/hub/response/provider-response.js +85 -27
  66. package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
  67. package/dist/conversion/hub/response/response-mappers.js +30 -6
  68. package/dist/conversion/hub/response/response-runtime.js +4 -38
  69. package/dist/conversion/hub/snapshot-recorder.js +5 -1
  70. package/dist/conversion/hub/standardized-bridge.js +23 -15
  71. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
  72. package/dist/conversion/responses/responses-openai-bridge.js +20 -4
  73. package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
  74. package/dist/conversion/shared/gemini-tool-utils.js +580 -108
  75. package/dist/conversion/shared/jsonish.js +1 -1
  76. package/dist/conversion/shared/mcp-injection.js +67 -33
  77. package/dist/conversion/shared/openai-finalizer.js +2 -1
  78. package/dist/conversion/shared/openai-message-normalize.js +76 -21
  79. package/dist/conversion/shared/responses-output-builder.js +6 -0
  80. package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
  81. package/dist/conversion/shared/runtime-metadata.js +23 -0
  82. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  83. package/dist/conversion/shared/text-markup-normalizer.js +284 -4
  84. package/dist/conversion/shared/tool-canonicalizer.js +2 -1
  85. package/dist/conversion/shared/tool-governor.js +3 -3
  86. package/dist/filters/engine.js +5 -5
  87. package/dist/filters/special/request-tool-list-filter.js +194 -60
  88. package/dist/filters/special/request-tools-normalize.js +1 -1
  89. package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
  90. package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
  91. package/dist/filters/special/tool-filter-hooks.js +58 -62
  92. package/dist/guidance/index.js +5 -1
  93. package/dist/http/sse-response.js +6 -6
  94. package/dist/router/virtual-router/bootstrap.js +65 -5
  95. package/dist/router/virtual-router/context-advisor.d.ts +4 -0
  96. package/dist/router/virtual-router/context-advisor.js +3 -0
  97. package/dist/router/virtual-router/context-weighted.d.ts +31 -0
  98. package/dist/router/virtual-router/context-weighted.js +54 -0
  99. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  100. package/dist/router/virtual-router/engine-health.js +11 -110
  101. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
  102. package/dist/router/virtual-router/engine-selection/alias-selection.js +156 -0
  103. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
  104. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
  105. package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
  106. package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
  107. package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
  108. package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
  109. package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
  110. package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
  111. package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
  112. package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
  113. package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
  114. package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
  115. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
  116. package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
  117. package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
  118. package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
  119. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
  120. package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
  121. package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
  122. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +400 -0
  123. package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
  124. package/dist/router/virtual-router/engine-selection/tier-selection.js +225 -0
  125. package/dist/router/virtual-router/engine-selection.d.ts +4 -30
  126. package/dist/router/virtual-router/engine-selection.js +10 -815
  127. package/dist/router/virtual-router/engine.d.ts +1 -0
  128. package/dist/router/virtual-router/engine.js +55 -10
  129. package/dist/router/virtual-router/routing-instructions.js +6 -1
  130. package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
  131. package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
  132. package/dist/router/virtual-router/types.d.ts +53 -1
  133. package/dist/servertool/clock/config.d.ts +8 -0
  134. package/dist/servertool/clock/config.js +22 -0
  135. package/dist/servertool/clock/log.d.ts +3 -0
  136. package/dist/servertool/clock/log.js +13 -0
  137. package/dist/servertool/clock/task-store.d.ts +1 -1
  138. package/dist/servertool/clock/task-store.js +1 -1
  139. package/dist/servertool/clock/tasks.js +1 -1
  140. package/dist/servertool/engine.js +146 -21
  141. package/dist/servertool/handlers/clock-auto.js +11 -6
  142. package/dist/servertool/handlers/clock.js +36 -10
  143. package/dist/servertool/handlers/followup-request-builder.js +8 -2
  144. package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
  145. package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
  146. package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
  147. package/dist/servertool/handlers/stop-message-auto.js +100 -10
  148. package/dist/servertool/handlers/vision.js +4 -1
  149. package/dist/servertool/handlers/web-search.js +3 -1
  150. package/dist/servertool/pending-session.d.ts +19 -0
  151. package/dist/servertool/pending-session.js +97 -0
  152. package/dist/servertool/reenter-backend.js +5 -3
  153. package/dist/servertool/server-side-tools.js +235 -6
  154. package/dist/servertool/types.d.ts +13 -0
  155. package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
  156. package/dist/sse/shared/chat-serializer.js +2 -2
  157. package/dist/sse/shared/constants.js +1 -1
  158. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
  159. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  160. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  161. package/dist/tools/apply-patch/execution-capturer.js +1 -1
  162. package/dist/tools/exec-command/normalize.js +4 -0
  163. package/dist/tools/exec-command/regression-capturer.js +1 -1
  164. package/package.json +10 -5
@@ -1,111 +1,10 @@
1
- import { Readable } from 'node:stream';
2
- import { isJsonObject, jsonClone } from '../types/json.js';
3
1
  import { VirtualRouterEngine } from '../../../router/virtual-router/engine.js';
4
2
  import { providerErrorCenter } from '../../../router/virtual-router/error-center.js';
5
- import { defaultSseCodecRegistry } from '../../../sse/index.js';
6
- import { ResponsesFormatAdapter } from '../format-adapters/responses-format-adapter.js';
7
- import { ResponsesSemanticMapper } from '../semantic-mappers/responses-mapper.js';
8
- import { AnthropicFormatAdapter } from '../format-adapters/anthropic-format-adapter.js';
9
- import { AnthropicSemanticMapper } from '../semantic-mappers/anthropic-mapper.js';
10
- import { GeminiFormatAdapter } from '../format-adapters/gemini-format-adapter.js';
11
- import { GeminiSemanticMapper } from '../semantic-mappers/gemini-mapper.js';
12
- import { ChatFormatAdapter } from '../format-adapters/chat-format-adapter.js';
13
- import { ChatSemanticMapper } from '../semantic-mappers/chat-mapper.js';
14
- import { createSnapshotRecorder } from '../snapshot-recorder.js';
15
- import { shouldRecordSnapshots } from '../../shared/snapshot-utils.js';
16
- import { runReqInboundStage1FormatParse } from './stages/req_inbound/req_inbound_stage1_format_parse/index.js';
17
- import { runReqInboundStage2SemanticMap } from './stages/req_inbound/req_inbound_stage2_semantic_map/index.js';
18
- import { runChatContextCapture, captureResponsesContextSnapshot } from './stages/req_inbound/req_inbound_stage3_context_capture/index.js';
19
- import { createResponsesContextCapture, createNoopContextCapture } from './stages/req_inbound/req_inbound_stage3_context_capture/context-factories.js';
20
- import { runReqProcessStage1ToolGovernance } from './stages/req_process/req_process_stage1_tool_governance/index.js';
21
- import { runReqProcessStage2RouteSelect } from './stages/req_process/req_process_stage2_route_select/index.js';
22
- import { runReqOutboundStage1SemanticMap } from './stages/req_outbound/req_outbound_stage1_semantic_map/index.js';
23
- import { runReqOutboundStage2FormatBuild } from './stages/req_outbound/req_outbound_stage2_format_build/index.js';
24
- import { runReqOutboundStage3Compat } from './stages/req_outbound/req_outbound_stage3_compat/index.js';
25
- import { extractSessionIdentifiersFromMetadata } from './session-identifiers.js';
26
- import { computeRequestTokens } from '../../../router/virtual-router/token-estimator.js';
27
- import { isCompactionRequest } from '../../shared/compaction-detect.js';
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
- }
3
+ import { setHubPolicyRuntimePolicy } from '../policy/policy-engine.js';
4
+ import { executeChatProcessEntryPipeline } from './hub-pipeline/execute-chat-process-entry.js';
5
+ import { executeRequestStagePipeline } from './hub-pipeline/execute-request-stage.js';
6
+ import { normalizeHubPipelineRequest } from './hub-pipeline/payload-normalize.js';
7
+ import { resolveProtocolHooks } from './hub-pipeline/resolve-protocol-hooks.js';
109
8
  export class HubPipeline {
110
9
  routerEngine;
111
10
  config;
@@ -175,1187 +74,26 @@ export class HubPipeline {
175
74
  this.unsubscribeProviderErrors = undefined;
176
75
  }
177
76
  }
178
- async executeRequestStagePipeline(normalized, hooks) {
179
- const formatAdapter = hooks.createFormatAdapter();
180
- const semanticMapper = hooks.createSemanticMapper();
181
- const rawRequest = this.asJsonObject(normalized.payload);
182
- // Preserve the client-provided raw tool definitions (schema included) for response-side validation
183
- // and protocol-correct outbound tool-call formatting. This must reflect the inbound request shape,
184
- // not the governed/augmented tools.
185
- try {
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
- }
192
- if (toolsRaw && toolsRaw.length > 0) {
193
- normalized.metadata = normalized.metadata || {};
194
- normalized.metadata.clientToolsRaw = jsonClone(toolsRaw);
195
- }
196
- }
197
- catch {
198
- // best-effort: do not block request handling due to tool snapshot failures
199
- }
200
- if (isCompactionRequest(rawRequest)) {
201
- normalized.metadata = normalized.metadata || {};
202
- normalized.metadata.compactionRequest = true;
203
- }
204
- const effectivePolicy = normalized.policyOverride ?? this.config.policy;
205
- const shadowCompareBaselineMode = normalized.shadowCompare?.baselineMode;
206
- const inboundAdapterContext = this.buildAdapterContext(normalized);
207
- const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint, {
208
- disableSnapshots: normalized.disableSnapshots === true
209
- });
210
- const inboundStart = Date.now();
211
- // Phase 0: observe client inbound payload violations (best-effort; no rewrites).
212
- recordHubPolicyObservation({
213
- policy: effectivePolicy,
214
- providerProtocol: this.resolveClientProtocol(normalized.entryEndpoint),
215
- payload: rawRequest,
216
- phase: 'client_inbound',
217
- stageRecorder: inboundRecorder,
218
- requestId: normalized.id
219
- });
220
- const formatEnvelope = await runReqInboundStage1FormatParse({
221
- rawRequest,
222
- adapterContext: inboundAdapterContext,
223
- formatAdapter,
224
- stageRecorder: inboundRecorder
225
- });
226
- const inboundStage2 = await runReqInboundStage2SemanticMap({
227
- adapterContext: inboundAdapterContext,
228
- formatEnvelope,
229
- semanticMapper,
230
- stageRecorder: inboundRecorder
231
- });
232
- this.captureAnthropicAliasMap(normalized, inboundAdapterContext, inboundStage2.chatEnvelope);
233
- const contextSnapshot = await hooks.captureContext({
234
- rawRequest,
235
- adapterContext: inboundAdapterContext,
236
- stageRecorder: inboundRecorder
237
- });
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
- }
248
- const inboundEnd = Date.now();
249
- const nodeResults = [];
250
- nodeResults.push({
251
- id: 'req_inbound',
252
- success: true,
253
- metadata: {
254
- node: 'req_inbound',
255
- executionTime: inboundEnd - inboundStart,
256
- startTime: inboundStart,
257
- endTime: inboundEnd,
258
- dataProcessed: {
259
- messages: standardizedRequest.messages.length,
260
- tools: standardizedRequest.tools?.length ?? 0
261
- }
262
- }
263
- });
264
- // 将 VirtualRouter 层的 servertool 相关配置注入到 metadata,保证响应侧
265
- // servertool(第三跳 reenter)也能访问到相同配置,即使当前 route 标记为 passthrough。
266
- const metaBase = {
267
- ...(normalized.metadata ?? {})
268
- };
269
- const webSearchConfig = this.config.virtualRouter?.webSearch;
270
- if (webSearchConfig) {
271
- metaBase.webSearch = webSearchConfig;
272
- }
273
- const execCommandGuard = this.config.virtualRouter?.execCommandGuard;
274
- if (execCommandGuard) {
275
- metaBase.execCommandGuard = execCommandGuard;
276
- }
277
- const clockConfig = this.config.virtualRouter?.clock;
278
- if (clockConfig) {
279
- metaBase.clock = clockConfig;
280
- }
281
- normalized.metadata = metaBase;
282
- let processedRequest;
283
- if (normalized.processMode !== 'passthrough') {
284
- const processResult = await runReqProcessStage1ToolGovernance({
285
- request: standardizedRequest,
286
- rawPayload: rawRequest,
287
- metadata: metaBase,
288
- entryEndpoint: normalized.entryEndpoint,
289
- requestId: normalized.id,
290
- stageRecorder: inboundRecorder
291
- });
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
- }
304
- if (processResult.nodeResult) {
305
- nodeResults.push(this.convertProcessNodeResult('req_process_stage1_tool_governance', processResult.nodeResult));
306
- }
307
- }
308
- let workingRequest = processedRequest ?? standardizedRequest;
309
- // 使用与 VirtualRouter 一致的 tiktoken 计数逻辑,对标准化请求进行一次
310
- // 上下文 token 估算,供后续 usage 归一化与统计使用。
311
- try {
312
- const estimatedTokens = computeRequestTokens(workingRequest, '');
313
- if (typeof estimatedTokens === 'number' && Number.isFinite(estimatedTokens) && estimatedTokens > 0) {
314
- normalized.metadata = normalized.metadata || {};
315
- normalized.metadata.estimatedInputTokens = estimatedTokens;
316
- }
317
- }
318
- catch {
319
- // 估算失败不应影响主流程
320
- }
321
- const normalizedMeta = normalized.metadata;
322
- const responsesResume = normalizedMeta && typeof normalizedMeta.responsesResume === 'object'
323
- ? normalizedMeta.responsesResume
324
- : undefined;
325
- const stdMetadata = workingRequest?.metadata;
326
- const hasImageAttachment = (stdMetadata?.hasImageAttachment === true || stdMetadata?.hasImageAttachment === 'true') ||
327
- (normalizedMeta?.hasImageAttachment === true || normalizedMeta?.hasImageAttachment === 'true');
328
- const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
329
- stdMetadata?.serverToolRequired === true;
330
- const sessionIdentifiers = extractSessionIdentifiersFromMetadata(normalized.metadata);
331
- // 将从 metadata / clientHeaders 中解析出的会话标识同步回 normalized.metadata,
332
- // 便于后续 AdapterContext(响应侧 servertool)也能访问到相同的 sessionId /
333
- // conversationId,用于 sticky-session 相关逻辑(例如 stopMessage)。
334
- if (sessionIdentifiers.sessionId && normalized.metadata && typeof normalized.metadata === 'object') {
335
- normalized.metadata.sessionId = sessionIdentifiers.sessionId;
336
- }
337
- if (sessionIdentifiers.conversationId && normalized.metadata && typeof normalized.metadata === 'object') {
338
- normalized.metadata.conversationId = sessionIdentifiers.conversationId;
339
- }
340
- const metadataInput = {
341
- requestId: normalized.id,
342
- entryEndpoint: normalized.entryEndpoint,
343
- processMode: normalized.processMode,
344
- stream: normalized.stream,
345
- direction: normalized.direction,
346
- providerProtocol: normalized.providerProtocol,
347
- routeHint: normalized.routeHint,
348
- stage: normalized.stage,
349
- responsesResume: responsesResume,
350
- ...(serverToolRequired ? { serverToolRequired: true } : {}),
351
- ...(sessionIdentifiers.sessionId ? { sessionId: sessionIdentifiers.sessionId } : {}),
352
- ...(sessionIdentifiers.conversationId ? { conversationId: sessionIdentifiers.conversationId } : {})
353
- };
354
- const routing = runReqProcessStage2RouteSelect({
355
- routerEngine: this.routerEngine,
356
- request: workingRequest,
357
- metadataInput,
358
- normalizedMetadata: normalized.metadata,
359
- stageRecorder: inboundRecorder
360
- });
361
- const stopMessageState = this.routerEngine.getStopMessageState(metadataInput);
362
- if (stopMessageState && normalized.metadata && typeof normalized.metadata === 'object') {
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
- ]);
370
- }
371
- // Emit virtual router hit log for debugging (orange [virtual-router] ...)
372
- try {
373
- const routeName = routing.decision?.routeName;
374
- const providerKey = routing.target?.providerKey;
375
- const modelId = workingRequest.model;
376
- const logger = (normalized.metadata && normalized.metadata.logger);
377
- if (logger && typeof logger.logVirtualRouterHit === 'function' && routeName && providerKey) {
378
- logger.logVirtualRouterHit(routeName, providerKey, typeof modelId === 'string' ? modelId : undefined);
379
- }
380
- }
381
- catch {
382
- // logging must not break routing
383
- }
384
- const outboundStream = this.resolveOutboundStreamIntent(routing.target?.streaming);
385
- workingRequest = this.applyOutboundStreamPreference(workingRequest, outboundStream);
386
- const outboundAdapterContext = this.buildAdapterContext(normalized, routing.target);
387
- if (routing.target?.compatibilityProfile) {
388
- outboundAdapterContext.compatibilityProfile = routing.target.compatibilityProfile;
389
- }
390
- const outboundProtocol = outboundAdapterContext.providerProtocol;
391
- const protocolSwitch = outboundProtocol !== normalized.providerProtocol;
392
- const outboundHooks = protocolSwitch ? this.resolveProtocolHooks(outboundProtocol) : hooks;
393
- if (!outboundHooks) {
394
- throw new Error(`[HubPipeline] Unsupported provider protocol for hub pipeline: ${outboundProtocol}`);
395
- }
396
- const outboundSemanticMapper = protocolSwitch ? outboundHooks.createSemanticMapper() : semanticMapper;
397
- const outboundFormatAdapter = protocolSwitch ? outboundHooks.createFormatAdapter() : formatAdapter;
398
- const outboundContextMetadataKey = protocolSwitch ? outboundHooks.contextMetadataKey : hooks.contextMetadataKey;
399
- const outboundContextSnapshot = protocolSwitch ? undefined : contextSnapshot;
400
- // Snapshots must be grouped by entry endpoint (client-facing protocol), not by provider protocol.
401
- // Otherwise one request would be split across multiple folders (e.g. openai-responses + anthropic-messages),
402
- // which breaks codex-samples correlation.
403
- const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint, {
404
- disableSnapshots: normalized.disableSnapshots === true
405
- });
406
- const outboundStart = Date.now();
407
- let providerPayload;
408
- const outboundStage1 = await runReqOutboundStage1SemanticMap({
409
- request: workingRequest,
410
- adapterContext: outboundAdapterContext,
411
- semanticMapper: outboundSemanticMapper,
412
- contextSnapshot: outboundContextSnapshot,
413
- contextMetadataKey: outboundContextMetadataKey,
414
- stageRecorder: outboundRecorder
415
- });
416
- let formattedPayload = await runReqOutboundStage2FormatBuild({
417
- formatEnvelope: outboundStage1.formatEnvelope,
418
- adapterContext: outboundAdapterContext,
419
- formatAdapter: outboundFormatAdapter,
420
- stageRecorder: outboundRecorder
421
- });
422
- formattedPayload = await runReqOutboundStage3Compat({
423
- payload: formattedPayload,
424
- adapterContext: outboundAdapterContext,
425
- stageRecorder: outboundRecorder
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
77
+ async execute(request) {
78
+ const normalized = await normalizeHubPipelineRequest(request);
79
+ if (normalized.direction === 'request' && normalized.hubEntryMode === 'chat_process') {
80
+ return await executeChatProcessEntryPipeline({
81
+ config: this.config,
82
+ routerEngine: this.routerEngine,
83
+ normalized,
84
+ resolveProtocolHooks
452
85
  });
453
- shadowBaselineProviderPayload = baselinePayload;
454
86
  }
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
- });
464
- providerPayload = applyHubProviderOutboundPolicy({
465
- policy: effectivePolicy,
466
- providerProtocol: outboundProtocol,
467
- payload: formattedPayload,
468
- stageRecorder: outboundRecorder,
469
- requestId: normalized.id
470
- });
471
- providerPayload = applyProviderOutboundToolSurface({
472
- config: this.config.toolSurface,
473
- providerProtocol: outboundProtocol,
474
- payload: providerPayload,
475
- stageRecorder: outboundRecorder,
476
- requestId: normalized.id
477
- });
478
- recordHubPolicyObservation({
479
- policy: effectivePolicy,
480
- providerProtocol: outboundProtocol,
481
- payload: providerPayload,
482
- stageRecorder: outboundRecorder,
483
- requestId: normalized.id
484
- });
485
- const outboundEnd = Date.now();
486
- nodeResults.push({
487
- id: 'req_outbound',
488
- success: true,
489
- metadata: {
490
- node: 'req_outbound',
491
- executionTime: outboundEnd - outboundStart,
492
- startTime: outboundStart,
493
- endTime: outboundEnd,
494
- dataProcessed: {
495
- messages: workingRequest.messages.length,
496
- tools: workingRequest.tools?.length ?? 0
497
- }
498
- }
499
- });
500
- // 为响应侧 servertool/web_search 提供一次性 Chat 请求快照,便于在 Hub 内部实现
501
- // 第三跳(将工具结果注入消息历史后重新调用主模型)。
502
- //
503
- // 注意:这里不再根据 processMode(passthrough/chat) 做分支判断——即使某些
504
- // route 将 processMode 标记为 passthrough,我们仍然需要保留一次规范化后的
505
- // Chat 请求快照,供 stopMessage / gemini_empty_reply_continue 等被动触发型
506
- // servertool 在响应阶段使用。
507
- //
508
- // 之前这里通过 JSON.stringify/parse 做深拷贝,但在部分 Responses/Gemini
509
- // 场景下,workingRequest 上携带的 metadata 可能包含无法安全序列化的字段,
510
- // 导致克隆过程抛错、capturedChatRequest 被静默丢弃,从而让响应侧的
511
- // stop_message_auto 等 ServerTool 无法获取上一跳的 Chat 请求。
512
- //
513
- // 对于 capturedChatRequest,我们只需要一个“可读快照”,不会在后续流程中
514
- // 对其做就地修改,因此可以直接使用浅拷贝结构,避免序列化失败导致整段
515
- // 逻辑失效。
516
- // Deep-clone a JSON-safe snapshot for servertool followups.
517
- // Only capture the canonical Chat payload fields (model/messages/tools/parameters) to keep it serializable.
518
- const capturedChatRequest = {
519
- model: workingRequest.model,
520
- messages: jsonClone(workingRequest.messages),
521
- tools: workingRequest.tools ? jsonClone(workingRequest.tools) : workingRequest.tools,
522
- parameters: workingRequest.parameters ? jsonClone(workingRequest.parameters) : workingRequest.parameters
523
- };
524
- const metadata = {
525
- ...normalized.metadata,
526
- ...(hasImageAttachment ? { hasImageAttachment: true } : {}),
527
- capturedChatRequest,
528
- entryEndpoint: normalized.entryEndpoint,
529
- providerProtocol: outboundProtocol,
530
- stream: normalized.stream,
531
- processMode: normalized.processMode,
532
- routeHint: normalized.routeHint,
533
- target: routing.target,
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
- : {})
545
- };
546
- return {
547
- requestId: normalized.id,
548
- providerPayload,
549
- standardizedRequest,
550
- processedRequest,
551
- routingDecision: routing.decision,
552
- routingDiagnostics: routing.diagnostics,
553
- target: routing.target,
554
- metadata,
555
- nodeResults
556
- };
557
- }
558
- resolveClientProtocol(entryEndpoint) {
559
- const lowered = String(entryEndpoint || '').toLowerCase();
560
- if (lowered.includes('/v1/responses'))
561
- return 'openai-responses';
562
- if (lowered.includes('/v1/messages'))
563
- return 'anthropic-messages';
564
- return 'openai-chat';
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);
87
+ const hooks = resolveProtocolHooks(normalized.providerProtocol);
607
88
  if (!hooks) {
608
89
  throw new Error(`Unsupported provider protocol for hub pipeline: ${normalized.providerProtocol}`);
609
90
  }
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({
91
+ return await executeRequestStagePipeline({
92
+ config: this.config,
724
93
  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
94
+ normalized,
95
+ hooks,
96
+ resolveProtocolHooks
778
97
  });
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
- }
865
- async execute(request) {
866
- const normalized = await this.normalizeRequest(request);
867
- if (normalized.direction === 'request' && normalized.hubEntryMode === 'chat_process') {
868
- return await this.executeChatProcessEntryPipeline(normalized);
869
- }
870
- const hooks = this.resolveProtocolHooks(normalized.providerProtocol);
871
- if (!hooks) {
872
- throw new Error(`Unsupported provider protocol for hub pipeline: ${normalized.providerProtocol}`);
873
- }
874
- return await this.executeRequestStagePipeline(normalized, hooks);
875
- }
876
- captureAnthropicAliasMap(normalized, adapterContext, chatEnvelope) {
877
- if (!this.shouldCaptureAnthropicAlias(normalized.entryEndpoint)) {
878
- return;
879
- }
880
- const aliasMap = this.resolveAliasMapFromSources(adapterContext, chatEnvelope);
881
- if (!aliasMap) {
882
- return;
883
- }
884
- normalized.metadata = normalized.metadata || {};
885
- if (!normalized.metadata.anthropicToolNameMap) {
886
- normalized.metadata.anthropicToolNameMap = aliasMap;
887
- }
888
- }
889
- shouldCaptureAnthropicAlias(endpoint) {
890
- return typeof endpoint === 'string' && endpoint.toLowerCase().includes('/v1/messages');
891
- }
892
- resolveAliasMapFromSources(adapterContext, chatEnvelope) {
893
- const fromContext = coerceAliasMap(adapterContext.anthropicToolNameMap);
894
- if (fromContext) {
895
- return fromContext;
896
- }
897
- const metadataNode = chatEnvelope.metadata;
898
- const direct = metadataNode ? coerceAliasMap(metadataNode.anthropicToolNameMap) : undefined;
899
- if (direct) {
900
- return direct;
901
- }
902
- const contextNode = metadataNode && metadataNode.context && typeof metadataNode.context === 'object'
903
- ? metadataNode.context
904
- : undefined;
905
- const fromContextNode = coerceAliasMap(contextNode?.anthropicToolNameMap);
906
- if (fromContextNode) {
907
- return fromContextNode;
908
- }
909
- return readAliasMapFromSemantics(chatEnvelope);
910
- }
911
- resolveProtocolHooks(protocol) {
912
- switch (protocol) {
913
- case 'openai-chat':
914
- return {
915
- createFormatAdapter: () => new ChatFormatAdapter(),
916
- createSemanticMapper: () => new ChatSemanticMapper(),
917
- captureContext: (options) => runChatContextCapture(options),
918
- contextMetadataKey: 'chatContext'
919
- };
920
- case 'openai-responses':
921
- return {
922
- createFormatAdapter: () => new ResponsesFormatAdapter(),
923
- createSemanticMapper: () => new ResponsesSemanticMapper(),
924
- captureContext: createResponsesContextCapture(captureResponsesContextSnapshot),
925
- contextMetadataKey: 'responsesContext'
926
- };
927
- case 'anthropic-messages':
928
- return {
929
- createFormatAdapter: () => new AnthropicFormatAdapter(),
930
- createSemanticMapper: () => new AnthropicSemanticMapper(),
931
- captureContext: (options) => runChatContextCapture(options),
932
- contextMetadataKey: 'anthropicContext'
933
- };
934
- case 'gemini-chat':
935
- return {
936
- createFormatAdapter: () => new GeminiFormatAdapter(),
937
- createSemanticMapper: () => new GeminiSemanticMapper(),
938
- captureContext: createNoopContextCapture('gemini-chat')
939
- };
940
- default:
941
- return undefined;
942
- }
943
- }
944
- buildAdapterContext(normalized, target) {
945
- const metadata = normalized.metadata || {};
946
- const providerProtocol = target?.outboundProfile || normalized.providerProtocol;
947
- const providerId = (target?.providerKey || metadata.providerKey);
948
- const routeId = metadata.routeName;
949
- const profileId = (target?.providerKey || metadata.pipelineId);
950
- const streamingHint = normalized.stream === true ? 'force' : normalized.stream === false ? 'disable' : 'auto';
951
- const toolCallIdStyle = normalizeToolCallIdStyleCandidate(metadata.toolCallIdStyle);
952
- const adapterContext = {
953
- requestId: normalized.id,
954
- entryEndpoint: normalized.entryEndpoint || '/v1/chat/completions',
955
- providerProtocol,
956
- providerId,
957
- routeId,
958
- profileId,
959
- streamingHint,
960
- toolCallIdStyle
961
- };
962
- const runtime = metadata.runtime;
963
- if (runtime && typeof runtime === 'object' && !Array.isArray(runtime)) {
964
- adapterContext.runtime = jsonClone(runtime);
965
- }
966
- const clientRequestId = typeof metadata.clientRequestId === 'string'
967
- ? metadata.clientRequestId.trim()
968
- : '';
969
- if (clientRequestId) {
970
- adapterContext.clientRequestId = clientRequestId;
971
- }
972
- const groupRequestId = typeof metadata.groupRequestId === 'string'
973
- ? metadata.groupRequestId.trim()
974
- : '';
975
- if (groupRequestId) {
976
- adapterContext.groupRequestId = groupRequestId;
977
- }
978
- if (typeof metadata.originalModelId === 'string') {
979
- adapterContext.originalModelId = metadata.originalModelId;
980
- }
981
- if (typeof metadata.clientModelId === 'string') {
982
- adapterContext.clientModelId = metadata.clientModelId;
983
- }
984
- if (typeof metadata.assignedModelId === 'string') {
985
- adapterContext.modelId = metadata.assignedModelId;
986
- }
987
- // 将 serverToolFollowup 等 ServerTool 相关标记从 normalized.metadata 透传到 AdapterContext,
988
- // 便于响应侧的 convertProviderResponse / 各 ServerTool handler 识别“二跳/内部跳转”,
989
- // 再由具体 handler 决定是否在 followup 响应中继续生效(例如 stopMessage 多轮续写)。
990
- if (Object.prototype.hasOwnProperty.call(metadata, 'serverToolFollowup')) {
991
- adapterContext.serverToolFollowup = metadata
992
- .serverToolFollowup;
993
- }
994
- // Preserve raw client tools (schemas) for response-side validation/formatting.
995
- if (Object.prototype.hasOwnProperty.call(metadata, 'clientToolsRaw')) {
996
- adapterContext.clientToolsRaw = metadata
997
- .clientToolsRaw;
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
- }
1005
- const sessionId = typeof metadata.sessionId === 'string'
1006
- ? metadata.sessionId.trim()
1007
- : '';
1008
- if (sessionId) {
1009
- adapterContext.sessionId = sessionId;
1010
- }
1011
- const conversationId = typeof metadata.conversationId === 'string'
1012
- ? metadata.conversationId.trim()
1013
- : '';
1014
- if (conversationId) {
1015
- adapterContext.conversationId = conversationId;
1016
- }
1017
- const stopMessageState = metadata.stopMessageState;
1018
- if (stopMessageState && typeof stopMessageState === 'object') {
1019
- adapterContext.stopMessageState = stopMessageState;
1020
- }
1021
- const compactionRequest = metadata.compactionRequest;
1022
- if (compactionRequest === true ||
1023
- (typeof compactionRequest === 'string' && compactionRequest.trim().toLowerCase() === 'true')) {
1024
- adapterContext.compactionRequest = true;
1025
- }
1026
- const clientConnectionState = metadata.clientConnectionState;
1027
- if (clientConnectionState && typeof clientConnectionState === 'object' && !Array.isArray(clientConnectionState)) {
1028
- const stateRecord = clientConnectionState;
1029
- adapterContext.clientConnectionState = clientConnectionState;
1030
- if (typeof stateRecord.disconnected === 'boolean') {
1031
- adapterContext.clientDisconnected = stateRecord.disconnected;
1032
- }
1033
- }
1034
- const clientDisconnectedRaw = metadata.clientDisconnected;
1035
- if (clientDisconnectedRaw === true ||
1036
- (typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
1037
- adapterContext.clientDisconnected = true;
1038
- }
1039
- const responsesResume = metadata.responsesResume &&
1040
- typeof metadata.responsesResume === 'object'
1041
- ? metadata.responsesResume
1042
- : undefined;
1043
- if (responsesResume) {
1044
- adapterContext.responsesResume = responsesResume;
1045
- }
1046
- const serverToolLoopState = metadata.serverToolLoopState;
1047
- if (serverToolLoopState && typeof serverToolLoopState === 'object' && !Array.isArray(serverToolLoopState)) {
1048
- adapterContext.serverToolLoopState = serverToolLoopState;
1049
- }
1050
- // 透传 gemini_empty_reply_continue 的重试计数,便于在多次空回复后终止自动续写。
1051
- const emptyReplyCount = metadata.geminiEmptyReplyCount;
1052
- if (typeof emptyReplyCount === 'number' && Number.isFinite(emptyReplyCount)) {
1053
- adapterContext.geminiEmptyReplyCount = emptyReplyCount;
1054
- }
1055
- if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
1056
- adapterContext.compatibilityProfile = target.compatibilityProfile;
1057
- }
1058
- return adapterContext;
1059
- }
1060
- maybeCreateStageRecorder(context, endpoint, options) {
1061
- if (options?.disableSnapshots === true) {
1062
- return undefined;
1063
- }
1064
- if (!shouldRecordSnapshots()) {
1065
- return undefined;
1066
- }
1067
- const effectiveEndpoint = endpoint || context.entryEndpoint || '/v1/chat/completions';
1068
- try {
1069
- return createSnapshotRecorder(context, effectiveEndpoint);
1070
- }
1071
- catch {
1072
- return undefined;
1073
- }
1074
- }
1075
- asJsonObject(value) {
1076
- if (!value || typeof value !== 'object') {
1077
- throw new Error('Responses pipeline requires JSON object payload');
1078
- }
1079
- return value;
1080
- }
1081
- async normalizeRequest(request) {
1082
- if (!request || typeof request !== 'object') {
1083
- throw new Error('HubPipeline requires request payload');
1084
- }
1085
- const id = request.id || `req_${Date.now()}`;
1086
- const endpoint = normalizeEndpoint(request.endpoint);
1087
- const metadataRecord = {
1088
- ...(request.metadata ?? {})
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
- }
1111
- const entryEndpoint = typeof metadataRecord.entryEndpoint === 'string'
1112
- ? normalizeEndpoint(metadataRecord.entryEndpoint)
1113
- : endpoint;
1114
- const providerProtocol = resolveProviderProtocol(metadataRecord.providerProtocol);
1115
- const processMode = metadataRecord.processMode === 'passthrough' ? 'passthrough' : 'chat';
1116
- const direction = metadataRecord.direction === 'response' ? 'response' : 'request';
1117
- const stage = metadataRecord.stage === 'outbound' ? 'outbound' : 'inbound';
1118
- const resolvedReadable = this.unwrapReadable(request.payload);
1119
- const stream = Boolean(metadataRecord.stream ||
1120
- resolvedReadable ||
1121
- (request.payload && typeof request.payload === 'object' && request.payload.stream));
1122
- const payload = await this.materializePayload(request.payload, {
1123
- requestId: id,
1124
- entryEndpoint,
1125
- providerProtocol,
1126
- metadata: metadataRecord
1127
- }, resolvedReadable);
1128
- const normalizedMetadata = {
1129
- ...metadataRecord,
1130
- entryEndpoint,
1131
- providerProtocol,
1132
- processMode,
1133
- direction,
1134
- stage,
1135
- stream
1136
- };
1137
- const routeHint = typeof metadataRecord.routeHint === 'string' ? metadataRecord.routeHint : undefined;
1138
- if (routeHint) {
1139
- normalizedMetadata.routeHint = routeHint;
1140
- }
1141
- return {
1142
- id,
1143
- endpoint,
1144
- entryEndpoint,
1145
- providerProtocol,
1146
- payload,
1147
- metadata: normalizedMetadata,
1148
- policyOverride: policyOverride ?? undefined,
1149
- shadowCompare: shadowCompare ?? undefined,
1150
- disableSnapshots,
1151
- processMode,
1152
- direction,
1153
- stage,
1154
- stream,
1155
- routeHint,
1156
- ...(hubEntryMode ? { hubEntryMode } : {})
1157
- };
1158
- }
1159
- convertProcessNodeResult(id, result) {
1160
- return {
1161
- id,
1162
- success: result.success,
1163
- metadata: result.metadata,
1164
- error: result.error
1165
- ? {
1166
- code: result.error.code ?? 'hub_chat_process_error',
1167
- message: result.error.message,
1168
- details: result.error.details
1169
- }
1170
- : undefined
1171
- };
1172
- }
1173
- async materializePayload(payload, context, resolvedStream) {
1174
- const stream = resolvedStream ?? this.unwrapReadable(payload);
1175
- if (stream) {
1176
- return await this.convertSsePayload(stream, context);
1177
- }
1178
- if (!payload || typeof payload !== 'object') {
1179
- throw new Error('HubPipeline requires JSON object payload');
1180
- }
1181
- return payload;
1182
- }
1183
- unwrapReadable(payload) {
1184
- if (!payload) {
1185
- return null;
1186
- }
1187
- if (payload instanceof Readable) {
1188
- return payload;
1189
- }
1190
- if (payload && typeof payload === 'object' && 'readable' in payload) {
1191
- const candidate = payload.readable;
1192
- if (candidate instanceof Readable) {
1193
- return candidate;
1194
- }
1195
- }
1196
- return null;
1197
- }
1198
- async convertSsePayload(stream, context) {
1199
- const protocol = this.resolveSseProtocol(context);
1200
- const codec = defaultSseCodecRegistry.get(protocol);
1201
- try {
1202
- const result = await codec.convertSseToJson(stream, {
1203
- requestId: context.requestId,
1204
- model: this.extractModelHint(context.metadata),
1205
- direction: 'request'
1206
- });
1207
- if (!result || typeof result !== 'object') {
1208
- throw new Error('SSE conversion returned empty payload');
1209
- }
1210
- return result;
1211
- }
1212
- catch (error) {
1213
- const message = error instanceof Error ? error.message : String(error ?? 'Unknown error');
1214
- throw new Error(`Failed to convert SSE payload for protocol ${protocol}: ${message}`);
1215
- }
1216
- }
1217
- resolveSseProtocol(context) {
1218
- const explicitProtocol = resolveSseProtocolFromMetadata(context.metadata);
1219
- if (explicitProtocol) {
1220
- return explicitProtocol;
1221
- }
1222
- return context.providerProtocol;
1223
- }
1224
- extractModelHint(metadata) {
1225
- if (typeof metadata.model === 'string' && metadata.model.trim()) {
1226
- return metadata.model;
1227
- }
1228
- const provider = metadata.provider;
1229
- const candidates = [
1230
- provider?.model,
1231
- provider?.modelId,
1232
- provider?.defaultModel
1233
- ];
1234
- for (const candidate of candidates) {
1235
- if (typeof candidate === 'string' && candidate.trim()) {
1236
- return candidate;
1237
- }
1238
- }
1239
- return undefined;
1240
- }
1241
- resolveOutboundStreamIntent(providerPreference) {
1242
- if (providerPreference === 'always') {
1243
- return true;
1244
- }
1245
- if (providerPreference === 'never') {
1246
- return false;
1247
- }
1248
- return undefined;
1249
- }
1250
- applyOutboundStreamPreference(request, stream) {
1251
- if (!request || typeof request !== 'object') {
1252
- return request;
1253
- }
1254
- const ops = [];
1255
- if (typeof stream === 'boolean') {
1256
- ops.push({ op: 'set_request_parameter_fields', fields: { stream } });
1257
- ops.push({ op: 'set_request_metadata_fields', fields: { outboundStream: stream } });
1258
- }
1259
- else {
1260
- ops.push({ op: 'unset_request_parameter_keys', keys: ['stream'] });
1261
- ops.push({ op: 'unset_request_metadata_keys', keys: ['outboundStream'] });
1262
- }
1263
- return applyHubOperations(request, ops);
1264
- }
1265
- }
1266
- function normalizeToolCallIdStyleCandidate(value) {
1267
- if (typeof value !== 'string') {
1268
- return undefined;
1269
- }
1270
- const normalized = value.trim().toLowerCase();
1271
- if (normalized === 'fc') {
1272
- return 'fc';
1273
- }
1274
- if (normalized === 'preserve') {
1275
- return 'preserve';
1276
- }
1277
- return undefined;
1278
- }
1279
- function normalizeEndpoint(endpoint) {
1280
- if (!endpoint)
1281
- return '/v1/chat/completions';
1282
- const trimmed = endpoint.trim();
1283
- if (!trimmed)
1284
- return '/v1/chat/completions';
1285
- const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
1286
- return normalized.replace(/\/{2,}/g, '/');
1287
- }
1288
- function resolveProviderProtocol(value) {
1289
- if (typeof value !== 'string' || !value.trim()) {
1290
- return 'openai-chat';
1291
- }
1292
- const normalized = value.trim().toLowerCase();
1293
- const mapped = PROVIDER_PROTOCOL_ALIASES[normalized];
1294
- if (mapped) {
1295
- return mapped;
1296
- }
1297
- throw new Error(`[HubPipeline] Unsupported providerProtocol "${value}". Configure a valid protocol (openai-chat|openai-responses|anthropic-messages|gemini-chat).`);
1298
- }
1299
- const PROVIDER_PROTOCOL_ALIASES = {
1300
- 'openai-chat': 'openai-chat',
1301
- openai: 'openai-chat',
1302
- chat: 'openai-chat',
1303
- 'responses': 'openai-responses',
1304
- 'openai-responses': 'openai-responses',
1305
- 'anthropic': 'anthropic-messages',
1306
- 'anthropic-messages': 'anthropic-messages',
1307
- 'messages': 'anthropic-messages',
1308
- 'gemini': 'gemini-chat',
1309
- 'google-gemini': 'gemini-chat',
1310
- 'gemini-chat': 'gemini-chat'
1311
- };
1312
- function resolveEndpointForProviderProtocol(protocol) {
1313
- if (!protocol) {
1314
- return undefined;
1315
- }
1316
- const value = protocol.toLowerCase();
1317
- if (value === 'openai-responses')
1318
- return '/v1/responses';
1319
- if (value === 'openai-chat')
1320
- return '/v1/chat/completions';
1321
- if (value === 'anthropic-messages' || value === 'anthropic')
1322
- return '/v1/messages';
1323
- if (value === 'gemini-chat' || value === 'gemini' || value === 'google-gemini')
1324
- return '/v1/responses';
1325
- return undefined;
1326
- }
1327
- function resolveSseProtocolFromMetadata(metadata) {
1328
- const candidate = metadata.sseProtocol ?? metadata.clientSseProtocol ?? metadata.routeSseProtocol;
1329
- if (typeof candidate !== 'string' || !candidate.trim()) {
1330
- return undefined;
1331
- }
1332
- return resolveProviderProtocol(candidate);
1333
- }
1334
- function coerceAliasMap(candidate) {
1335
- if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
1336
- return undefined;
1337
- }
1338
- const normalized = {};
1339
- for (const [key, value] of Object.entries(candidate)) {
1340
- if (typeof key !== 'string' || typeof value !== 'string') {
1341
- continue;
1342
- }
1343
- const trimmedKey = key.trim();
1344
- const trimmedValue = value.trim();
1345
- if (!trimmedKey.length || !trimmedValue.length) {
1346
- continue;
1347
- }
1348
- normalized[trimmedKey] = trimmedValue;
1349
- }
1350
- return Object.keys(normalized).length ? normalized : undefined;
1351
- }
1352
- function readAliasMapFromSemantics(chatEnvelope) {
1353
- if (!chatEnvelope?.semantics || typeof chatEnvelope.semantics !== 'object') {
1354
- return undefined;
1355
- }
1356
- const node = chatEnvelope.semantics.anthropic;
1357
- if (!node || !isJsonObject(node)) {
1358
- return undefined;
1359
98
  }
1360
- return coerceAliasMap(node.toolAliasMap);
1361
99
  }