@jsonstudio/llms 0.6.1397 → 0.6.1402
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.
- package/dist/conversion/codecs/gemini-openai-codec.d.ts +1 -3
- package/dist/conversion/codecs/gemini-openai-codec.js +4 -10
- package/dist/conversion/compat/actions/gemini-cli-request.d.ts +2 -0
- package/dist/conversion/compat/actions/gemini-cli-request.js +490 -0
- package/dist/conversion/compat/profiles/chat-gemini-cli.json +27 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +76 -348
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +95 -3
- package/dist/conversion/hub/pipeline/hub-pipeline.js +1365 -19
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +22 -0
- package/dist/conversion/hub/policy/policy-engine.js +50 -3
- package/dist/conversion/hub/process/chat-process.js +5 -146
- package/dist/conversion/hub/response/provider-response.js +11 -10
- package/dist/conversion/hub/response/response-mappers.d.ts +1 -3
- package/dist/conversion/hub/response/response-mappers.js +2 -20
- package/dist/conversion/hub/tool-surface/tool-surface-engine.js +2 -1
- package/dist/conversion/responses/responses-openai-bridge.js +4 -3
- package/dist/conversion/shared/gemini-tool-utils.d.ts +1 -6
- package/dist/conversion/shared/gemini-tool-utils.js +164 -542
- package/dist/conversion/shared/mcp-injection.js +29 -0
- package/dist/conversion/shared/openai-message-normalize.js +3 -17
- package/dist/filters/special/request-tool-list-filter.js +21 -13
- package/dist/filters/special/tool-filter-hooks.js +60 -3
- package/dist/router/virtual-router/bootstrap.js +8 -6
- package/dist/router/virtual-router/engine-health.d.ts +1 -1
- package/dist/router/virtual-router/engine-health.js +110 -11
- package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +0 -15
- package/dist/router/virtual-router/engine-selection/alias-selection.js +4 -85
- package/dist/router/virtual-router/engine-selection/route-utils.js +6 -12
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +17 -40
- package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -5
- package/dist/router/virtual-router/engine.js +6 -21
- package/dist/router/virtual-router/types.d.ts +1 -14
- package/dist/servertool/clock/config.d.ts +1 -1
- package/dist/servertool/clock/config.js +5 -9
- package/dist/servertool/engine.js +6 -83
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -2
- package/dist/sse/sse-to-json/builders/response-builder.js +0 -16
- package/package.json +1 -1
- package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +0 -10
- package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +0 -142
- package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +0 -6
- package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +0 -79
- package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +0 -3
- package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +0 -46
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +0 -8
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +0 -366
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +0 -9
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +0 -390
- package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +0 -3
- package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +0 -14
- package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +0 -2
- package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +0 -144
- package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +0 -4
- package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +0 -32
- package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +0 -8
- package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +0 -63
- package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +0 -2
- package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +0 -43
- package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +0 -1
- package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +0 -29
- package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +0 -2
- package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +0 -16
- package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +0 -116
- package/dist/conversion/hub/pipeline/hub-pipeline/types.js +0 -1
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.d.ts +0 -10
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.js +0 -172
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.d.ts +0 -10
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.js +0 -71
- package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.d.ts +0 -14
- package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.js +0 -289
|
@@ -1,10 +1,112 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { isJsonObject, jsonClone } from '../types/json.js';
|
|
1
3
|
import { VirtualRouterEngine } from '../../../router/virtual-router/engine.js';
|
|
2
4
|
import { providerErrorCenter } from '../../../router/virtual-router/error-center.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
+
import { cloneRuntimeMetadata, ensureRuntimeMetadata, readRuntimeMetadata } from '../../shared/runtime-metadata.js';
|
|
32
|
+
function isTruthyEnv(value) {
|
|
33
|
+
const v = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
34
|
+
return v === '1' || v === 'true' || v === 'yes' || v === 'on';
|
|
35
|
+
}
|
|
36
|
+
function resolveApplyPatchToolModeFromEnv() {
|
|
37
|
+
const rawMode = String(process.env.RCC_APPLY_PATCH_TOOL_MODE || process.env.ROUTECODEX_APPLY_PATCH_TOOL_MODE || '')
|
|
38
|
+
.trim()
|
|
39
|
+
.toLowerCase();
|
|
40
|
+
if (rawMode === 'freeform')
|
|
41
|
+
return 'freeform';
|
|
42
|
+
if (rawMode === 'schema' || rawMode === 'json_schema')
|
|
43
|
+
return 'schema';
|
|
44
|
+
const freeformFlag = process.env.RCC_APPLY_PATCH_FREEFORM || process.env.ROUTECODEX_APPLY_PATCH_FREEFORM;
|
|
45
|
+
if (isTruthyEnv(freeformFlag))
|
|
46
|
+
return 'freeform';
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function resolveApplyPatchToolModeFromTools(toolsRaw) {
|
|
50
|
+
if (!Array.isArray(toolsRaw) || toolsRaw.length === 0) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
for (const entry of toolsRaw) {
|
|
54
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
55
|
+
continue;
|
|
56
|
+
const record = entry;
|
|
57
|
+
const type = typeof record.type === 'string' ? record.type.trim().toLowerCase() : '';
|
|
58
|
+
if (type && type !== 'function')
|
|
59
|
+
continue;
|
|
60
|
+
const fn = record.function && typeof record.function === 'object' && !Array.isArray(record.function)
|
|
61
|
+
? record.function
|
|
62
|
+
: undefined;
|
|
63
|
+
const name = typeof fn?.name === 'string' ? fn.name.trim().toLowerCase() : '';
|
|
64
|
+
if (name !== 'apply_patch')
|
|
65
|
+
continue;
|
|
66
|
+
const format = typeof record.format === 'string'
|
|
67
|
+
? record.format.trim().toLowerCase()
|
|
68
|
+
: typeof fn?.format === 'string'
|
|
69
|
+
? String(fn.format).trim().toLowerCase()
|
|
70
|
+
: '';
|
|
71
|
+
if (format === 'freeform')
|
|
72
|
+
return 'freeform';
|
|
73
|
+
// If apply_patch is present without explicit freeform marker, default to schema mode.
|
|
74
|
+
return 'schema';
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
function extractHubPolicyOverride(metadata) {
|
|
79
|
+
const raw = metadata ? metadata.__hubPolicyOverride : undefined;
|
|
80
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const obj = raw;
|
|
84
|
+
const mode = typeof obj.mode === 'string' ? obj.mode.trim().toLowerCase() : '';
|
|
85
|
+
const sampleRate = typeof obj.sampleRate === 'number' && Number.isFinite(obj.sampleRate) ? obj.sampleRate : undefined;
|
|
86
|
+
if (mode !== 'off' && mode !== 'observe' && mode !== 'enforce') {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
mode: mode,
|
|
91
|
+
...(sampleRate !== undefined ? { sampleRate } : {})
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function extractHubShadowCompareConfig(metadata) {
|
|
95
|
+
const raw = metadata ? metadata.__hubShadowCompare : undefined;
|
|
96
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const obj = raw;
|
|
100
|
+
const modeCandidate = typeof obj.baselineMode === 'string'
|
|
101
|
+
? obj.baselineMode.trim().toLowerCase()
|
|
102
|
+
: typeof obj.mode === 'string'
|
|
103
|
+
? obj.mode.trim().toLowerCase()
|
|
104
|
+
: '';
|
|
105
|
+
if (modeCandidate !== 'off' && modeCandidate !== 'observe' && modeCandidate !== 'enforce') {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return { baselineMode: modeCandidate };
|
|
109
|
+
}
|
|
8
110
|
export class HubPipeline {
|
|
9
111
|
routerEngine;
|
|
10
112
|
config;
|
|
@@ -74,26 +176,1270 @@ export class HubPipeline {
|
|
|
74
176
|
this.unsubscribeProviderErrors = undefined;
|
|
75
177
|
}
|
|
76
178
|
}
|
|
77
|
-
async
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
179
|
+
async executeRequestStagePipeline(normalized, hooks) {
|
|
180
|
+
const formatAdapter = hooks.createFormatAdapter();
|
|
181
|
+
const semanticMapper = hooks.createSemanticMapper();
|
|
182
|
+
const rawRequest = this.asJsonObject(normalized.payload);
|
|
183
|
+
// Detect applyPatchToolMode (runtime/tooling hint). Client tool schemas are captured as chat semantics
|
|
184
|
+
// in req_inbound_stage2_semantic_map; they must not be stored in metadata.
|
|
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
|
+
const rt = ensureRuntimeMetadata(normalized.metadata);
|
|
191
|
+
rt.applyPatchToolMode = applyPatchToolMode;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// best-effort: do not block request handling due to tool scan failures
|
|
196
|
+
}
|
|
197
|
+
if (isCompactionRequest(rawRequest)) {
|
|
198
|
+
normalized.metadata = normalized.metadata || {};
|
|
199
|
+
const rt = ensureRuntimeMetadata(normalized.metadata);
|
|
200
|
+
rt.compactionRequest = true;
|
|
201
|
+
}
|
|
202
|
+
const effectivePolicy = normalized.policyOverride ?? this.config.policy;
|
|
203
|
+
const shadowCompareBaselineMode = normalized.shadowCompare?.baselineMode;
|
|
204
|
+
const inboundAdapterContext = this.buildAdapterContext(normalized);
|
|
205
|
+
const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint, {
|
|
206
|
+
disableSnapshots: normalized.disableSnapshots === true
|
|
207
|
+
});
|
|
208
|
+
const inboundStart = Date.now();
|
|
209
|
+
// Phase 0: observe client inbound payload violations (best-effort; no rewrites).
|
|
210
|
+
recordHubPolicyObservation({
|
|
211
|
+
policy: effectivePolicy,
|
|
212
|
+
providerProtocol: this.resolveClientProtocol(normalized.entryEndpoint),
|
|
213
|
+
payload: rawRequest,
|
|
214
|
+
phase: 'client_inbound',
|
|
215
|
+
stageRecorder: inboundRecorder,
|
|
216
|
+
requestId: normalized.id
|
|
217
|
+
});
|
|
218
|
+
const formatEnvelope = await runReqInboundStage1FormatParse({
|
|
219
|
+
rawRequest,
|
|
220
|
+
adapterContext: inboundAdapterContext,
|
|
221
|
+
formatAdapter,
|
|
222
|
+
stageRecorder: inboundRecorder
|
|
223
|
+
});
|
|
224
|
+
const responsesResumeFromMetadata = normalized.metadata && typeof normalized.metadata.responsesResume === 'object'
|
|
225
|
+
? normalized.metadata.responsesResume
|
|
226
|
+
: undefined;
|
|
227
|
+
const inboundStage2 = await runReqInboundStage2SemanticMap({
|
|
228
|
+
adapterContext: inboundAdapterContext,
|
|
229
|
+
formatEnvelope,
|
|
230
|
+
semanticMapper,
|
|
231
|
+
...(responsesResumeFromMetadata ? { responsesResume: responsesResumeFromMetadata } : {}),
|
|
232
|
+
stageRecorder: inboundRecorder
|
|
233
|
+
});
|
|
234
|
+
// responsesResume must not enter chat_process as metadata; it is lifted into chat.semantics in stage2.
|
|
235
|
+
if (responsesResumeFromMetadata &&
|
|
236
|
+
normalized.metadata &&
|
|
237
|
+
Object.prototype.hasOwnProperty.call(normalized.metadata, 'responsesResume')) {
|
|
238
|
+
delete normalized.metadata.responsesResume;
|
|
239
|
+
}
|
|
240
|
+
const contextSnapshot = await hooks.captureContext({
|
|
241
|
+
rawRequest,
|
|
242
|
+
adapterContext: inboundAdapterContext,
|
|
243
|
+
stageRecorder: inboundRecorder
|
|
244
|
+
});
|
|
245
|
+
const standardizedRequest = inboundStage2.standardizedRequest;
|
|
246
|
+
try {
|
|
247
|
+
const rt = readRuntimeMetadata(normalized.metadata);
|
|
248
|
+
const mode = String(rt?.applyPatchToolMode || '').trim().toLowerCase();
|
|
249
|
+
if (mode === 'freeform' || mode === 'schema') {
|
|
250
|
+
standardizedRequest.metadata.applyPatchToolMode = mode;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// best-effort: do not block request handling due to metadata propagation failures
|
|
255
|
+
}
|
|
256
|
+
const inboundEnd = Date.now();
|
|
257
|
+
const nodeResults = [];
|
|
258
|
+
nodeResults.push({
|
|
259
|
+
id: 'req_inbound',
|
|
260
|
+
success: true,
|
|
261
|
+
metadata: {
|
|
262
|
+
node: 'req_inbound',
|
|
263
|
+
executionTime: inboundEnd - inboundStart,
|
|
264
|
+
startTime: inboundStart,
|
|
265
|
+
endTime: inboundEnd,
|
|
266
|
+
dataProcessed: {
|
|
267
|
+
messages: standardizedRequest.messages.length,
|
|
268
|
+
tools: standardizedRequest.tools?.length ?? 0
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
// 将 VirtualRouter 层的 servertool 相关配置注入到 metadata,保证响应侧
|
|
273
|
+
// servertool(第三跳 reenter)也能访问到相同配置,即使当前 route 标记为 passthrough。
|
|
274
|
+
const metaBase = {
|
|
275
|
+
...(normalized.metadata ?? {})
|
|
276
|
+
};
|
|
277
|
+
const rtBase = ensureRuntimeMetadata(metaBase);
|
|
278
|
+
const webSearchConfig = this.config.virtualRouter?.webSearch;
|
|
279
|
+
if (webSearchConfig) {
|
|
280
|
+
rtBase.webSearch = webSearchConfig;
|
|
281
|
+
}
|
|
282
|
+
const execCommandGuard = this.config.virtualRouter?.execCommandGuard;
|
|
283
|
+
if (execCommandGuard) {
|
|
284
|
+
rtBase.execCommandGuard = execCommandGuard;
|
|
285
|
+
}
|
|
286
|
+
const clockConfig = this.config.virtualRouter?.clock;
|
|
287
|
+
if (clockConfig) {
|
|
288
|
+
rtBase.clock = clockConfig;
|
|
289
|
+
}
|
|
290
|
+
normalized.metadata = metaBase;
|
|
291
|
+
let processedRequest;
|
|
292
|
+
if (normalized.processMode !== 'passthrough') {
|
|
293
|
+
assertNoMappableSemanticsInMetadata(metaBase, 'chat_process.request.entry');
|
|
294
|
+
const processResult = await runReqProcessStage1ToolGovernance({
|
|
295
|
+
request: standardizedRequest,
|
|
296
|
+
rawPayload: rawRequest,
|
|
297
|
+
metadata: metaBase,
|
|
298
|
+
entryEndpoint: normalized.entryEndpoint,
|
|
299
|
+
requestId: normalized.id,
|
|
300
|
+
stageRecorder: inboundRecorder
|
|
85
301
|
});
|
|
302
|
+
processedRequest = processResult.processedRequest;
|
|
303
|
+
// Surface request-side clock reservation into pipeline metadata so response conversion
|
|
304
|
+
// can commit delivery only after a successful response is produced.
|
|
305
|
+
try {
|
|
306
|
+
const reservation = processedRequest?.metadata?.__clockReservation;
|
|
307
|
+
if (reservation && typeof reservation === 'object') {
|
|
308
|
+
metaBase.__clockReservation = reservation;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// best-effort: do not block request handling due to metadata propagation failures
|
|
313
|
+
}
|
|
314
|
+
if (processResult.nodeResult) {
|
|
315
|
+
nodeResults.push(this.convertProcessNodeResult('chat_process.req.stage4.tool_governance', processResult.nodeResult));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
let workingRequest = processedRequest ?? standardizedRequest;
|
|
319
|
+
// 使用与 VirtualRouter 一致的 tiktoken 计数逻辑,对标准化请求进行一次
|
|
320
|
+
// 上下文 token 估算,供后续 usage 归一化与统计使用。
|
|
321
|
+
try {
|
|
322
|
+
const estimatedTokens = computeRequestTokens(workingRequest, '');
|
|
323
|
+
if (typeof estimatedTokens === 'number' && Number.isFinite(estimatedTokens) && estimatedTokens > 0) {
|
|
324
|
+
normalized.metadata = normalized.metadata || {};
|
|
325
|
+
normalized.metadata.estimatedInputTokens = estimatedTokens;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// 估算失败不应影响主流程
|
|
330
|
+
}
|
|
331
|
+
const normalizedMeta = normalized.metadata;
|
|
332
|
+
// responsesResume is a client-protocol semantic (/v1/responses tool loop) and must live in chat.semantics.
|
|
333
|
+
// Do not read it from metadata once entering chat_process.
|
|
334
|
+
const responsesResume = (() => {
|
|
335
|
+
try {
|
|
336
|
+
const semantics = workingRequest?.semantics;
|
|
337
|
+
const node = semantics && typeof semantics === 'object' && !Array.isArray(semantics) ? semantics.responses : undefined;
|
|
338
|
+
const resume = node && typeof node === 'object' && !Array.isArray(node) ? node.resume : undefined;
|
|
339
|
+
return resume && typeof resume === 'object' && !Array.isArray(resume) ? resume : undefined;
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
})();
|
|
345
|
+
const stdMetadata = workingRequest?.metadata;
|
|
346
|
+
const hasImageAttachment = (stdMetadata?.hasImageAttachment === true || stdMetadata?.hasImageAttachment === 'true') ||
|
|
347
|
+
(normalizedMeta?.hasImageAttachment === true || normalizedMeta?.hasImageAttachment === 'true');
|
|
348
|
+
const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
|
|
349
|
+
stdMetadata?.serverToolRequired === true;
|
|
350
|
+
const sessionIdentifiers = extractSessionIdentifiersFromMetadata(normalized.metadata);
|
|
351
|
+
// 将从 metadata / clientHeaders 中解析出的会话标识同步回 normalized.metadata,
|
|
352
|
+
// 便于后续 AdapterContext(响应侧 servertool)也能访问到相同的 sessionId /
|
|
353
|
+
// conversationId,用于 sticky-session 相关逻辑(例如 stopMessage)。
|
|
354
|
+
if (sessionIdentifiers.sessionId && normalized.metadata && typeof normalized.metadata === 'object') {
|
|
355
|
+
normalized.metadata.sessionId = sessionIdentifiers.sessionId;
|
|
356
|
+
}
|
|
357
|
+
if (sessionIdentifiers.conversationId && normalized.metadata && typeof normalized.metadata === 'object') {
|
|
358
|
+
normalized.metadata.conversationId = sessionIdentifiers.conversationId;
|
|
359
|
+
}
|
|
360
|
+
const disableStickyRoutes = readRuntimeMetadata(normalized.metadata)?.disableStickyRoutes === true;
|
|
361
|
+
const metadataInput = {
|
|
362
|
+
requestId: normalized.id,
|
|
363
|
+
entryEndpoint: normalized.entryEndpoint,
|
|
364
|
+
processMode: normalized.processMode,
|
|
365
|
+
stream: normalized.stream,
|
|
366
|
+
direction: normalized.direction,
|
|
367
|
+
providerProtocol: normalized.providerProtocol,
|
|
368
|
+
routeHint: normalized.routeHint,
|
|
369
|
+
stage: normalized.stage,
|
|
370
|
+
responsesResume: responsesResume,
|
|
371
|
+
...(disableStickyRoutes ? { disableStickyRoutes: true } : {}),
|
|
372
|
+
...(serverToolRequired ? { serverToolRequired: true } : {}),
|
|
373
|
+
...(sessionIdentifiers.sessionId ? { sessionId: sessionIdentifiers.sessionId } : {}),
|
|
374
|
+
...(sessionIdentifiers.conversationId ? { conversationId: sessionIdentifiers.conversationId } : {})
|
|
375
|
+
};
|
|
376
|
+
const routing = runReqProcessStage2RouteSelect({
|
|
377
|
+
routerEngine: this.routerEngine,
|
|
378
|
+
request: workingRequest,
|
|
379
|
+
metadataInput,
|
|
380
|
+
normalizedMetadata: normalized.metadata,
|
|
381
|
+
stageRecorder: inboundRecorder
|
|
382
|
+
});
|
|
383
|
+
const stopMessageState = this.routerEngine.getStopMessageState(metadataInput);
|
|
384
|
+
if (stopMessageState && normalized.metadata && typeof normalized.metadata === 'object') {
|
|
385
|
+
const rt = ensureRuntimeMetadata(normalized.metadata);
|
|
386
|
+
rt.stopMessageState = stopMessageState;
|
|
86
387
|
}
|
|
87
|
-
|
|
388
|
+
// Emit virtual router hit log for debugging (orange [virtual-router] ...)
|
|
389
|
+
try {
|
|
390
|
+
const routeName = routing.decision?.routeName;
|
|
391
|
+
const providerKey = routing.target?.providerKey;
|
|
392
|
+
const modelId = workingRequest.model;
|
|
393
|
+
const logger = (normalized.metadata && normalized.metadata.logger);
|
|
394
|
+
if (logger && typeof logger.logVirtualRouterHit === 'function' && routeName && providerKey) {
|
|
395
|
+
logger.logVirtualRouterHit(routeName, providerKey, typeof modelId === 'string' ? modelId : undefined);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// logging must not break routing
|
|
400
|
+
}
|
|
401
|
+
const outboundStream = this.resolveOutboundStreamIntent(routing.target?.streaming);
|
|
402
|
+
workingRequest = this.applyOutboundStreamPreference(workingRequest, outboundStream);
|
|
403
|
+
const outboundAdapterContext = this.buildAdapterContext(normalized, routing.target);
|
|
404
|
+
if (routing.target?.compatibilityProfile) {
|
|
405
|
+
outboundAdapterContext.compatibilityProfile = routing.target.compatibilityProfile;
|
|
406
|
+
}
|
|
407
|
+
const outboundProtocol = outboundAdapterContext.providerProtocol;
|
|
408
|
+
const protocolSwitch = outboundProtocol !== normalized.providerProtocol;
|
|
409
|
+
const outboundHooks = protocolSwitch ? this.resolveProtocolHooks(outboundProtocol) : hooks;
|
|
410
|
+
if (!outboundHooks) {
|
|
411
|
+
throw new Error(`[HubPipeline] Unsupported provider protocol for hub pipeline: ${outboundProtocol}`);
|
|
412
|
+
}
|
|
413
|
+
const outboundSemanticMapper = protocolSwitch ? outboundHooks.createSemanticMapper() : semanticMapper;
|
|
414
|
+
const outboundFormatAdapter = protocolSwitch ? outboundHooks.createFormatAdapter() : formatAdapter;
|
|
415
|
+
const outboundContextMetadataKey = protocolSwitch ? outboundHooks.contextMetadataKey : hooks.contextMetadataKey;
|
|
416
|
+
const outboundContextSnapshot = protocolSwitch ? undefined : contextSnapshot;
|
|
417
|
+
// Snapshots must be grouped by entry endpoint (client-facing protocol), not by provider protocol.
|
|
418
|
+
// Otherwise one request would be split across multiple folders (e.g. openai-responses + anthropic-messages),
|
|
419
|
+
// which breaks codex-samples correlation.
|
|
420
|
+
const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint, {
|
|
421
|
+
disableSnapshots: normalized.disableSnapshots === true
|
|
422
|
+
});
|
|
423
|
+
const outboundStart = Date.now();
|
|
424
|
+
let providerPayload;
|
|
425
|
+
const outboundStage1 = await runReqOutboundStage1SemanticMap({
|
|
426
|
+
request: workingRequest,
|
|
427
|
+
adapterContext: outboundAdapterContext,
|
|
428
|
+
semanticMapper: outboundSemanticMapper,
|
|
429
|
+
contextSnapshot: outboundContextSnapshot,
|
|
430
|
+
contextMetadataKey: outboundContextMetadataKey,
|
|
431
|
+
stageRecorder: outboundRecorder
|
|
432
|
+
});
|
|
433
|
+
let formattedPayload = await runReqOutboundStage2FormatBuild({
|
|
434
|
+
formatEnvelope: outboundStage1.formatEnvelope,
|
|
435
|
+
adapterContext: outboundAdapterContext,
|
|
436
|
+
formatAdapter: outboundFormatAdapter,
|
|
437
|
+
stageRecorder: outboundRecorder
|
|
438
|
+
});
|
|
439
|
+
formattedPayload = await runReqOutboundStage3Compat({
|
|
440
|
+
payload: formattedPayload,
|
|
441
|
+
adapterContext: outboundAdapterContext,
|
|
442
|
+
stageRecorder: outboundRecorder
|
|
443
|
+
});
|
|
444
|
+
let shadowBaselineProviderPayload;
|
|
445
|
+
if (shadowCompareBaselineMode) {
|
|
446
|
+
const baselinePolicy = {
|
|
447
|
+
...(effectivePolicy ?? {}),
|
|
448
|
+
mode: shadowCompareBaselineMode
|
|
449
|
+
};
|
|
450
|
+
// Compute a baseline provider payload in the *same execution*, without recording
|
|
451
|
+
// snapshots/diffs and without re-running the full pipeline. This avoids side effects
|
|
452
|
+
// (conversation store, followup captures, etc.) that a second execute() would trigger.
|
|
453
|
+
const baselineFormatted = typeof globalThis.structuredClone === 'function'
|
|
454
|
+
? globalThis.structuredClone(formattedPayload)
|
|
455
|
+
: jsonClone(formattedPayload);
|
|
456
|
+
let baselinePayload = applyHubProviderOutboundPolicy({
|
|
457
|
+
policy: baselinePolicy,
|
|
458
|
+
providerProtocol: outboundProtocol,
|
|
459
|
+
payload: baselineFormatted,
|
|
460
|
+
stageRecorder: undefined,
|
|
461
|
+
requestId: normalized.id
|
|
462
|
+
});
|
|
463
|
+
baselinePayload = applyProviderOutboundToolSurface({
|
|
464
|
+
config: this.config.toolSurface,
|
|
465
|
+
providerProtocol: outboundProtocol,
|
|
466
|
+
payload: baselinePayload,
|
|
467
|
+
stageRecorder: undefined,
|
|
468
|
+
requestId: normalized.id
|
|
469
|
+
});
|
|
470
|
+
shadowBaselineProviderPayload = baselinePayload;
|
|
471
|
+
}
|
|
472
|
+
// Phase 0/1: observe provider outbound payload violations before any enforcement rewrites.
|
|
473
|
+
// This provides black-box visibility into what the pipeline would have sent upstream.
|
|
474
|
+
recordHubPolicyObservation({
|
|
475
|
+
policy: effectivePolicy,
|
|
476
|
+
providerProtocol: outboundProtocol,
|
|
477
|
+
payload: formattedPayload,
|
|
478
|
+
stageRecorder: outboundRecorder,
|
|
479
|
+
requestId: normalized.id
|
|
480
|
+
});
|
|
481
|
+
providerPayload = applyHubProviderOutboundPolicy({
|
|
482
|
+
policy: effectivePolicy,
|
|
483
|
+
providerProtocol: outboundProtocol,
|
|
484
|
+
payload: formattedPayload,
|
|
485
|
+
stageRecorder: outboundRecorder,
|
|
486
|
+
requestId: normalized.id
|
|
487
|
+
});
|
|
488
|
+
providerPayload = applyProviderOutboundToolSurface({
|
|
489
|
+
config: this.config.toolSurface,
|
|
490
|
+
providerProtocol: outboundProtocol,
|
|
491
|
+
payload: providerPayload,
|
|
492
|
+
stageRecorder: outboundRecorder,
|
|
493
|
+
requestId: normalized.id
|
|
494
|
+
});
|
|
495
|
+
recordHubPolicyObservation({
|
|
496
|
+
policy: effectivePolicy,
|
|
497
|
+
providerProtocol: outboundProtocol,
|
|
498
|
+
payload: providerPayload,
|
|
499
|
+
stageRecorder: outboundRecorder,
|
|
500
|
+
requestId: normalized.id
|
|
501
|
+
});
|
|
502
|
+
const outboundEnd = Date.now();
|
|
503
|
+
nodeResults.push({
|
|
504
|
+
id: 'req_outbound',
|
|
505
|
+
success: true,
|
|
506
|
+
metadata: {
|
|
507
|
+
node: 'req_outbound',
|
|
508
|
+
executionTime: outboundEnd - outboundStart,
|
|
509
|
+
startTime: outboundStart,
|
|
510
|
+
endTime: outboundEnd,
|
|
511
|
+
dataProcessed: {
|
|
512
|
+
messages: workingRequest.messages.length,
|
|
513
|
+
tools: workingRequest.tools?.length ?? 0
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
// 为响应侧 servertool/web_search 提供一次性 Chat 请求快照,便于在 Hub 内部实现
|
|
518
|
+
// 第三跳(将工具结果注入消息历史后重新调用主模型)。
|
|
519
|
+
//
|
|
520
|
+
// 注意:这里不再根据 processMode(passthrough/chat) 做分支判断——即使某些
|
|
521
|
+
// route 将 processMode 标记为 passthrough,我们仍然需要保留一次规范化后的
|
|
522
|
+
// Chat 请求快照,供 stopMessage / gemini_empty_reply_continue 等被动触发型
|
|
523
|
+
// servertool 在响应阶段使用。
|
|
524
|
+
//
|
|
525
|
+
// 之前这里通过 JSON.stringify/parse 做深拷贝,但在部分 Responses/Gemini
|
|
526
|
+
// 场景下,workingRequest 上携带的 metadata 可能包含无法安全序列化的字段,
|
|
527
|
+
// 导致克隆过程抛错、capturedChatRequest 被静默丢弃,从而让响应侧的
|
|
528
|
+
// stop_message_auto 等 ServerTool 无法获取上一跳的 Chat 请求。
|
|
529
|
+
//
|
|
530
|
+
// 对于 capturedChatRequest,我们只需要一个“可读快照”,不会在后续流程中
|
|
531
|
+
// 对其做就地修改,因此可以直接使用浅拷贝结构,避免序列化失败导致整段
|
|
532
|
+
// 逻辑失效。
|
|
533
|
+
// Deep-clone a JSON-safe snapshot for servertool followups.
|
|
534
|
+
// Only capture the canonical Chat payload fields (model/messages/tools/parameters) to keep it serializable.
|
|
535
|
+
const capturedChatRequest = {
|
|
536
|
+
model: workingRequest.model,
|
|
537
|
+
messages: jsonClone(workingRequest.messages),
|
|
538
|
+
tools: workingRequest.tools ? jsonClone(workingRequest.tools) : workingRequest.tools,
|
|
539
|
+
parameters: workingRequest.parameters ? jsonClone(workingRequest.parameters) : workingRequest.parameters
|
|
540
|
+
};
|
|
541
|
+
const metadata = {
|
|
542
|
+
...normalized.metadata,
|
|
543
|
+
...(hasImageAttachment ? { hasImageAttachment: true } : {}),
|
|
544
|
+
capturedChatRequest,
|
|
545
|
+
entryEndpoint: normalized.entryEndpoint,
|
|
546
|
+
providerProtocol: outboundProtocol,
|
|
547
|
+
stream: normalized.stream,
|
|
548
|
+
processMode: normalized.processMode,
|
|
549
|
+
routeHint: normalized.routeHint,
|
|
550
|
+
target: routing.target,
|
|
551
|
+
...(typeof outboundStream === 'boolean' ? { providerStream: outboundStream } : {}),
|
|
552
|
+
...(shadowBaselineProviderPayload
|
|
553
|
+
? {
|
|
554
|
+
hubShadowCompare: {
|
|
555
|
+
baselineMode: shadowCompareBaselineMode,
|
|
556
|
+
candidateMode: (effectivePolicy?.mode ?? 'off'),
|
|
557
|
+
providerProtocol: outboundProtocol,
|
|
558
|
+
baselineProviderPayload: shadowBaselineProviderPayload
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
: {})
|
|
562
|
+
};
|
|
563
|
+
return {
|
|
564
|
+
requestId: normalized.id,
|
|
565
|
+
providerPayload,
|
|
566
|
+
standardizedRequest,
|
|
567
|
+
processedRequest,
|
|
568
|
+
routingDecision: routing.decision,
|
|
569
|
+
routingDiagnostics: routing.diagnostics,
|
|
570
|
+
target: routing.target,
|
|
571
|
+
metadata,
|
|
572
|
+
nodeResults
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
resolveClientProtocol(entryEndpoint) {
|
|
576
|
+
const lowered = String(entryEndpoint || '').toLowerCase();
|
|
577
|
+
if (lowered.includes('/v1/responses'))
|
|
578
|
+
return 'openai-responses';
|
|
579
|
+
if (lowered.includes('/v1/messages'))
|
|
580
|
+
return 'anthropic-messages';
|
|
581
|
+
return 'openai-chat';
|
|
582
|
+
}
|
|
583
|
+
coerceStandardizedRequestFromPayload(payload, normalized) {
|
|
584
|
+
const model = typeof payload.model === 'string' && payload.model.trim().length ? payload.model.trim() : '';
|
|
585
|
+
if (!model) {
|
|
586
|
+
throw new Error('[HubPipeline] outbound stage requires payload.model');
|
|
587
|
+
}
|
|
588
|
+
const messages = Array.isArray(payload.messages) ? payload.messages : null;
|
|
589
|
+
if (!messages) {
|
|
590
|
+
throw new Error('[HubPipeline] outbound stage requires payload.messages[]');
|
|
591
|
+
}
|
|
592
|
+
const tools = Array.isArray(payload.tools) ? payload.tools : undefined;
|
|
593
|
+
const parameters = payload.parameters && typeof payload.parameters === 'object' && !Array.isArray(payload.parameters)
|
|
594
|
+
? payload.parameters
|
|
595
|
+
: {};
|
|
596
|
+
const semanticsFromPayload = payload.semantics && typeof payload.semantics === 'object' && !Array.isArray(payload.semantics)
|
|
597
|
+
? jsonClone(payload.semantics)
|
|
598
|
+
: undefined;
|
|
599
|
+
const metadataFromPayload = payload.metadata && typeof payload.metadata === 'object' && !Array.isArray(payload.metadata)
|
|
600
|
+
? payload.metadata
|
|
601
|
+
: undefined;
|
|
602
|
+
const standardizedRequest = {
|
|
603
|
+
model,
|
|
604
|
+
messages,
|
|
605
|
+
...(tools ? { tools } : {}),
|
|
606
|
+
parameters,
|
|
607
|
+
metadata: {
|
|
608
|
+
originalEndpoint: normalized.entryEndpoint,
|
|
609
|
+
...(metadataFromPayload ? metadataFromPayload : {}),
|
|
610
|
+
requestId: normalized.id,
|
|
611
|
+
stream: normalized.stream,
|
|
612
|
+
processMode: normalized.processMode,
|
|
613
|
+
...(normalized.routeHint ? { routeHint: normalized.routeHint } : {})
|
|
614
|
+
},
|
|
615
|
+
...(semanticsFromPayload ? { semantics: semanticsFromPayload } : {})
|
|
616
|
+
};
|
|
617
|
+
// Ensure followup/chat_process entry can still preserve mappable semantics
|
|
618
|
+
// without injecting them into metadata.
|
|
619
|
+
try {
|
|
620
|
+
const semantics = standardizedRequest.semantics && typeof standardizedRequest.semantics === 'object'
|
|
621
|
+
? standardizedRequest.semantics
|
|
622
|
+
: (standardizedRequest.semantics = {});
|
|
623
|
+
if (!semantics.tools || typeof semantics.tools !== 'object' || Array.isArray(semantics.tools)) {
|
|
624
|
+
semantics.tools = {};
|
|
625
|
+
}
|
|
626
|
+
const toolsNode = semantics.tools;
|
|
627
|
+
if (Array.isArray(payload.tools) && payload.tools.length && toolsNode.clientToolsRaw === undefined) {
|
|
628
|
+
toolsNode.clientToolsRaw = jsonClone(payload.tools);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
// best-effort
|
|
633
|
+
}
|
|
634
|
+
// Keep rawPayload minimal and JSON-safe; chat-process only needs the OpenAI-chat-like surface here.
|
|
635
|
+
const rawPayload = {
|
|
636
|
+
model,
|
|
637
|
+
messages,
|
|
638
|
+
...(tools ? { tools } : {}),
|
|
639
|
+
...(parameters && Object.keys(parameters).length ? { parameters } : {})
|
|
640
|
+
};
|
|
641
|
+
return { standardizedRequest, rawPayload };
|
|
642
|
+
}
|
|
643
|
+
async executeChatProcessEntryPipeline(normalized) {
|
|
644
|
+
const hooks = this.resolveProtocolHooks(normalized.providerProtocol);
|
|
88
645
|
if (!hooks) {
|
|
89
646
|
throw new Error(`Unsupported provider protocol for hub pipeline: ${normalized.providerProtocol}`);
|
|
90
647
|
}
|
|
91
|
-
|
|
92
|
-
|
|
648
|
+
const nodeResults = [];
|
|
649
|
+
nodeResults.push({
|
|
650
|
+
id: 'req_inbound',
|
|
651
|
+
success: true,
|
|
652
|
+
metadata: {
|
|
653
|
+
node: 'req_inbound',
|
|
654
|
+
skipped: true,
|
|
655
|
+
reason: 'stage=outbound',
|
|
656
|
+
dataProcessed: {}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
const rawPayloadInput = this.asJsonObject(normalized.payload);
|
|
660
|
+
const { standardizedRequest: standardizedRequestBase, rawPayload } = this.coerceStandardizedRequestFromPayload(rawPayloadInput, normalized);
|
|
661
|
+
// Keep metadata injection consistent with the inbound path: servertool/web_search config must be available
|
|
662
|
+
// to chat-process/tool governance even when request enters at outbound stage.
|
|
663
|
+
const metaBase = {
|
|
664
|
+
...(normalized.metadata ?? {})
|
|
665
|
+
};
|
|
666
|
+
const rtBase = ensureRuntimeMetadata(metaBase);
|
|
667
|
+
const webSearchConfig = this.config.virtualRouter?.webSearch;
|
|
668
|
+
if (webSearchConfig) {
|
|
669
|
+
rtBase.webSearch = webSearchConfig;
|
|
670
|
+
}
|
|
671
|
+
const execCommandGuard = this.config.virtualRouter?.execCommandGuard;
|
|
672
|
+
if (execCommandGuard) {
|
|
673
|
+
rtBase.execCommandGuard = execCommandGuard;
|
|
674
|
+
}
|
|
675
|
+
const clockConfig = this.config.virtualRouter?.clock;
|
|
676
|
+
if (clockConfig) {
|
|
677
|
+
rtBase.clock = clockConfig;
|
|
678
|
+
}
|
|
679
|
+
normalized.metadata = metaBase;
|
|
680
|
+
const standardizedRequest = standardizedRequestBase;
|
|
681
|
+
// Semantic Gate (chat_process entry): lift any mappable protocol semantics from metadata into request.semantics.
|
|
682
|
+
// This is the last chance before entering chat_process; after this point we fail-fast on banned metadata keys.
|
|
683
|
+
try {
|
|
684
|
+
const resumeMeta = metaBase && typeof metaBase.responsesResume === 'object' && metaBase.responsesResume
|
|
685
|
+
? metaBase.responsesResume
|
|
686
|
+
: undefined;
|
|
687
|
+
if (resumeMeta) {
|
|
688
|
+
standardizedRequest.semantics = standardizedRequest.semantics ?? {};
|
|
689
|
+
const semantics = standardizedRequest.semantics;
|
|
690
|
+
if (!semantics.responses || typeof semantics.responses !== 'object' || Array.isArray(semantics.responses)) {
|
|
691
|
+
semantics.responses = {};
|
|
692
|
+
}
|
|
693
|
+
const responsesNode = semantics.responses;
|
|
694
|
+
if (responsesNode.resume === undefined) {
|
|
695
|
+
responsesNode.resume = jsonClone(resumeMeta);
|
|
696
|
+
}
|
|
697
|
+
delete metaBase.responsesResume;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
// best-effort; validation happens below
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
const rt = readRuntimeMetadata(metaBase);
|
|
705
|
+
const mode = String(rt?.applyPatchToolMode || '').trim().toLowerCase();
|
|
706
|
+
if (mode === 'freeform' || mode === 'schema') {
|
|
707
|
+
standardizedRequest.metadata.applyPatchToolMode = mode;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
// ignore
|
|
712
|
+
}
|
|
713
|
+
const adapterContext = this.buildAdapterContext(normalized);
|
|
714
|
+
const stageRecorder = this.maybeCreateStageRecorder(adapterContext, normalized.entryEndpoint, {
|
|
715
|
+
disableSnapshots: normalized.disableSnapshots === true
|
|
716
|
+
});
|
|
717
|
+
let processedRequest;
|
|
718
|
+
if (normalized.processMode !== 'passthrough') {
|
|
719
|
+
assertNoMappableSemanticsInMetadata(metaBase, 'chat_process.request.entry');
|
|
720
|
+
const processResult = await runReqProcessStage1ToolGovernance({
|
|
721
|
+
request: standardizedRequest,
|
|
722
|
+
rawPayload,
|
|
723
|
+
metadata: metaBase,
|
|
724
|
+
entryEndpoint: normalized.entryEndpoint,
|
|
725
|
+
requestId: normalized.id,
|
|
726
|
+
stageRecorder
|
|
727
|
+
});
|
|
728
|
+
processedRequest = processResult.processedRequest;
|
|
729
|
+
// Surface request-side clock reservation into pipeline metadata so response conversion
|
|
730
|
+
// can commit delivery only after a successful response is produced.
|
|
731
|
+
try {
|
|
732
|
+
const reservation = processedRequest?.metadata?.__clockReservation;
|
|
733
|
+
if (reservation && typeof reservation === 'object') {
|
|
734
|
+
metaBase.__clockReservation = reservation;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// best-effort
|
|
739
|
+
}
|
|
740
|
+
if (processResult.nodeResult) {
|
|
741
|
+
nodeResults.push(this.convertProcessNodeResult('chat_process.req.stage4.tool_governance', processResult.nodeResult));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
let workingRequest = processedRequest ?? standardizedRequest;
|
|
745
|
+
// Token estimate for stats/diagnostics (best-effort).
|
|
746
|
+
try {
|
|
747
|
+
const estimatedTokens = computeRequestTokens(workingRequest, '');
|
|
748
|
+
if (typeof estimatedTokens === 'number' && Number.isFinite(estimatedTokens) && estimatedTokens > 0) {
|
|
749
|
+
normalized.metadata = normalized.metadata || {};
|
|
750
|
+
normalized.metadata.estimatedInputTokens = estimatedTokens;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
// ignore
|
|
755
|
+
}
|
|
756
|
+
const normalizedMeta = normalized.metadata;
|
|
757
|
+
// responsesResume is a client-protocol semantic (/v1/responses tool loop) and must live in chat.semantics.
|
|
758
|
+
// Do not read it from metadata once entering chat_process.
|
|
759
|
+
const responsesResume = (() => {
|
|
760
|
+
try {
|
|
761
|
+
const semantics = workingRequest?.semantics;
|
|
762
|
+
const node = semantics && typeof semantics === 'object' && !Array.isArray(semantics) ? semantics.responses : undefined;
|
|
763
|
+
const resume = node && typeof node === 'object' && !Array.isArray(node) ? node.resume : undefined;
|
|
764
|
+
return resume && typeof resume === 'object' && !Array.isArray(resume) ? resume : undefined;
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
})();
|
|
770
|
+
const stdMetadata = workingRequest?.metadata;
|
|
771
|
+
const hasImageAttachment = (stdMetadata?.hasImageAttachment === true || stdMetadata?.hasImageAttachment === 'true') ||
|
|
772
|
+
(normalizedMeta?.hasImageAttachment === true || normalizedMeta?.hasImageAttachment === 'true');
|
|
773
|
+
const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
|
|
774
|
+
stdMetadata?.serverToolRequired === true;
|
|
775
|
+
const sessionIdentifiers = extractSessionIdentifiersFromMetadata(normalized.metadata);
|
|
776
|
+
if (sessionIdentifiers.sessionId && normalized.metadata && typeof normalized.metadata === 'object') {
|
|
777
|
+
normalized.metadata.sessionId = sessionIdentifiers.sessionId;
|
|
778
|
+
}
|
|
779
|
+
if (sessionIdentifiers.conversationId && normalized.metadata && typeof normalized.metadata === 'object') {
|
|
780
|
+
normalized.metadata.conversationId = sessionIdentifiers.conversationId;
|
|
781
|
+
}
|
|
782
|
+
const disableStickyRoutes = readRuntimeMetadata(normalized.metadata)?.disableStickyRoutes === true;
|
|
783
|
+
const metadataInput = {
|
|
784
|
+
requestId: normalized.id,
|
|
785
|
+
entryEndpoint: normalized.entryEndpoint,
|
|
786
|
+
processMode: normalized.processMode,
|
|
787
|
+
stream: normalized.stream,
|
|
788
|
+
direction: normalized.direction,
|
|
789
|
+
providerProtocol: normalized.providerProtocol,
|
|
790
|
+
routeHint: normalized.routeHint,
|
|
791
|
+
stage: normalized.stage,
|
|
792
|
+
responsesResume: responsesResume,
|
|
793
|
+
...(disableStickyRoutes ? { disableStickyRoutes: true } : {}),
|
|
794
|
+
...(serverToolRequired ? { serverToolRequired: true } : {}),
|
|
795
|
+
...(sessionIdentifiers.sessionId ? { sessionId: sessionIdentifiers.sessionId } : {}),
|
|
796
|
+
...(sessionIdentifiers.conversationId ? { conversationId: sessionIdentifiers.conversationId } : {})
|
|
797
|
+
};
|
|
798
|
+
const routing = runReqProcessStage2RouteSelect({
|
|
93
799
|
routerEngine: this.routerEngine,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
800
|
+
request: workingRequest,
|
|
801
|
+
metadataInput,
|
|
802
|
+
normalizedMetadata: normalized.metadata,
|
|
803
|
+
stageRecorder
|
|
97
804
|
});
|
|
805
|
+
const stopMessageState = this.routerEngine.getStopMessageState(metadataInput);
|
|
806
|
+
if (stopMessageState && normalized.metadata && typeof normalized.metadata === 'object') {
|
|
807
|
+
const rt = ensureRuntimeMetadata(normalized.metadata);
|
|
808
|
+
rt.stopMessageState = stopMessageState;
|
|
809
|
+
}
|
|
810
|
+
// Emit virtual router hit log for debugging (same as inbound path).
|
|
811
|
+
try {
|
|
812
|
+
const routeName = routing.decision?.routeName;
|
|
813
|
+
const providerKey = routing.target?.providerKey;
|
|
814
|
+
const modelId = workingRequest.model;
|
|
815
|
+
const logger = (normalized.metadata && normalized.metadata.logger);
|
|
816
|
+
if (logger && typeof logger.logVirtualRouterHit === 'function' && routeName && providerKey) {
|
|
817
|
+
logger.logVirtualRouterHit(routeName, providerKey, typeof modelId === 'string' ? modelId : undefined);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
catch {
|
|
821
|
+
// ignore
|
|
822
|
+
}
|
|
823
|
+
const outboundStream = this.resolveOutboundStreamIntent(routing.target?.streaming);
|
|
824
|
+
workingRequest = this.applyOutboundStreamPreference(workingRequest, outboundStream);
|
|
825
|
+
const outboundAdapterContext = this.buildAdapterContext(normalized, routing.target);
|
|
826
|
+
if (routing.target?.compatibilityProfile) {
|
|
827
|
+
outboundAdapterContext.compatibilityProfile = routing.target.compatibilityProfile;
|
|
828
|
+
}
|
|
829
|
+
const outboundProtocol = outboundAdapterContext.providerProtocol;
|
|
830
|
+
const protocolSwitch = outboundProtocol !== normalized.providerProtocol;
|
|
831
|
+
const outboundHooks = protocolSwitch ? this.resolveProtocolHooks(outboundProtocol) : hooks;
|
|
832
|
+
if (!outboundHooks) {
|
|
833
|
+
throw new Error(`[HubPipeline] Unsupported provider protocol for hub pipeline: ${outboundProtocol}`);
|
|
834
|
+
}
|
|
835
|
+
const outboundSemanticMapper = protocolSwitch ? outboundHooks.createSemanticMapper() : hooks.createSemanticMapper();
|
|
836
|
+
const outboundFormatAdapter = protocolSwitch ? outboundHooks.createFormatAdapter() : hooks.createFormatAdapter();
|
|
837
|
+
const outboundContextMetadataKey = protocolSwitch ? outboundHooks.contextMetadataKey : hooks.contextMetadataKey;
|
|
838
|
+
const outboundContextSnapshot = undefined;
|
|
839
|
+
const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint, {
|
|
840
|
+
disableSnapshots: normalized.disableSnapshots === true
|
|
841
|
+
});
|
|
842
|
+
const outboundStart = Date.now();
|
|
843
|
+
let providerPayload;
|
|
844
|
+
const outboundStage1 = await runReqOutboundStage1SemanticMap({
|
|
845
|
+
request: workingRequest,
|
|
846
|
+
adapterContext: outboundAdapterContext,
|
|
847
|
+
semanticMapper: outboundSemanticMapper,
|
|
848
|
+
contextSnapshot: outboundContextSnapshot,
|
|
849
|
+
contextMetadataKey: outboundContextMetadataKey,
|
|
850
|
+
stageRecorder: outboundRecorder
|
|
851
|
+
});
|
|
852
|
+
let formattedPayload = await runReqOutboundStage2FormatBuild({
|
|
853
|
+
formatEnvelope: outboundStage1.formatEnvelope,
|
|
854
|
+
adapterContext: outboundAdapterContext,
|
|
855
|
+
formatAdapter: outboundFormatAdapter,
|
|
856
|
+
stageRecorder: outboundRecorder
|
|
857
|
+
});
|
|
858
|
+
formattedPayload = await runReqOutboundStage3Compat({
|
|
859
|
+
payload: formattedPayload,
|
|
860
|
+
adapterContext: outboundAdapterContext,
|
|
861
|
+
stageRecorder: outboundRecorder
|
|
862
|
+
});
|
|
863
|
+
// Phase 0/1: observe + enforce provider outbound policy and tool surface (same as inbound path).
|
|
864
|
+
const effectivePolicy = normalized.policyOverride ?? this.config.policy;
|
|
865
|
+
recordHubPolicyObservation({
|
|
866
|
+
policy: effectivePolicy,
|
|
867
|
+
providerProtocol: outboundProtocol,
|
|
868
|
+
payload: formattedPayload,
|
|
869
|
+
stageRecorder: outboundRecorder,
|
|
870
|
+
requestId: normalized.id
|
|
871
|
+
});
|
|
872
|
+
providerPayload = applyHubProviderOutboundPolicy({
|
|
873
|
+
policy: effectivePolicy,
|
|
874
|
+
providerProtocol: outboundProtocol,
|
|
875
|
+
payload: formattedPayload,
|
|
876
|
+
stageRecorder: outboundRecorder,
|
|
877
|
+
requestId: normalized.id
|
|
878
|
+
});
|
|
879
|
+
providerPayload = applyProviderOutboundToolSurface({
|
|
880
|
+
config: this.config.toolSurface,
|
|
881
|
+
providerProtocol: outboundProtocol,
|
|
882
|
+
payload: providerPayload,
|
|
883
|
+
stageRecorder: outboundRecorder,
|
|
884
|
+
requestId: normalized.id
|
|
885
|
+
});
|
|
886
|
+
recordHubPolicyObservation({
|
|
887
|
+
policy: effectivePolicy,
|
|
888
|
+
providerProtocol: outboundProtocol,
|
|
889
|
+
payload: providerPayload,
|
|
890
|
+
stageRecorder: outboundRecorder,
|
|
891
|
+
requestId: normalized.id
|
|
892
|
+
});
|
|
893
|
+
const outboundEnd = Date.now();
|
|
894
|
+
nodeResults.push({
|
|
895
|
+
id: 'req_outbound',
|
|
896
|
+
success: true,
|
|
897
|
+
metadata: {
|
|
898
|
+
node: 'req_outbound',
|
|
899
|
+
executionTime: outboundEnd - outboundStart,
|
|
900
|
+
startTime: outboundStart,
|
|
901
|
+
endTime: outboundEnd,
|
|
902
|
+
dataProcessed: {
|
|
903
|
+
messages: workingRequest.messages.length,
|
|
904
|
+
tools: workingRequest.tools?.length ?? 0
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
const capturedChatRequest = {
|
|
909
|
+
model: workingRequest.model,
|
|
910
|
+
messages: jsonClone(workingRequest.messages),
|
|
911
|
+
tools: workingRequest.tools ? jsonClone(workingRequest.tools) : workingRequest.tools,
|
|
912
|
+
parameters: workingRequest.parameters ? jsonClone(workingRequest.parameters) : workingRequest.parameters
|
|
913
|
+
};
|
|
914
|
+
const metadata = {
|
|
915
|
+
...normalized.metadata,
|
|
916
|
+
...(hasImageAttachment ? { hasImageAttachment: true } : {}),
|
|
917
|
+
capturedChatRequest,
|
|
918
|
+
entryEndpoint: normalized.entryEndpoint,
|
|
919
|
+
providerProtocol: outboundProtocol,
|
|
920
|
+
stream: normalized.stream,
|
|
921
|
+
processMode: normalized.processMode,
|
|
922
|
+
routeHint: normalized.routeHint,
|
|
923
|
+
target: routing.target,
|
|
924
|
+
...(typeof outboundStream === 'boolean' ? { providerStream: outboundStream } : {})
|
|
925
|
+
};
|
|
926
|
+
return {
|
|
927
|
+
requestId: normalized.id,
|
|
928
|
+
providerPayload,
|
|
929
|
+
standardizedRequest,
|
|
930
|
+
processedRequest,
|
|
931
|
+
routingDecision: routing.decision,
|
|
932
|
+
routingDiagnostics: routing.diagnostics,
|
|
933
|
+
target: routing.target,
|
|
934
|
+
metadata,
|
|
935
|
+
nodeResults
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
async execute(request) {
|
|
939
|
+
const normalized = await this.normalizeRequest(request);
|
|
940
|
+
if (normalized.direction === 'request' && normalized.hubEntryMode === 'chat_process') {
|
|
941
|
+
return await this.executeChatProcessEntryPipeline(normalized);
|
|
942
|
+
}
|
|
943
|
+
const hooks = this.resolveProtocolHooks(normalized.providerProtocol);
|
|
944
|
+
if (!hooks) {
|
|
945
|
+
throw new Error(`Unsupported provider protocol for hub pipeline: ${normalized.providerProtocol}`);
|
|
946
|
+
}
|
|
947
|
+
return await this.executeRequestStagePipeline(normalized, hooks);
|
|
948
|
+
}
|
|
949
|
+
captureAnthropicAliasMap(normalized, adapterContext, chatEnvelope) {
|
|
950
|
+
if (!this.shouldCaptureAnthropicAlias(normalized.entryEndpoint)) {
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const aliasMap = this.resolveAliasMapFromSources(adapterContext, chatEnvelope);
|
|
954
|
+
if (!aliasMap) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
// A1: tool name alias map is mappable semantics and must live in chat.semantics (never metadata).
|
|
958
|
+
try {
|
|
959
|
+
if (!chatEnvelope.semantics || typeof chatEnvelope.semantics !== 'object' || Array.isArray(chatEnvelope.semantics)) {
|
|
960
|
+
chatEnvelope.semantics = {};
|
|
961
|
+
}
|
|
962
|
+
const semantics = chatEnvelope.semantics;
|
|
963
|
+
if (!semantics.tools || !isJsonObject(semantics.tools)) {
|
|
964
|
+
semantics.tools = {};
|
|
965
|
+
}
|
|
966
|
+
const toolsNode = semantics.tools;
|
|
967
|
+
if (!isJsonObject(toolsNode.toolNameAliasMap) && !isJsonObject(toolsNode.toolAliasMap)) {
|
|
968
|
+
toolsNode.toolNameAliasMap = jsonClone(aliasMap);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
catch {
|
|
972
|
+
// best-effort: never block request handling due to alias map propagation failures
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
shouldCaptureAnthropicAlias(endpoint) {
|
|
976
|
+
return typeof endpoint === 'string' && endpoint.toLowerCase().includes('/v1/messages');
|
|
977
|
+
}
|
|
978
|
+
resolveAliasMapFromSources(adapterContext, chatEnvelope) {
|
|
979
|
+
const fromContext = coerceAliasMap(adapterContext.anthropicToolNameMap);
|
|
980
|
+
if (fromContext) {
|
|
981
|
+
return fromContext;
|
|
982
|
+
}
|
|
983
|
+
const metadataNode = chatEnvelope.metadata;
|
|
984
|
+
const direct = metadataNode ? coerceAliasMap(metadataNode.anthropicToolNameMap) : undefined;
|
|
985
|
+
if (direct) {
|
|
986
|
+
return direct;
|
|
987
|
+
}
|
|
988
|
+
const contextNode = metadataNode && metadataNode.context && typeof metadataNode.context === 'object'
|
|
989
|
+
? metadataNode.context
|
|
990
|
+
: undefined;
|
|
991
|
+
const fromContextNode = coerceAliasMap(contextNode?.anthropicToolNameMap);
|
|
992
|
+
if (fromContextNode) {
|
|
993
|
+
return fromContextNode;
|
|
994
|
+
}
|
|
995
|
+
return readAliasMapFromSemantics(chatEnvelope);
|
|
996
|
+
}
|
|
997
|
+
resolveProtocolHooks(protocol) {
|
|
998
|
+
switch (protocol) {
|
|
999
|
+
case 'openai-chat':
|
|
1000
|
+
return {
|
|
1001
|
+
createFormatAdapter: () => new ChatFormatAdapter(),
|
|
1002
|
+
createSemanticMapper: () => new ChatSemanticMapper(),
|
|
1003
|
+
captureContext: (options) => runChatContextCapture(options),
|
|
1004
|
+
contextMetadataKey: 'chatContext'
|
|
1005
|
+
};
|
|
1006
|
+
case 'openai-responses':
|
|
1007
|
+
return {
|
|
1008
|
+
createFormatAdapter: () => new ResponsesFormatAdapter(),
|
|
1009
|
+
createSemanticMapper: () => new ResponsesSemanticMapper(),
|
|
1010
|
+
captureContext: createResponsesContextCapture(captureResponsesContextSnapshot),
|
|
1011
|
+
contextMetadataKey: 'responsesContext'
|
|
1012
|
+
};
|
|
1013
|
+
case 'anthropic-messages':
|
|
1014
|
+
return {
|
|
1015
|
+
createFormatAdapter: () => new AnthropicFormatAdapter(),
|
|
1016
|
+
createSemanticMapper: () => new AnthropicSemanticMapper(),
|
|
1017
|
+
captureContext: (options) => runChatContextCapture(options),
|
|
1018
|
+
contextMetadataKey: 'anthropicContext'
|
|
1019
|
+
};
|
|
1020
|
+
case 'gemini-chat':
|
|
1021
|
+
return {
|
|
1022
|
+
createFormatAdapter: () => new GeminiFormatAdapter(),
|
|
1023
|
+
createSemanticMapper: () => new GeminiSemanticMapper(),
|
|
1024
|
+
captureContext: createNoopContextCapture('gemini-chat')
|
|
1025
|
+
};
|
|
1026
|
+
default:
|
|
1027
|
+
return undefined;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
buildAdapterContext(normalized, target) {
|
|
1031
|
+
const metadata = normalized.metadata || {};
|
|
1032
|
+
const providerProtocol = target?.outboundProfile || normalized.providerProtocol;
|
|
1033
|
+
const providerId = (target?.providerKey || metadata.providerKey);
|
|
1034
|
+
const routeId = metadata.routeName;
|
|
1035
|
+
const profileId = (target?.providerKey || metadata.pipelineId);
|
|
1036
|
+
const compatibilityProfile = typeof target?.compatibilityProfile === 'string' && target.compatibilityProfile.trim()
|
|
1037
|
+
? target.compatibilityProfile.trim()
|
|
1038
|
+
: typeof metadata.compatibilityProfile === 'string'
|
|
1039
|
+
? String(metadata.compatibilityProfile).trim()
|
|
1040
|
+
: undefined;
|
|
1041
|
+
const streamingHint = normalized.stream === true ? 'force' : normalized.stream === false ? 'disable' : 'auto';
|
|
1042
|
+
const toolCallIdStyle = normalizeToolCallIdStyleCandidate(metadata.toolCallIdStyle);
|
|
1043
|
+
const adapterContext = {
|
|
1044
|
+
requestId: normalized.id,
|
|
1045
|
+
entryEndpoint: normalized.entryEndpoint || '/v1/chat/completions',
|
|
1046
|
+
providerProtocol,
|
|
1047
|
+
providerId,
|
|
1048
|
+
routeId,
|
|
1049
|
+
profileId,
|
|
1050
|
+
streamingHint,
|
|
1051
|
+
toolCallIdStyle,
|
|
1052
|
+
...(compatibilityProfile ? { compatibilityProfile } : {})
|
|
1053
|
+
};
|
|
1054
|
+
const runtime = metadata.runtime;
|
|
1055
|
+
if (runtime && typeof runtime === 'object' && !Array.isArray(runtime)) {
|
|
1056
|
+
adapterContext.runtime = jsonClone(runtime);
|
|
1057
|
+
}
|
|
1058
|
+
const clientRequestId = typeof metadata.clientRequestId === 'string'
|
|
1059
|
+
? metadata.clientRequestId.trim()
|
|
1060
|
+
: '';
|
|
1061
|
+
if (clientRequestId) {
|
|
1062
|
+
adapterContext.clientRequestId = clientRequestId;
|
|
1063
|
+
}
|
|
1064
|
+
const groupRequestId = typeof metadata.groupRequestId === 'string'
|
|
1065
|
+
? metadata.groupRequestId.trim()
|
|
1066
|
+
: '';
|
|
1067
|
+
if (groupRequestId) {
|
|
1068
|
+
adapterContext.groupRequestId = groupRequestId;
|
|
1069
|
+
}
|
|
1070
|
+
if (typeof metadata.originalModelId === 'string') {
|
|
1071
|
+
adapterContext.originalModelId = metadata.originalModelId;
|
|
1072
|
+
}
|
|
1073
|
+
if (typeof metadata.clientModelId === 'string') {
|
|
1074
|
+
adapterContext.clientModelId = metadata.clientModelId;
|
|
1075
|
+
}
|
|
1076
|
+
if (typeof metadata.assignedModelId === 'string') {
|
|
1077
|
+
adapterContext.modelId = metadata.assignedModelId;
|
|
1078
|
+
}
|
|
1079
|
+
const rt = cloneRuntimeMetadata(metadata);
|
|
1080
|
+
if (rt) {
|
|
1081
|
+
adapterContext.__rt = rt;
|
|
1082
|
+
}
|
|
1083
|
+
const sessionId = typeof metadata.sessionId === 'string'
|
|
1084
|
+
? metadata.sessionId.trim()
|
|
1085
|
+
: '';
|
|
1086
|
+
if (sessionId) {
|
|
1087
|
+
adapterContext.sessionId = sessionId;
|
|
1088
|
+
}
|
|
1089
|
+
const conversationId = typeof metadata.conversationId === 'string'
|
|
1090
|
+
? metadata.conversationId.trim()
|
|
1091
|
+
: '';
|
|
1092
|
+
if (conversationId) {
|
|
1093
|
+
adapterContext.conversationId = conversationId;
|
|
1094
|
+
}
|
|
1095
|
+
const clientConnectionState = metadata.clientConnectionState;
|
|
1096
|
+
if (clientConnectionState && typeof clientConnectionState === 'object' && !Array.isArray(clientConnectionState)) {
|
|
1097
|
+
const stateRecord = clientConnectionState;
|
|
1098
|
+
adapterContext.clientConnectionState = clientConnectionState;
|
|
1099
|
+
if (typeof stateRecord.disconnected === 'boolean') {
|
|
1100
|
+
adapterContext.clientDisconnected = stateRecord.disconnected;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const clientDisconnectedRaw = metadata.clientDisconnected;
|
|
1104
|
+
if (clientDisconnectedRaw === true ||
|
|
1105
|
+
(typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
|
|
1106
|
+
adapterContext.clientDisconnected = true;
|
|
1107
|
+
}
|
|
1108
|
+
if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
|
|
1109
|
+
adapterContext.compatibilityProfile = target.compatibilityProfile;
|
|
1110
|
+
}
|
|
1111
|
+
return adapterContext;
|
|
1112
|
+
}
|
|
1113
|
+
maybeCreateStageRecorder(context, endpoint, options) {
|
|
1114
|
+
if (options?.disableSnapshots === true) {
|
|
1115
|
+
return undefined;
|
|
1116
|
+
}
|
|
1117
|
+
if (!shouldRecordSnapshots()) {
|
|
1118
|
+
return undefined;
|
|
1119
|
+
}
|
|
1120
|
+
const effectiveEndpoint = endpoint || context.entryEndpoint || '/v1/chat/completions';
|
|
1121
|
+
try {
|
|
1122
|
+
return createSnapshotRecorder(context, effectiveEndpoint);
|
|
1123
|
+
}
|
|
1124
|
+
catch {
|
|
1125
|
+
return undefined;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
asJsonObject(value) {
|
|
1129
|
+
if (!value || typeof value !== 'object') {
|
|
1130
|
+
throw new Error('Responses pipeline requires JSON object payload');
|
|
1131
|
+
}
|
|
1132
|
+
return value;
|
|
1133
|
+
}
|
|
1134
|
+
async normalizeRequest(request) {
|
|
1135
|
+
if (!request || typeof request !== 'object') {
|
|
1136
|
+
throw new Error('HubPipeline requires request payload');
|
|
1137
|
+
}
|
|
1138
|
+
const id = request.id || `req_${Date.now()}`;
|
|
1139
|
+
const endpoint = normalizeEndpoint(request.endpoint);
|
|
1140
|
+
const metadataRecord = {
|
|
1141
|
+
...(request.metadata ?? {})
|
|
1142
|
+
};
|
|
1143
|
+
const policyOverride = extractHubPolicyOverride(metadataRecord);
|
|
1144
|
+
if (Object.prototype.hasOwnProperty.call(metadataRecord, '__hubPolicyOverride')) {
|
|
1145
|
+
delete metadataRecord.__hubPolicyOverride;
|
|
1146
|
+
}
|
|
1147
|
+
const shadowCompare = extractHubShadowCompareConfig(metadataRecord);
|
|
1148
|
+
if (Object.prototype.hasOwnProperty.call(metadataRecord, '__hubShadowCompare')) {
|
|
1149
|
+
delete metadataRecord.__hubShadowCompare;
|
|
1150
|
+
}
|
|
1151
|
+
const disableSnapshots = metadataRecord.__disableHubSnapshots === true;
|
|
1152
|
+
if (Object.prototype.hasOwnProperty.call(metadataRecord, '__disableHubSnapshots')) {
|
|
1153
|
+
delete metadataRecord.__disableHubSnapshots;
|
|
1154
|
+
}
|
|
1155
|
+
const hubEntryRaw = typeof metadataRecord.__hubEntry === 'string'
|
|
1156
|
+
? String(metadataRecord.__hubEntry).trim().toLowerCase()
|
|
1157
|
+
: '';
|
|
1158
|
+
const hubEntryMode = hubEntryRaw === 'chat_process' || hubEntryRaw === 'chat-process' || hubEntryRaw === 'chatprocess'
|
|
1159
|
+
? 'chat_process'
|
|
1160
|
+
: undefined;
|
|
1161
|
+
if (Object.prototype.hasOwnProperty.call(metadataRecord, '__hubEntry')) {
|
|
1162
|
+
delete metadataRecord.__hubEntry;
|
|
1163
|
+
}
|
|
1164
|
+
const entryEndpoint = typeof metadataRecord.entryEndpoint === 'string'
|
|
1165
|
+
? normalizeEndpoint(metadataRecord.entryEndpoint)
|
|
1166
|
+
: endpoint;
|
|
1167
|
+
const providerProtocol = resolveProviderProtocol(metadataRecord.providerProtocol);
|
|
1168
|
+
const processMode = metadataRecord.processMode === 'passthrough' ? 'passthrough' : 'chat';
|
|
1169
|
+
const direction = metadataRecord.direction === 'response' ? 'response' : 'request';
|
|
1170
|
+
const stage = metadataRecord.stage === 'outbound' ? 'outbound' : 'inbound';
|
|
1171
|
+
const resolvedReadable = this.unwrapReadable(request.payload);
|
|
1172
|
+
const stream = Boolean(metadataRecord.stream ||
|
|
1173
|
+
resolvedReadable ||
|
|
1174
|
+
(request.payload && typeof request.payload === 'object' && request.payload.stream));
|
|
1175
|
+
const payload = await this.materializePayload(request.payload, {
|
|
1176
|
+
requestId: id,
|
|
1177
|
+
entryEndpoint,
|
|
1178
|
+
providerProtocol,
|
|
1179
|
+
metadata: metadataRecord
|
|
1180
|
+
}, resolvedReadable);
|
|
1181
|
+
const normalizedMetadata = {
|
|
1182
|
+
...metadataRecord,
|
|
1183
|
+
entryEndpoint,
|
|
1184
|
+
providerProtocol,
|
|
1185
|
+
processMode,
|
|
1186
|
+
direction,
|
|
1187
|
+
stage,
|
|
1188
|
+
stream
|
|
1189
|
+
};
|
|
1190
|
+
const routeHint = typeof metadataRecord.routeHint === 'string' ? metadataRecord.routeHint : undefined;
|
|
1191
|
+
if (routeHint) {
|
|
1192
|
+
normalizedMetadata.routeHint = routeHint;
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
id,
|
|
1196
|
+
endpoint,
|
|
1197
|
+
entryEndpoint,
|
|
1198
|
+
providerProtocol,
|
|
1199
|
+
payload,
|
|
1200
|
+
metadata: normalizedMetadata,
|
|
1201
|
+
policyOverride: policyOverride ?? undefined,
|
|
1202
|
+
shadowCompare: shadowCompare ?? undefined,
|
|
1203
|
+
disableSnapshots,
|
|
1204
|
+
processMode,
|
|
1205
|
+
direction,
|
|
1206
|
+
stage,
|
|
1207
|
+
stream,
|
|
1208
|
+
routeHint,
|
|
1209
|
+
...(hubEntryMode ? { hubEntryMode } : {})
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
convertProcessNodeResult(id, result) {
|
|
1213
|
+
return {
|
|
1214
|
+
id,
|
|
1215
|
+
success: result.success,
|
|
1216
|
+
metadata: result.metadata,
|
|
1217
|
+
error: result.error
|
|
1218
|
+
? {
|
|
1219
|
+
code: result.error.code ?? 'hub_chat_process_error',
|
|
1220
|
+
message: result.error.message,
|
|
1221
|
+
details: result.error.details
|
|
1222
|
+
}
|
|
1223
|
+
: undefined
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
async materializePayload(payload, context, resolvedStream) {
|
|
1227
|
+
const stream = resolvedStream ?? this.unwrapReadable(payload);
|
|
1228
|
+
if (stream) {
|
|
1229
|
+
return await this.convertSsePayload(stream, context);
|
|
1230
|
+
}
|
|
1231
|
+
if (!payload || typeof payload !== 'object') {
|
|
1232
|
+
throw new Error('HubPipeline requires JSON object payload');
|
|
1233
|
+
}
|
|
1234
|
+
return payload;
|
|
1235
|
+
}
|
|
1236
|
+
unwrapReadable(payload) {
|
|
1237
|
+
if (!payload) {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
if (payload instanceof Readable) {
|
|
1241
|
+
return payload;
|
|
1242
|
+
}
|
|
1243
|
+
if (payload && typeof payload === 'object' && 'readable' in payload) {
|
|
1244
|
+
const candidate = payload.readable;
|
|
1245
|
+
if (candidate instanceof Readable) {
|
|
1246
|
+
return candidate;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
async convertSsePayload(stream, context) {
|
|
1252
|
+
const protocol = this.resolveSseProtocol(context);
|
|
1253
|
+
const codec = defaultSseCodecRegistry.get(protocol);
|
|
1254
|
+
try {
|
|
1255
|
+
const result = await codec.convertSseToJson(stream, {
|
|
1256
|
+
requestId: context.requestId,
|
|
1257
|
+
model: this.extractModelHint(context.metadata),
|
|
1258
|
+
direction: 'request'
|
|
1259
|
+
});
|
|
1260
|
+
if (!result || typeof result !== 'object') {
|
|
1261
|
+
throw new Error('SSE conversion returned empty payload');
|
|
1262
|
+
}
|
|
1263
|
+
return result;
|
|
1264
|
+
}
|
|
1265
|
+
catch (error) {
|
|
1266
|
+
const message = error instanceof Error ? error.message : String(error ?? 'Unknown error');
|
|
1267
|
+
throw new Error(`Failed to convert SSE payload for protocol ${protocol}: ${message}`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
resolveSseProtocol(context) {
|
|
1271
|
+
const explicitProtocol = resolveSseProtocolFromMetadata(context.metadata);
|
|
1272
|
+
if (explicitProtocol) {
|
|
1273
|
+
return explicitProtocol;
|
|
1274
|
+
}
|
|
1275
|
+
return context.providerProtocol;
|
|
1276
|
+
}
|
|
1277
|
+
extractModelHint(metadata) {
|
|
1278
|
+
if (typeof metadata.model === 'string' && metadata.model.trim()) {
|
|
1279
|
+
return metadata.model;
|
|
1280
|
+
}
|
|
1281
|
+
const provider = metadata.provider;
|
|
1282
|
+
const candidates = [
|
|
1283
|
+
provider?.model,
|
|
1284
|
+
provider?.modelId,
|
|
1285
|
+
provider?.defaultModel
|
|
1286
|
+
];
|
|
1287
|
+
for (const candidate of candidates) {
|
|
1288
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
1289
|
+
return candidate;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return undefined;
|
|
1293
|
+
}
|
|
1294
|
+
resolveOutboundStreamIntent(providerPreference) {
|
|
1295
|
+
if (providerPreference === 'always') {
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
if (providerPreference === 'never') {
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
return undefined;
|
|
1302
|
+
}
|
|
1303
|
+
applyOutboundStreamPreference(request, stream) {
|
|
1304
|
+
if (!request || typeof request !== 'object') {
|
|
1305
|
+
return request;
|
|
1306
|
+
}
|
|
1307
|
+
const ops = [];
|
|
1308
|
+
if (typeof stream === 'boolean') {
|
|
1309
|
+
ops.push({ op: 'set_request_parameter_fields', fields: { stream } });
|
|
1310
|
+
ops.push({ op: 'set_request_metadata_fields', fields: { outboundStream: stream } });
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
ops.push({ op: 'unset_request_parameter_keys', keys: ['stream'] });
|
|
1314
|
+
ops.push({ op: 'unset_request_metadata_keys', keys: ['outboundStream'] });
|
|
1315
|
+
}
|
|
1316
|
+
return applyHubOperations(request, ops);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function normalizeToolCallIdStyleCandidate(value) {
|
|
1320
|
+
if (typeof value !== 'string') {
|
|
1321
|
+
return undefined;
|
|
1322
|
+
}
|
|
1323
|
+
const normalized = value.trim().toLowerCase();
|
|
1324
|
+
if (normalized === 'fc') {
|
|
1325
|
+
return 'fc';
|
|
1326
|
+
}
|
|
1327
|
+
if (normalized === 'preserve') {
|
|
1328
|
+
return 'preserve';
|
|
1329
|
+
}
|
|
1330
|
+
return undefined;
|
|
1331
|
+
}
|
|
1332
|
+
function normalizeEndpoint(endpoint) {
|
|
1333
|
+
if (!endpoint)
|
|
1334
|
+
return '/v1/chat/completions';
|
|
1335
|
+
const trimmed = endpoint.trim();
|
|
1336
|
+
if (!trimmed)
|
|
1337
|
+
return '/v1/chat/completions';
|
|
1338
|
+
const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
1339
|
+
return normalized.replace(/\/{2,}/g, '/');
|
|
1340
|
+
}
|
|
1341
|
+
function resolveProviderProtocol(value) {
|
|
1342
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
1343
|
+
return 'openai-chat';
|
|
1344
|
+
}
|
|
1345
|
+
const normalized = value.trim().toLowerCase();
|
|
1346
|
+
const mapped = PROVIDER_PROTOCOL_ALIASES[normalized];
|
|
1347
|
+
if (mapped) {
|
|
1348
|
+
return mapped;
|
|
1349
|
+
}
|
|
1350
|
+
throw new Error(`[HubPipeline] Unsupported providerProtocol "${value}". Configure a valid protocol (openai-chat|openai-responses|anthropic-messages|gemini-chat).`);
|
|
1351
|
+
}
|
|
1352
|
+
const PROVIDER_PROTOCOL_ALIASES = {
|
|
1353
|
+
'openai-chat': 'openai-chat',
|
|
1354
|
+
openai: 'openai-chat',
|
|
1355
|
+
chat: 'openai-chat',
|
|
1356
|
+
'responses': 'openai-responses',
|
|
1357
|
+
'openai-responses': 'openai-responses',
|
|
1358
|
+
'anthropic': 'anthropic-messages',
|
|
1359
|
+
'anthropic-messages': 'anthropic-messages',
|
|
1360
|
+
'messages': 'anthropic-messages',
|
|
1361
|
+
'gemini': 'gemini-chat',
|
|
1362
|
+
'google-gemini': 'gemini-chat',
|
|
1363
|
+
'gemini-chat': 'gemini-chat'
|
|
1364
|
+
};
|
|
1365
|
+
function resolveEndpointForProviderProtocol(protocol) {
|
|
1366
|
+
if (!protocol) {
|
|
1367
|
+
return undefined;
|
|
1368
|
+
}
|
|
1369
|
+
const value = protocol.toLowerCase();
|
|
1370
|
+
if (value === 'openai-responses')
|
|
1371
|
+
return '/v1/responses';
|
|
1372
|
+
if (value === 'openai-chat')
|
|
1373
|
+
return '/v1/chat/completions';
|
|
1374
|
+
if (value === 'anthropic-messages' || value === 'anthropic')
|
|
1375
|
+
return '/v1/messages';
|
|
1376
|
+
if (value === 'gemini-chat' || value === 'gemini' || value === 'google-gemini')
|
|
1377
|
+
return '/v1/responses';
|
|
1378
|
+
return undefined;
|
|
1379
|
+
}
|
|
1380
|
+
function resolveSseProtocolFromMetadata(metadata) {
|
|
1381
|
+
const candidate = metadata.sseProtocol ?? metadata.clientSseProtocol ?? metadata.routeSseProtocol;
|
|
1382
|
+
if (typeof candidate !== 'string' || !candidate.trim()) {
|
|
1383
|
+
return undefined;
|
|
1384
|
+
}
|
|
1385
|
+
return resolveProviderProtocol(candidate);
|
|
1386
|
+
}
|
|
1387
|
+
function coerceAliasMap(candidate) {
|
|
1388
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
1389
|
+
return undefined;
|
|
1390
|
+
}
|
|
1391
|
+
const normalized = {};
|
|
1392
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
1393
|
+
if (typeof key !== 'string' || typeof value !== 'string') {
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
const trimmedKey = key.trim();
|
|
1397
|
+
const trimmedValue = value.trim();
|
|
1398
|
+
if (!trimmedKey.length || !trimmedValue.length) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
normalized[trimmedKey] = trimmedValue;
|
|
1402
|
+
}
|
|
1403
|
+
return Object.keys(normalized).length ? normalized : undefined;
|
|
1404
|
+
}
|
|
1405
|
+
function readAliasMapFromSemantics(chatEnvelope) {
|
|
1406
|
+
if (!chatEnvelope?.semantics || typeof chatEnvelope.semantics !== 'object') {
|
|
1407
|
+
return undefined;
|
|
1408
|
+
}
|
|
1409
|
+
const node = chatEnvelope.semantics.tools;
|
|
1410
|
+
if (!node || !isJsonObject(node)) {
|
|
1411
|
+
return undefined;
|
|
1412
|
+
}
|
|
1413
|
+
const candidate = node.toolNameAliasMap ??
|
|
1414
|
+
node.toolAliasMap;
|
|
1415
|
+
return coerceAliasMap(candidate);
|
|
1416
|
+
}
|
|
1417
|
+
function assertNoMappableSemanticsInMetadata(metadata, scope) {
|
|
1418
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const banned = [
|
|
1422
|
+
'responsesResume',
|
|
1423
|
+
'responses_resume',
|
|
1424
|
+
'clientToolsRaw',
|
|
1425
|
+
'client_tools_raw',
|
|
1426
|
+
'anthropicToolNameMap',
|
|
1427
|
+
'anthropic_tool_name_map',
|
|
1428
|
+
// Responses semantics must be stored in chat.semantics.responses.context, not metadata.
|
|
1429
|
+
'responsesContext',
|
|
1430
|
+
'responses_context',
|
|
1431
|
+
'responseFormat',
|
|
1432
|
+
'response_format',
|
|
1433
|
+
// OpenAI Chat legacy semantics must be stored in chat.semantics (system/tools/providerExtras), not metadata.
|
|
1434
|
+
'systemInstructions',
|
|
1435
|
+
'system_instructions',
|
|
1436
|
+
'toolsFieldPresent',
|
|
1437
|
+
'tools_field_present',
|
|
1438
|
+
'extraFields',
|
|
1439
|
+
'extra_fields'
|
|
1440
|
+
];
|
|
1441
|
+
const present = banned.filter((k) => Object.prototype.hasOwnProperty.call(metadata, k));
|
|
1442
|
+
if (present.length) {
|
|
1443
|
+
throw new Error(`[HubPipeline][semantic_gate] Mappable semantics must not be stored in metadata (${scope}): ${present.join(', ')}`);
|
|
98
1444
|
}
|
|
99
1445
|
}
|