@jsonstudio/llms 0.6.633 → 0.6.749
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/anthropic-openai-codec.js +0 -5
- package/dist/conversion/codecs/openai-openai-codec.js +0 -6
- package/dist/conversion/codecs/responses-openai-codec.js +1 -7
- package/dist/conversion/hub/node-support.js +5 -4
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +14 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +82 -18
- package/dist/conversion/hub/pipeline/session-identifiers.js +132 -2
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +23 -19
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +47 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +4 -2
- package/dist/conversion/hub/process/chat-process.js +2 -0
- package/dist/conversion/hub/response/provider-response.js +6 -1
- package/dist/conversion/hub/snapshot-recorder.js +8 -1
- package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +0 -7
- package/dist/conversion/responses/responses-openai-bridge.js +47 -7
- package/dist/conversion/shared/compaction-detect.d.ts +2 -0
- package/dist/conversion/shared/compaction-detect.js +53 -0
- package/dist/conversion/shared/errors.d.ts +1 -1
- package/dist/conversion/shared/reasoning-tool-normalizer.js +7 -0
- package/dist/conversion/shared/snapshot-hooks.d.ts +2 -0
- package/dist/conversion/shared/snapshot-hooks.js +180 -4
- package/dist/conversion/shared/snapshot-utils.d.ts +4 -0
- package/dist/conversion/shared/snapshot-utils.js +4 -0
- package/dist/conversion/shared/tool-filter-pipeline.js +3 -9
- package/dist/conversion/shared/tool-governor.d.ts +2 -0
- package/dist/conversion/shared/tool-governor.js +101 -13
- package/dist/conversion/shared/tool-harvester.js +42 -2
- package/dist/filters/index.d.ts +0 -2
- package/dist/filters/index.js +0 -2
- package/dist/filters/special/request-tools-normalize.d.ts +11 -0
- package/dist/filters/special/request-tools-normalize.js +13 -50
- package/dist/filters/special/response-apply-patch-toon-decode.js +403 -82
- package/dist/filters/special/response-tool-arguments-toon-decode.js +6 -75
- package/dist/filters/utils/snapshot-writer.js +42 -4
- package/dist/guidance/index.js +8 -2
- package/dist/router/virtual-router/bootstrap.js +68 -4
- package/dist/router/virtual-router/engine-health.js +0 -4
- package/dist/router/virtual-router/engine-selection.d.ts +8 -1
- package/dist/router/virtual-router/engine-selection.js +168 -9
- package/dist/router/virtual-router/engine.d.ts +6 -1
- package/dist/router/virtual-router/engine.js +263 -14
- package/dist/router/virtual-router/load-balancer.d.ts +18 -0
- package/dist/router/virtual-router/load-balancer.js +3 -2
- package/dist/router/virtual-router/routing-instructions.d.ts +6 -0
- package/dist/router/virtual-router/routing-instructions.js +18 -3
- package/dist/router/virtual-router/sticky-session-store.d.ts +1 -0
- package/dist/router/virtual-router/sticky-session-store.js +36 -0
- package/dist/router/virtual-router/types.d.ts +29 -0
- package/dist/servertool/engine.js +335 -9
- package/dist/servertool/handlers/compaction-detect.d.ts +1 -0
- package/dist/servertool/handlers/compaction-detect.js +1 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +29 -5
- package/dist/servertool/handlers/iflow-model-error-retry.js +17 -0
- package/dist/servertool/handlers/stop-message-auto.js +199 -19
- package/dist/servertool/server-side-tools.d.ts +0 -1
- package/dist/servertool/server-side-tools.js +0 -1
- package/dist/servertool/types.d.ts +1 -0
- package/dist/tools/apply-patch-structured.js +52 -15
- package/dist/tools/tool-registry.js +537 -15
- package/dist/utils/toon.d.ts +4 -0
- package/dist/utils/toon.js +75 -0
- package/package.json +4 -2
- package/dist/test-output/virtual-router/results.json +0 -1
- package/dist/test-output/virtual-router/summary.json +0 -12
|
@@ -80,11 +80,6 @@ export class AnthropicOpenAIConversionCodec {
|
|
|
80
80
|
};
|
|
81
81
|
const engine = new FilterEngine();
|
|
82
82
|
engine.registerFilter(new ResponseToolTextCanonicalizeFilter());
|
|
83
|
-
try {
|
|
84
|
-
const { ResponseToolArgumentsToonDecodeFilter } = await import('../../filters/index.js');
|
|
85
|
-
engine.registerFilter(new ResponseToolArgumentsToonDecodeFilter());
|
|
86
|
-
}
|
|
87
|
-
catch { /* optional */ }
|
|
88
83
|
engine.registerFilter(new ResponseToolArgumentsStringifyFilter());
|
|
89
84
|
engine.registerFilter(new ResponseFinishInvariantsFilter());
|
|
90
85
|
let staged = await engine.run('response_pre', dto.data, resCtxBase);
|
|
@@ -88,12 +88,6 @@ export class OpenAIOpenAIConversionCodec {
|
|
|
88
88
|
const engine = new FilterEngine();
|
|
89
89
|
// Response-side filters (idempotent w.r.t existing logic)
|
|
90
90
|
engine.registerFilter(new ResponseToolTextCanonicalizeFilter()); // response_pre
|
|
91
|
-
try {
|
|
92
|
-
const { ResponseToolArgumentsToonDecodeFilter, ResponseApplyPatchToonDecodeFilter } = await import('../../filters/index.js');
|
|
93
|
-
engine.registerFilter(new ResponseToolArgumentsToonDecodeFilter()); // response_pre, runs before stringify
|
|
94
|
-
engine.registerFilter(new ResponseApplyPatchToonDecodeFilter()); // response_pre, runs before stringify
|
|
95
|
-
}
|
|
96
|
-
catch { /* optional */ }
|
|
97
91
|
engine.registerFilter(new ResponseToolArgumentsStringifyFilter()); // response_post
|
|
98
92
|
engine.registerFilter(new ResponseFinishInvariantsFilter()); // response_post
|
|
99
93
|
// Load field map config (if present) and register transforms
|
|
@@ -147,14 +147,8 @@ export class ResponsesOpenAIConversionCodec {
|
|
|
147
147
|
debug: { emit: () => { } }
|
|
148
148
|
};
|
|
149
149
|
const engine = new FilterEngine();
|
|
150
|
-
// Response-side filters:文本标准化 →
|
|
150
|
+
// Response-side filters:文本标准化 → apply_patch 结构化补丁规范化 → shell/basics → finish_reason 不变式
|
|
151
151
|
engine.registerFilter(new ResponseToolTextCanonicalizeFilter()); // response_pre
|
|
152
|
-
try {
|
|
153
|
-
const { ResponseToolArgumentsToonDecodeFilter, ResponseApplyPatchToonDecodeFilter } = await import('../../filters/index.js');
|
|
154
|
-
engine.registerFilter(new ResponseToolArgumentsToonDecodeFilter()); // response_pre, runs before stringify
|
|
155
|
-
engine.registerFilter(new ResponseApplyPatchToonDecodeFilter()); // response_pre, runs before stringify
|
|
156
|
-
}
|
|
157
|
-
catch { /* optional */ }
|
|
158
152
|
engine.registerFilter(new ResponseToolArgumentsStringifyFilter()); // response_post
|
|
159
153
|
engine.registerFilter(new ResponseFinishInvariantsFilter()); // response_post
|
|
160
154
|
let staged = await engine.run('response_pre', dto.data, resCtxBase);
|
|
@@ -34,8 +34,9 @@ export async function runHubInboundConversion(options) {
|
|
|
34
34
|
}
|
|
35
35
|
export async function runHubOutboundConversion(options) {
|
|
36
36
|
const adapterContext = deriveAdapterContext(options.nodeContext, options.protocol);
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Snapshots must be grouped by the client-facing entry endpoint, not by the provider protocol.
|
|
38
|
+
// A /v1/responses request routed to an anthropic upstream is still a responses *entry* request.
|
|
39
|
+
const stageRecorder = maybeCreateStageRecorder(adapterContext, options.nodeContext.request.endpoint);
|
|
39
40
|
const { outbound } = createProtocolPlans(options.protocol);
|
|
40
41
|
const chatEnvelope = standardizedToChatEnvelope(options.request, { adapterContext });
|
|
41
42
|
const payload = await runOutboundPipeline({
|
|
@@ -46,12 +47,12 @@ export async function runHubOutboundConversion(options) {
|
|
|
46
47
|
});
|
|
47
48
|
return payload;
|
|
48
49
|
}
|
|
49
|
-
function maybeCreateStageRecorder(adapterContext, defaultEndpoint
|
|
50
|
+
function maybeCreateStageRecorder(adapterContext, defaultEndpoint) {
|
|
50
51
|
const flag = process.env.ROUTECODEX_HUB_SNAPSHOTS;
|
|
51
52
|
if (flag && flag.trim() === '0') {
|
|
52
53
|
return undefined;
|
|
53
54
|
}
|
|
54
|
-
const endpoint =
|
|
55
|
+
const endpoint = defaultEndpoint || adapterContext.entryEndpoint || '/v1/chat/completions';
|
|
55
56
|
return createSnapshotRecorder(adapterContext, endpoint);
|
|
56
57
|
}
|
|
57
58
|
function resolveEndpointForProtocol(protocol) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
2
|
import type { StandardizedRequest, ProcessedRequest } from '../types/standardized.js';
|
|
3
3
|
import type { JsonObject } from '../types/json.js';
|
|
4
|
-
import type { VirtualRouterConfig, RoutingDecision, RoutingDiagnostics, TargetMetadata, VirtualRouterHealthStore } from '../../../router/virtual-router/types.js';
|
|
4
|
+
import type { VirtualRouterConfig, RoutingDecision, RoutingDiagnostics, TargetMetadata, VirtualRouterHealthStore, ProviderQuotaView } from '../../../router/virtual-router/types.js';
|
|
5
5
|
import { type HubProcessNodeResult } from '../process/chat-process.js';
|
|
6
6
|
export interface HubPipelineConfig {
|
|
7
7
|
virtualRouter: VirtualRouterConfig;
|
|
@@ -10,6 +10,19 @@ export interface HubPipelineConfig {
|
|
|
10
10
|
* 当提供时,VirtualRouterEngine 将在初始化时恢复上一次快照,并在 cooldown/熔断变化时调用 persistSnapshot。
|
|
11
11
|
*/
|
|
12
12
|
healthStore?: VirtualRouterHealthStore;
|
|
13
|
+
/**
|
|
14
|
+
* 可选:路由状态存储,用于持久化 sticky routing / stopMessage 等指令状态。
|
|
15
|
+
*/
|
|
16
|
+
routingStateStore?: {
|
|
17
|
+
loadSync(key: string): unknown;
|
|
18
|
+
saveAsync(key: string, state: unknown): void;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* 可选:配额视图。若提供,VirtualRouterEngine 将在路由过程中参考
|
|
22
|
+
* provider 的 quota 状态(inPool/priorityTier/cooldownUntil/blacklistUntil)
|
|
23
|
+
* 过滤目标并按优先级分层调度。
|
|
24
|
+
*/
|
|
25
|
+
quotaView?: ProviderQuotaView;
|
|
13
26
|
}
|
|
14
27
|
export interface HubPipelineRequestMetadata extends Record<string, unknown> {
|
|
15
28
|
entryEndpoint?: string;
|
|
@@ -24,6 +24,7 @@ import { runReqOutboundStage2FormatBuild } from './stages/req_outbound/req_outbo
|
|
|
24
24
|
import { runReqOutboundStage3Compat } from './stages/req_outbound/req_outbound_stage3_compat/index.js';
|
|
25
25
|
import { extractSessionIdentifiersFromMetadata } from './session-identifiers.js';
|
|
26
26
|
import { computeRequestTokens } from '../../../router/virtual-router/token-estimator.js';
|
|
27
|
+
import { isCompactionRequest } from '../../shared/compaction-detect.js';
|
|
27
28
|
export class HubPipeline {
|
|
28
29
|
routerEngine;
|
|
29
30
|
config;
|
|
@@ -31,7 +32,9 @@ export class HubPipeline {
|
|
|
31
32
|
constructor(config) {
|
|
32
33
|
this.config = config;
|
|
33
34
|
this.routerEngine = new VirtualRouterEngine({
|
|
34
|
-
healthStore: config.healthStore
|
|
35
|
+
healthStore: config.healthStore,
|
|
36
|
+
routingStateStore: config.routingStateStore,
|
|
37
|
+
quotaView: config.quotaView
|
|
35
38
|
});
|
|
36
39
|
this.routerEngine.initialize(config.virtualRouter);
|
|
37
40
|
try {
|
|
@@ -58,9 +61,13 @@ export class HubPipeline {
|
|
|
58
61
|
async executeRequestStagePipeline(normalized, hooks) {
|
|
59
62
|
const formatAdapter = hooks.createFormatAdapter();
|
|
60
63
|
const semanticMapper = hooks.createSemanticMapper();
|
|
64
|
+
const rawRequest = this.asJsonObject(normalized.payload);
|
|
65
|
+
if (isCompactionRequest(rawRequest)) {
|
|
66
|
+
normalized.metadata = normalized.metadata || {};
|
|
67
|
+
normalized.metadata.compactionRequest = true;
|
|
68
|
+
}
|
|
61
69
|
const inboundAdapterContext = this.buildAdapterContext(normalized);
|
|
62
70
|
const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint);
|
|
63
|
-
const rawRequest = this.asJsonObject(normalized.payload);
|
|
64
71
|
const inboundStart = Date.now();
|
|
65
72
|
const formatEnvelope = await runReqInboundStage1FormatParse({
|
|
66
73
|
rawRequest,
|
|
@@ -143,6 +150,15 @@ export class HubPipeline {
|
|
|
143
150
|
const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
|
|
144
151
|
stdMetadata?.serverToolRequired === true;
|
|
145
152
|
const sessionIdentifiers = extractSessionIdentifiersFromMetadata(normalized.metadata);
|
|
153
|
+
// 将从 metadata / clientHeaders 中解析出的会话标识同步回 normalized.metadata,
|
|
154
|
+
// 便于后续 AdapterContext(响应侧 servertool)也能访问到相同的 sessionId /
|
|
155
|
+
// conversationId,用于 sticky-session 相关逻辑(例如 stopMessage)。
|
|
156
|
+
if (sessionIdentifiers.sessionId && normalized.metadata && typeof normalized.metadata === 'object') {
|
|
157
|
+
normalized.metadata.sessionId = sessionIdentifiers.sessionId;
|
|
158
|
+
}
|
|
159
|
+
if (sessionIdentifiers.conversationId && normalized.metadata && typeof normalized.metadata === 'object') {
|
|
160
|
+
normalized.metadata.conversationId = sessionIdentifiers.conversationId;
|
|
161
|
+
}
|
|
146
162
|
const metadataInput = {
|
|
147
163
|
requestId: normalized.id,
|
|
148
164
|
entryEndpoint: normalized.entryEndpoint,
|
|
@@ -164,6 +180,10 @@ export class HubPipeline {
|
|
|
164
180
|
normalizedMetadata: normalized.metadata,
|
|
165
181
|
stageRecorder: inboundRecorder
|
|
166
182
|
});
|
|
183
|
+
const stopMessageState = this.routerEngine.getStopMessageState(metadataInput);
|
|
184
|
+
if (stopMessageState && normalized.metadata && typeof normalized.metadata === 'object') {
|
|
185
|
+
normalized.metadata.stopMessageState = stopMessageState;
|
|
186
|
+
}
|
|
167
187
|
// Emit virtual router hit log for debugging (orange [virtual-router] ...)
|
|
168
188
|
try {
|
|
169
189
|
const routeName = routing.decision?.routeName;
|
|
@@ -193,8 +213,10 @@ export class HubPipeline {
|
|
|
193
213
|
const outboundFormatAdapter = protocolSwitch ? outboundHooks.createFormatAdapter() : formatAdapter;
|
|
194
214
|
const outboundContextMetadataKey = protocolSwitch ? outboundHooks.contextMetadataKey : hooks.contextMetadataKey;
|
|
195
215
|
const outboundContextSnapshot = protocolSwitch ? undefined : contextSnapshot;
|
|
196
|
-
|
|
197
|
-
|
|
216
|
+
// Snapshots must be grouped by entry endpoint (client-facing protocol), not by provider protocol.
|
|
217
|
+
// Otherwise one request would be split across multiple folders (e.g. openai-responses + anthropic-messages),
|
|
218
|
+
// which breaks codex-samples correlation.
|
|
219
|
+
const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint);
|
|
198
220
|
const outboundStart = Date.now();
|
|
199
221
|
let providerPayload;
|
|
200
222
|
const outboundStage1 = await runReqOutboundStage1SemanticMap({
|
|
@@ -239,22 +261,25 @@ export class HubPipeline {
|
|
|
239
261
|
// route 将 processMode 标记为 passthrough,我们仍然需要保留一次规范化后的
|
|
240
262
|
// Chat 请求快照,供 stopMessage / gemini_empty_reply_continue 等被动触发型
|
|
241
263
|
// servertool 在响应阶段使用。
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
264
|
+
//
|
|
265
|
+
// 之前这里通过 JSON.stringify/parse 做深拷贝,但在部分 Responses/Gemini
|
|
266
|
+
// 场景下,workingRequest 上携带的 metadata 可能包含无法安全序列化的字段,
|
|
267
|
+
// 导致克隆过程抛错、capturedChatRequest 被静默丢弃,从而让响应侧的
|
|
268
|
+
// stop_message_auto 等 ServerTool 无法获取上一跳的 Chat 请求。
|
|
269
|
+
//
|
|
270
|
+
// 对于 capturedChatRequest,我们只需要一个“可读快照”,不会在后续流程中
|
|
271
|
+
// 对其做就地修改,因此可以直接使用浅拷贝结构,避免序列化失败导致整段
|
|
272
|
+
// 逻辑失效。
|
|
273
|
+
const capturedChatRequest = {
|
|
274
|
+
model: workingRequest.model,
|
|
275
|
+
messages: Array.isArray(workingRequest.messages) ? [...workingRequest.messages] : workingRequest.messages,
|
|
276
|
+
tools: workingRequest.tools,
|
|
277
|
+
parameters: workingRequest.parameters
|
|
278
|
+
};
|
|
254
279
|
const metadata = {
|
|
255
280
|
...normalized.metadata,
|
|
256
281
|
...(hasImageAttachment ? { hasImageAttachment: true } : {}),
|
|
257
|
-
|
|
282
|
+
capturedChatRequest,
|
|
258
283
|
entryEndpoint: normalized.entryEndpoint,
|
|
259
284
|
providerProtocol: outboundProtocol,
|
|
260
285
|
stream: normalized.stream,
|
|
@@ -369,6 +394,18 @@ export class HubPipeline {
|
|
|
369
394
|
streamingHint,
|
|
370
395
|
toolCallIdStyle
|
|
371
396
|
};
|
|
397
|
+
const clientRequestId = typeof metadata.clientRequestId === 'string'
|
|
398
|
+
? metadata.clientRequestId.trim()
|
|
399
|
+
: '';
|
|
400
|
+
if (clientRequestId) {
|
|
401
|
+
adapterContext.clientRequestId = clientRequestId;
|
|
402
|
+
}
|
|
403
|
+
const groupRequestId = typeof metadata.groupRequestId === 'string'
|
|
404
|
+
? metadata.groupRequestId.trim()
|
|
405
|
+
: '';
|
|
406
|
+
if (groupRequestId) {
|
|
407
|
+
adapterContext.groupRequestId = groupRequestId;
|
|
408
|
+
}
|
|
372
409
|
if (typeof metadata.originalModelId === 'string') {
|
|
373
410
|
adapterContext.originalModelId = metadata.originalModelId;
|
|
374
411
|
}
|
|
@@ -379,7 +416,8 @@ export class HubPipeline {
|
|
|
379
416
|
adapterContext.modelId = metadata.assignedModelId;
|
|
380
417
|
}
|
|
381
418
|
// 将 serverToolFollowup 等 ServerTool 相关标记从 normalized.metadata 透传到 AdapterContext,
|
|
382
|
-
// 便于响应侧的 convertProviderResponse
|
|
419
|
+
// 便于响应侧的 convertProviderResponse / 各 ServerTool handler 识别“二跳/内部跳转”,
|
|
420
|
+
// 再由具体 handler 决定是否在 followup 响应中继续生效(例如 stopMessage 多轮续写)。
|
|
383
421
|
if (Object.prototype.hasOwnProperty.call(metadata, 'serverToolFollowup')) {
|
|
384
422
|
adapterContext.serverToolFollowup = metadata
|
|
385
423
|
.serverToolFollowup;
|
|
@@ -396,6 +434,28 @@ export class HubPipeline {
|
|
|
396
434
|
if (conversationId) {
|
|
397
435
|
adapterContext.conversationId = conversationId;
|
|
398
436
|
}
|
|
437
|
+
const stopMessageState = metadata.stopMessageState;
|
|
438
|
+
if (stopMessageState && typeof stopMessageState === 'object') {
|
|
439
|
+
adapterContext.stopMessageState = stopMessageState;
|
|
440
|
+
}
|
|
441
|
+
const compactionRequest = metadata.compactionRequest;
|
|
442
|
+
if (compactionRequest === true ||
|
|
443
|
+
(typeof compactionRequest === 'string' && compactionRequest.trim().toLowerCase() === 'true')) {
|
|
444
|
+
adapterContext.compactionRequest = true;
|
|
445
|
+
}
|
|
446
|
+
const clientConnectionState = metadata.clientConnectionState;
|
|
447
|
+
if (clientConnectionState && typeof clientConnectionState === 'object' && !Array.isArray(clientConnectionState)) {
|
|
448
|
+
const stateRecord = clientConnectionState;
|
|
449
|
+
adapterContext.clientConnectionState = clientConnectionState;
|
|
450
|
+
if (typeof stateRecord.disconnected === 'boolean') {
|
|
451
|
+
adapterContext.clientDisconnected = stateRecord.disconnected;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const clientDisconnectedRaw = metadata.clientDisconnected;
|
|
455
|
+
if (clientDisconnectedRaw === true ||
|
|
456
|
+
(typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
|
|
457
|
+
adapterContext.clientDisconnected = true;
|
|
458
|
+
}
|
|
399
459
|
const responsesResume = metadata.responsesResume &&
|
|
400
460
|
typeof metadata.responsesResume === 'object'
|
|
401
461
|
? metadata.responsesResume
|
|
@@ -403,6 +463,10 @@ export class HubPipeline {
|
|
|
403
463
|
if (responsesResume) {
|
|
404
464
|
adapterContext.responsesResume = responsesResume;
|
|
405
465
|
}
|
|
466
|
+
const serverToolLoopState = metadata.serverToolLoopState;
|
|
467
|
+
if (serverToolLoopState && typeof serverToolLoopState === 'object' && !Array.isArray(serverToolLoopState)) {
|
|
468
|
+
adapterContext.serverToolLoopState = serverToolLoopState;
|
|
469
|
+
}
|
|
406
470
|
// 透传 gemini_empty_reply_continue 的重试计数,便于在多次空回复后终止自动续写。
|
|
407
471
|
const emptyReplyCount = metadata.geminiEmptyReplyCount;
|
|
408
472
|
if (typeof emptyReplyCount === 'number' && Number.isFinite(emptyReplyCount)) {
|
|
@@ -2,9 +2,9 @@ export function extractSessionIdentifiersFromMetadata(metadata) {
|
|
|
2
2
|
const directSession = normalizeIdentifier(metadata?.sessionId);
|
|
3
3
|
const directConversation = normalizeIdentifier(metadata?.conversationId);
|
|
4
4
|
const headers = coerceClientHeaders(metadata?.clientHeaders);
|
|
5
|
-
|
|
5
|
+
let sessionId = directSession ||
|
|
6
6
|
(headers ? pickHeader(headers, ['session_id', 'session-id', 'x-session-id', 'anthropic-session-id']) : undefined);
|
|
7
|
-
|
|
7
|
+
let conversationId = directConversation ||
|
|
8
8
|
(headers
|
|
9
9
|
? pickHeader(headers, [
|
|
10
10
|
'conversation_id',
|
|
@@ -14,6 +14,11 @@ export function extractSessionIdentifiersFromMetadata(metadata) {
|
|
|
14
14
|
'openai-conversation-id'
|
|
15
15
|
])
|
|
16
16
|
: undefined);
|
|
17
|
+
if (!sessionId || !conversationId) {
|
|
18
|
+
const fallback = deriveIdentifiersFromRawPayload(metadata);
|
|
19
|
+
sessionId = sessionId || fallback.sessionId;
|
|
20
|
+
conversationId = conversationId || fallback.conversationId || fallback.sessionId;
|
|
21
|
+
}
|
|
17
22
|
return {
|
|
18
23
|
...(sessionId ? { sessionId } : {}),
|
|
19
24
|
...(conversationId ? { conversationId } : {})
|
|
@@ -74,3 +79,128 @@ function normalizeIdentifier(value) {
|
|
|
74
79
|
const trimmed = value.trim();
|
|
75
80
|
return trimmed || undefined;
|
|
76
81
|
}
|
|
82
|
+
const SESSION_TOKEN_REGEX = /session[_:\-\s]?([0-9a-f]{8,}(?:-[0-9a-f]{4,}){0,5})/i;
|
|
83
|
+
const CONVERSATION_TOKEN_REGEX = /conversation[_:\-\s]?([0-9a-f]{8,}(?:-[0-9a-f]{4,}){0,5})/i;
|
|
84
|
+
const SESSION_FIELD_KEYS = ['sessionid', 'session_id', 'session-id', 'anthropic-session-id'];
|
|
85
|
+
const CONVERSATION_FIELD_KEYS = ['conversationid', 'conversation_id', 'conversation-id', 'anthropic-conversation-id', 'openai-conversation-id'];
|
|
86
|
+
function deriveIdentifiersFromRawPayload(metadata) {
|
|
87
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
const raw = metadata.__raw_request_body;
|
|
91
|
+
const targets = [];
|
|
92
|
+
let rawUserMetadataSession;
|
|
93
|
+
if (raw !== undefined) {
|
|
94
|
+
targets.push(raw);
|
|
95
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
96
|
+
const rawRecord = raw;
|
|
97
|
+
rawUserMetadataSession = extractSessionIdFromUserMetadata(rawRecord);
|
|
98
|
+
if (rawRecord.metadata) {
|
|
99
|
+
targets.push(rawRecord.metadata);
|
|
100
|
+
}
|
|
101
|
+
if (rawRecord.rawText) {
|
|
102
|
+
targets.push(rawRecord.rawText);
|
|
103
|
+
}
|
|
104
|
+
if (rawRecord.events) {
|
|
105
|
+
targets.push(rawRecord.events);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let sessionId;
|
|
110
|
+
let conversationId;
|
|
111
|
+
if (rawUserMetadataSession) {
|
|
112
|
+
sessionId = rawUserMetadataSession;
|
|
113
|
+
conversationId = rawUserMetadataSession;
|
|
114
|
+
}
|
|
115
|
+
for (const candidate of targets) {
|
|
116
|
+
if (!sessionId) {
|
|
117
|
+
sessionId = findIdentifier(candidate, SESSION_FIELD_KEYS, SESSION_TOKEN_REGEX);
|
|
118
|
+
}
|
|
119
|
+
if (!conversationId) {
|
|
120
|
+
conversationId = findIdentifier(candidate, CONVERSATION_FIELD_KEYS, CONVERSATION_TOKEN_REGEX);
|
|
121
|
+
}
|
|
122
|
+
if (sessionId && conversationId) {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!sessionId && conversationId) {
|
|
127
|
+
sessionId = conversationId;
|
|
128
|
+
}
|
|
129
|
+
else if (sessionId && !conversationId) {
|
|
130
|
+
conversationId = sessionId;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
...(sessionId ? { sessionId } : {}),
|
|
134
|
+
...(conversationId ? { conversationId } : {})
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function extractSessionIdFromUserMetadata(raw) {
|
|
138
|
+
const metadataNode = raw.metadata;
|
|
139
|
+
if (!metadataNode || typeof metadataNode !== 'object') {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const userId = metadataNode.user_id;
|
|
143
|
+
if (typeof userId !== 'string') {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const trimmed = userId.trim();
|
|
147
|
+
if (!trimmed) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const match = trimmed.match(SESSION_TOKEN_REGEX);
|
|
151
|
+
if (match && match[1]) {
|
|
152
|
+
return match[1];
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
function findIdentifier(source, preferredKeys, regex) {
|
|
157
|
+
if (source === undefined || source === null) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
if (typeof source === 'string') {
|
|
161
|
+
const trimmed = source.trim();
|
|
162
|
+
if (!trimmed) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const match = trimmed.match(regex);
|
|
166
|
+
if (match && match[1]) {
|
|
167
|
+
return match[1];
|
|
168
|
+
}
|
|
169
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(trimmed);
|
|
172
|
+
return findIdentifier(parsed, preferredKeys, regex);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
if (Array.isArray(source)) {
|
|
181
|
+
for (const entry of source) {
|
|
182
|
+
const candidate = findIdentifier(entry, preferredKeys, regex);
|
|
183
|
+
if (candidate) {
|
|
184
|
+
return candidate;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
if (typeof source === 'object') {
|
|
190
|
+
const record = source;
|
|
191
|
+
for (const [key, value] of Object.entries(record)) {
|
|
192
|
+
const lowered = typeof key === 'string' ? key.toLowerCase() : '';
|
|
193
|
+
if (preferredKeys.includes(lowered)) {
|
|
194
|
+
const normalized = normalizeIdentifier(value);
|
|
195
|
+
if (normalized) {
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const fromNested = findIdentifier(value, preferredKeys, regex);
|
|
200
|
+
if (fromNested) {
|
|
201
|
+
return fromNested;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { captureResponsesContext, buildChatRequestFromResponses } from '../../../../../responses/responses-openai-bridge.js';
|
|
2
2
|
import { recordStage } from '../../../stages/utils.js';
|
|
3
3
|
export async function runReqInboundStage3ContextCapture(options) {
|
|
4
|
+
// 对由 server-side 工具触发的二跳/内部跳转请求(例如 stopMessage followup),
|
|
5
|
+
// 跳过工具输出扫描与 apply_patch 诊断日志,避免在内部流中重复放大客户端已收到的
|
|
6
|
+
// 工具错误信息。此类请求的工具治理在 chat-process 阶段完成,这里仅保留最小快照。
|
|
7
|
+
try {
|
|
8
|
+
const ctx = options.adapterContext;
|
|
9
|
+
const followupFlag = ctx?.serverToolFollowup;
|
|
10
|
+
const isFollowup = followupFlag === true ||
|
|
11
|
+
(typeof followupFlag === 'string' && followupFlag.trim().toLowerCase() === 'true');
|
|
12
|
+
if (isFollowup) {
|
|
13
|
+
const snapshot = {
|
|
14
|
+
providerProtocol: options.adapterContext.providerProtocol ?? 'unknown',
|
|
15
|
+
tool_outputs: []
|
|
16
|
+
};
|
|
17
|
+
recordStage(options.stageRecorder, 'req_inbound_stage3_context_capture', snapshot);
|
|
18
|
+
return snapshot;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// best-effort: 若检测 serverToolFollowup 失败,则继续走通用路径
|
|
23
|
+
}
|
|
4
24
|
let context;
|
|
5
25
|
if (options.captureContext) {
|
|
6
26
|
try {
|
|
@@ -104,23 +124,6 @@ function collectToolOutputs(payload) {
|
|
|
104
124
|
if (!id) {
|
|
105
125
|
return;
|
|
106
126
|
}
|
|
107
|
-
// 针对 apply_patch 工具的失败结果做醒目日志,便于监控
|
|
108
|
-
try {
|
|
109
|
-
const name = typeof entry.name === 'string' ? entry.name.trim() : undefined;
|
|
110
|
-
const output = typeof entry.output === 'string' ? entry.output : undefined;
|
|
111
|
-
if (name === 'apply_patch' && output) {
|
|
112
|
-
const lower = output.toLowerCase();
|
|
113
|
-
if (lower.includes('apply_patch verification failed') ||
|
|
114
|
-
lower.includes('failed to parse function arguments')) {
|
|
115
|
-
const firstLine = output.split('\n')[0] ?? output;
|
|
116
|
-
// eslint-disable-next-line no-console
|
|
117
|
-
console.error(`\x1b[31m[apply_patch][tool_error] tool_call_id=${id} ${firstLine}\x1b[0m`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
// logging best-effort
|
|
123
|
-
}
|
|
124
127
|
if (seen.has(id)) {
|
|
125
128
|
return;
|
|
126
129
|
}
|
|
@@ -254,6 +257,7 @@ function readResponsesInputToolOutputs(payload) {
|
|
|
254
257
|
})
|
|
255
258
|
.filter((entry) => Boolean(entry));
|
|
256
259
|
}
|
|
260
|
+
const loggedApplyPatchErrorIds = new Set();
|
|
257
261
|
function normalizeToolOutputEntry(entry) {
|
|
258
262
|
if (!entry || typeof entry !== 'object') {
|
|
259
263
|
return undefined;
|
|
@@ -299,10 +303,10 @@ function buildApplyPatchDiagnostics(output) {
|
|
|
299
303
|
return undefined;
|
|
300
304
|
}
|
|
301
305
|
if (output.includes('missing field `input`')) {
|
|
302
|
-
return '\n\n[RouteCodex precheck] apply_patch 参数解析失败:缺少字段 "input"。当前 RouteCodex 期望 { input, patch }
|
|
306
|
+
return '\n\n[RouteCodex precheck] apply_patch 参数解析失败:缺少字段 "input"。当前 RouteCodex 期望 { input, patch } 形态,并且两个字段都应包含完整统一 diff 文本。';
|
|
303
307
|
}
|
|
304
308
|
if (output.includes('invalid type: map, expected a string')) {
|
|
305
|
-
return '\n\n[RouteCodex precheck] apply_patch 参数类型错误:检测到 JSON 对象(map),但客户端期望字符串。请先对参数做 JSON.stringify 再写入 arguments,或直接提供 { patch: \"<统一 diff>\" }
|
|
309
|
+
return '\n\n[RouteCodex precheck] apply_patch 参数类型错误:检测到 JSON 对象(map),但客户端期望字符串。请先对参数做 JSON.stringify 再写入 arguments,或直接提供 { patch: \"<统一 diff>\" } 形式。';
|
|
306
310
|
}
|
|
307
311
|
return undefined;
|
|
308
312
|
}
|
package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { defaultSseCodecRegistry } from '../../../../../../sse/index.js';
|
|
2
2
|
import { recordStage } from '../../../stages/utils.js';
|
|
3
3
|
import { ProviderProtocolError } from '../../../../../shared/errors.js';
|
|
4
|
+
function extractSseWrapperError(payload) {
|
|
5
|
+
return findSseWrapperError(payload, 2);
|
|
6
|
+
}
|
|
7
|
+
function findSseWrapperError(record, depth) {
|
|
8
|
+
if (!record || typeof record !== 'object' || depth < 0) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
const mode = record.mode;
|
|
12
|
+
const errVal = record.error;
|
|
13
|
+
if (mode === 'sse' && typeof errVal === 'string' && errVal.trim()) {
|
|
14
|
+
return errVal.trim();
|
|
15
|
+
}
|
|
16
|
+
const nestedKeys = ['body', 'data', 'payload', 'response'];
|
|
17
|
+
for (const key of nestedKeys) {
|
|
18
|
+
const nested = record[key];
|
|
19
|
+
if (!nested || typeof nested !== 'object' || Array.isArray(nested)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const found = findSseWrapperError(nested, depth - 1);
|
|
23
|
+
if (found) {
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
4
29
|
function resolveProviderType(protocol) {
|
|
5
30
|
if (protocol === 'openai-chat')
|
|
6
31
|
return 'openai';
|
|
@@ -13,7 +38,29 @@ function resolveProviderType(protocol) {
|
|
|
13
38
|
return undefined;
|
|
14
39
|
}
|
|
15
40
|
export async function runRespInboundStage1SseDecode(options) {
|
|
41
|
+
const wrapperError = extractSseWrapperError(options.payload);
|
|
16
42
|
const stream = extractSseStream(options.payload);
|
|
43
|
+
// 某些 mock-provider / 捕获样本在 SSE 连接被异常终止时会携带 error 标记,
|
|
44
|
+
// 即使仍保留 __sse_responses 流,也应视为上游异常并终止。
|
|
45
|
+
if (wrapperError) {
|
|
46
|
+
recordStage(options.stageRecorder, 'resp_inbound_stage1_sse_decode', {
|
|
47
|
+
streamDetected: Boolean(stream),
|
|
48
|
+
decoded: false,
|
|
49
|
+
protocol: options.providerProtocol,
|
|
50
|
+
reason: 'sse_wrapper_error',
|
|
51
|
+
error: wrapperError
|
|
52
|
+
});
|
|
53
|
+
throw new ProviderProtocolError(`[resp_inbound_stage1_sse_decode] Upstream SSE terminated: ${wrapperError}`, {
|
|
54
|
+
code: 'SSE_DECODE_ERROR',
|
|
55
|
+
protocol: options.providerProtocol,
|
|
56
|
+
providerType: resolveProviderType(options.providerProtocol),
|
|
57
|
+
details: {
|
|
58
|
+
phase: 'resp_inbound_stage1_sse_decode',
|
|
59
|
+
requestId: options.adapterContext.requestId,
|
|
60
|
+
message: wrapperError
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
17
64
|
if (!stream) {
|
|
18
65
|
recordStage(options.stageRecorder, 'resp_inbound_stage1_sse_decode', {
|
|
19
66
|
streamDetected: false
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runChatResponseToolFilters } from '../../../../../shared/tool-filter-pipeline.js';
|
|
2
|
+
import { normalizeApplyPatchToolCallsOnResponse } from '../../../../../shared/tool-governor.js';
|
|
2
3
|
import { ToolGovernanceEngine } from '../../../../tool-governance/index.js';
|
|
3
4
|
import { recordStage } from '../../../stages/utils.js';
|
|
4
5
|
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
@@ -8,11 +9,12 @@ export async function runRespProcessStage1ToolGovernance(options) {
|
|
|
8
9
|
requestId: options.requestId,
|
|
9
10
|
profile: 'openai-chat'
|
|
10
11
|
});
|
|
11
|
-
const
|
|
12
|
+
const patched = normalizeApplyPatchToolCallsOnResponse(filtered);
|
|
13
|
+
const { payload: governed, summary } = toolGovernanceEngine.governResponse(patched, options.clientProtocol);
|
|
12
14
|
recordStage(options.stageRecorder, 'resp_process_stage1_tool_governance', {
|
|
13
15
|
summary,
|
|
14
16
|
applied: summary?.applied,
|
|
15
|
-
filteredPayload:
|
|
17
|
+
filteredPayload: patched,
|
|
16
18
|
governedPayload: governed
|
|
17
19
|
});
|
|
18
20
|
return { governedPayload: governed };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js';
|
|
2
2
|
import { ToolGovernanceEngine } from '../tool-governance/index.js';
|
|
3
3
|
import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
|
|
4
|
+
import { normalizeApplyPatchToolCallsOnRequest } from '../../shared/tool-governor.js';
|
|
4
5
|
import { isJsonObject } from '../types/json.js';
|
|
5
6
|
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
6
7
|
export async function runHubChatProcess(options) {
|
|
@@ -79,6 +80,7 @@ async function applyRequestToolGovernance(request, context) {
|
|
|
79
80
|
...merged,
|
|
80
81
|
messages: stripHistoricalImageAttachments(merged.messages)
|
|
81
82
|
};
|
|
83
|
+
merged = normalizeApplyPatchToolCallsOnRequest(merged);
|
|
82
84
|
if (containsImageAttachment(merged.messages)) {
|
|
83
85
|
if (!merged.metadata) {
|
|
84
86
|
merged.metadata = {
|
|
@@ -85,7 +85,12 @@ function resolveChatReasoningMode(_entryEndpoint) {
|
|
|
85
85
|
export async function convertProviderResponse(options) {
|
|
86
86
|
const clientProtocol = resolveClientProtocol(options.entryEndpoint);
|
|
87
87
|
const hasServerToolSupport = Boolean(options.providerInvoker) || Boolean(options.reenterPipeline);
|
|
88
|
-
|
|
88
|
+
// 是否跳过 ServerTool 编排:
|
|
89
|
+
// - 仅在当前 Provider 完全不支持 ServerTool(没有 invoker/reenterPipeline)时跳过;
|
|
90
|
+
// - 对于 serverToolFollowup=true 的二/三跳请求,也允许再次进入 ServerTool 流程,
|
|
91
|
+
// 由具体 handler(例如 gemini_empty_reply_continue / iflow_model_error_retry 等)
|
|
92
|
+
// 自行通过 serverToolFollowup 标记决定是否生效。
|
|
93
|
+
const skipServerTools = !hasServerToolSupport;
|
|
89
94
|
// 对于由 server-side 工具触发的内部跳转(二跳/三跳),统一禁用 SSE 聚合输出,
|
|
90
95
|
// 始终返回完整的 ChatCompletion JSON,便于在 llms 内部直接解析,而不是拿到
|
|
91
96
|
// __sse_responses 可读流。
|
|
@@ -5,9 +5,16 @@ export class SnapshotStageRecorder {
|
|
|
5
5
|
writer;
|
|
6
6
|
constructor(options) {
|
|
7
7
|
this.options = options;
|
|
8
|
+
const contextAny = options.context;
|
|
8
9
|
this.writer = createSnapshotWriter({
|
|
9
10
|
requestId: options.context.requestId,
|
|
10
|
-
endpoint: options.endpoint
|
|
11
|
+
endpoint: options.endpoint,
|
|
12
|
+
providerKey: typeof options.context.providerId === 'string' ? options.context.providerId : undefined,
|
|
13
|
+
groupRequestId: typeof contextAny.clientRequestId === 'string'
|
|
14
|
+
? contextAny.clientRequestId
|
|
15
|
+
: typeof contextAny.groupRequestId === 'string'
|
|
16
|
+
? contextAny.groupRequestId
|
|
17
|
+
: undefined
|
|
11
18
|
});
|
|
12
19
|
}
|
|
13
20
|
record(stage, payload) {
|
|
@@ -43,13 +43,6 @@ export async function canonicalizeOpenAIChatResponse(payload, context, options)
|
|
|
43
43
|
};
|
|
44
44
|
const engine = new FilterEngine();
|
|
45
45
|
engine.registerFilter(new ResponseToolTextCanonicalizeFilter());
|
|
46
|
-
try {
|
|
47
|
-
const { ResponseToolArgumentsToonDecodeFilter } = await import('../../../../../filters/index.js');
|
|
48
|
-
engine.registerFilter(new ResponseToolArgumentsToonDecodeFilter());
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
// optional decode filter
|
|
52
|
-
}
|
|
53
46
|
engine.registerFilter(new ResponseToolArgumentsStringifyFilter());
|
|
54
47
|
engine.registerFilter(new ResponseFinishInvariantsFilter());
|
|
55
48
|
const stage1 = await engine.run('response_pre', dto.data, filterContext);
|