@jsonstudio/llms 0.6.633 → 0.6.743
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/engine-health.js +0 -4
- package/dist/router/virtual-router/engine-selection.d.ts +2 -1
- package/dist/router/virtual-router/engine-selection.js +101 -9
- package/dist/router/virtual-router/engine.d.ts +5 -1
- package/dist/router/virtual-router/engine.js +188 -5
- 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 +22 -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
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { runServerSideToolEngine } from './server-side-tools.js';
|
|
2
|
+
import { ProviderProtocolError } from '../conversion/shared/errors.js';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateSync } from '../router/virtual-router/sticky-session-store.js';
|
|
5
|
+
import { deserializeRoutingInstructionState, serializeRoutingInstructionState } from '../router/virtual-router/routing-instructions.js';
|
|
2
6
|
export async function runServerToolOrchestration(options) {
|
|
3
7
|
const engineOptions = {
|
|
4
8
|
chatResponse: options.chat,
|
|
@@ -23,31 +27,214 @@ export async function runServerToolOrchestration(options) {
|
|
|
23
27
|
flowId: engineResult.execution.flowId
|
|
24
28
|
};
|
|
25
29
|
}
|
|
30
|
+
const isStopMessageFlow = engineResult.execution.flowId === 'stop_message_flow';
|
|
31
|
+
const stopMessageSource = isStopMessageFlow ? getStopMessageSource(options.adapterContext) : undefined;
|
|
32
|
+
const isAutoStopMessage = isStopMessageFlow && stopMessageSource !== 'explicit';
|
|
33
|
+
const isErrorAutoFlow = engineResult.execution.flowId === 'iflow_model_error_retry';
|
|
34
|
+
const applyAutoLimit = isAutoStopMessage || isErrorAutoFlow;
|
|
26
35
|
const routeHint = resolveRouteHint(options.adapterContext, engineResult.execution.flowId);
|
|
36
|
+
const loopState = buildServerToolLoopState(options.adapterContext, engineResult.execution.flowId, engineResult.execution.followup.payload);
|
|
37
|
+
if (applyAutoLimit && loopState && typeof loopState.repeatCount === 'number' && loopState.repeatCount >= 3) {
|
|
38
|
+
return {
|
|
39
|
+
chat: engineResult.finalChatResponse,
|
|
40
|
+
executed: true,
|
|
41
|
+
flowId: engineResult.execution.flowId
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (isAdapterClientDisconnected(options.adapterContext)) {
|
|
45
|
+
return {
|
|
46
|
+
chat: engineResult.finalChatResponse,
|
|
47
|
+
executed: true,
|
|
48
|
+
flowId: engineResult.execution.flowId
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const followupEntryEndpoint = engineResult.execution.followup.entryEndpoint ||
|
|
52
|
+
options.entryEndpoint ||
|
|
53
|
+
'/v1/chat/completions';
|
|
27
54
|
const metadata = {
|
|
28
55
|
serverToolFollowup: true,
|
|
29
56
|
stream: false,
|
|
57
|
+
...(loopState ? { serverToolLoopState: loopState } : {}),
|
|
30
58
|
...(engineResult.execution.followup.metadata ?? {})
|
|
31
59
|
};
|
|
32
60
|
if (routeHint && typeof metadata.routeHint !== 'string') {
|
|
33
61
|
metadata.routeHint = routeHint;
|
|
34
62
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
const maxAttempts = isStopMessageFlow ? 2 : 1;
|
|
64
|
+
const followupRequestId = buildFollowupRequestId(options.requestId, engineResult.execution.followup.requestIdSuffix);
|
|
65
|
+
let followup;
|
|
66
|
+
let lastError;
|
|
67
|
+
let reservation = null;
|
|
68
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
69
|
+
try {
|
|
70
|
+
if (isStopMessageFlow) {
|
|
71
|
+
reservation = reserveStopMessageUsage(options.adapterContext);
|
|
72
|
+
}
|
|
73
|
+
followup = await options.reenterPipeline({
|
|
74
|
+
entryEndpoint: followupEntryEndpoint,
|
|
75
|
+
requestId: followupRequestId,
|
|
76
|
+
body: engineResult.execution.followup.payload,
|
|
77
|
+
metadata
|
|
78
|
+
});
|
|
79
|
+
lastError = undefined;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (reservation) {
|
|
84
|
+
rollbackStopMessageUsage(reservation);
|
|
85
|
+
reservation = null;
|
|
86
|
+
}
|
|
87
|
+
lastError = error;
|
|
88
|
+
if (attempt >= maxAttempts) {
|
|
89
|
+
const wrapped = new ProviderProtocolError(`[servertool] Followup failed for flow ${engineResult.execution.flowId ?? 'unknown'} ` +
|
|
90
|
+
`(attempt ${attempt}/${maxAttempts})`, {
|
|
91
|
+
code: 'SERVERTOOL_FOLLOWUP_FAILED',
|
|
92
|
+
details: {
|
|
93
|
+
flowId: engineResult.execution.flowId,
|
|
94
|
+
requestId: options.requestId,
|
|
95
|
+
attempt,
|
|
96
|
+
maxAttempts,
|
|
97
|
+
error: error instanceof Error ? error.message : String(error ?? 'unknown')
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
wrapped.cause = error;
|
|
101
|
+
throw wrapped;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const followupBody = followup && followup.body && typeof followup.body === 'object'
|
|
42
106
|
? followup.body
|
|
43
|
-
:
|
|
44
|
-
|
|
107
|
+
: undefined;
|
|
108
|
+
if (isStopMessageFlow && !followupBody) {
|
|
109
|
+
const wrapped = new ProviderProtocolError(`[servertool] Followup returned empty response for flow ${engineResult.execution.flowId ?? 'unknown'}`, {
|
|
110
|
+
code: 'SERVERTOOL_FOLLOWUP_FAILED',
|
|
111
|
+
details: {
|
|
112
|
+
flowId: engineResult.execution.flowId,
|
|
113
|
+
requestId: options.requestId,
|
|
114
|
+
error: lastError instanceof Error ? lastError.message : undefined
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
wrapped.cause = lastError;
|
|
118
|
+
throw wrapped;
|
|
119
|
+
}
|
|
120
|
+
const decorated = decorateFinalChatWithServerToolContext(followupBody ?? engineResult.finalChatResponse, engineResult.execution);
|
|
45
121
|
return {
|
|
46
122
|
chat: decorated,
|
|
47
123
|
executed: true,
|
|
48
124
|
flowId: engineResult.execution.flowId
|
|
49
125
|
};
|
|
50
126
|
}
|
|
127
|
+
function reserveStopMessageUsage(adapterContext) {
|
|
128
|
+
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const sessionId = typeof adapterContext.sessionId === 'string'
|
|
132
|
+
? adapterContext.sessionId.trim()
|
|
133
|
+
: '';
|
|
134
|
+
const conversationId = typeof adapterContext.conversationId === 'string'
|
|
135
|
+
? adapterContext.conversationId.trim()
|
|
136
|
+
: '';
|
|
137
|
+
const stickyKey = sessionId ? `session:${sessionId}` : conversationId ? `conversation:${conversationId}` : '';
|
|
138
|
+
if (!stickyKey) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
let state = loadRoutingInstructionStateSync(stickyKey);
|
|
142
|
+
if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
|
|
143
|
+
const fallback = resolveStopMessageSnapshot(adapterContext.stopMessageState);
|
|
144
|
+
if (!fallback) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
state = createStopMessageState(fallback);
|
|
148
|
+
}
|
|
149
|
+
const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
|
|
150
|
+
const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
|
|
151
|
+
? Math.max(1, Math.floor(state.stopMessageMaxRepeats))
|
|
152
|
+
: 0;
|
|
153
|
+
if (!text || maxRepeats <= 0) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const previousState = cloneRoutingInstructionState(state);
|
|
157
|
+
const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
|
|
158
|
+
? Math.max(0, Math.floor(state.stopMessageUsed))
|
|
159
|
+
: 0;
|
|
160
|
+
const nextUsed = used + 1;
|
|
161
|
+
state.stopMessageUsed = nextUsed;
|
|
162
|
+
state.stopMessageLastUsedAt = Date.now();
|
|
163
|
+
if (nextUsed >= maxRepeats) {
|
|
164
|
+
state.stopMessageText = undefined;
|
|
165
|
+
state.stopMessageMaxRepeats = undefined;
|
|
166
|
+
state.stopMessageUsed = undefined;
|
|
167
|
+
state.stopMessageUpdatedAt = undefined;
|
|
168
|
+
state.stopMessageLastUsedAt = undefined;
|
|
169
|
+
}
|
|
170
|
+
saveRoutingInstructionStateSync(stickyKey, state);
|
|
171
|
+
return { stickyKey, previousState };
|
|
172
|
+
}
|
|
173
|
+
function rollbackStopMessageUsage(reservation) {
|
|
174
|
+
saveRoutingInstructionStateSync(reservation.stickyKey, reservation.previousState);
|
|
175
|
+
}
|
|
176
|
+
function cloneRoutingInstructionState(state) {
|
|
177
|
+
if (!state) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const serialized = serializeRoutingInstructionState(state);
|
|
182
|
+
return deserializeRoutingInstructionState(serialized);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function resolveStopMessageSnapshot(raw) {
|
|
189
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const record = raw;
|
|
193
|
+
const text = typeof record.stopMessageText === 'string' ? record.stopMessageText.trim() : '';
|
|
194
|
+
const maxRepeats = typeof record.stopMessageMaxRepeats === 'number' && Number.isFinite(record.stopMessageMaxRepeats)
|
|
195
|
+
? Math.max(1, Math.floor(record.stopMessageMaxRepeats))
|
|
196
|
+
: 0;
|
|
197
|
+
if (!text || maxRepeats <= 0) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const used = typeof record.stopMessageUsed === 'number' && Number.isFinite(record.stopMessageUsed)
|
|
201
|
+
? Math.max(0, Math.floor(record.stopMessageUsed))
|
|
202
|
+
: 0;
|
|
203
|
+
const updatedAt = typeof record.stopMessageUpdatedAt === 'number' && Number.isFinite(record.stopMessageUpdatedAt)
|
|
204
|
+
? record.stopMessageUpdatedAt
|
|
205
|
+
: undefined;
|
|
206
|
+
const lastUsedAt = typeof record.stopMessageLastUsedAt === 'number' && Number.isFinite(record.stopMessageLastUsedAt)
|
|
207
|
+
? record.stopMessageLastUsedAt
|
|
208
|
+
: undefined;
|
|
209
|
+
const source = typeof record.stopMessageSource === 'string' &&
|
|
210
|
+
record.stopMessageSource.trim()
|
|
211
|
+
? record.stopMessageSource.trim()
|
|
212
|
+
: undefined;
|
|
213
|
+
return {
|
|
214
|
+
text,
|
|
215
|
+
maxRepeats,
|
|
216
|
+
used,
|
|
217
|
+
...(source ? { source } : {}),
|
|
218
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
219
|
+
...(lastUsedAt ? { lastUsedAt } : {})
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function createStopMessageState(snapshot) {
|
|
223
|
+
return {
|
|
224
|
+
forcedTarget: undefined,
|
|
225
|
+
stickyTarget: undefined,
|
|
226
|
+
allowedProviders: new Set(),
|
|
227
|
+
disabledProviders: new Set(),
|
|
228
|
+
disabledKeys: new Map(),
|
|
229
|
+
disabledModels: new Map(),
|
|
230
|
+
stopMessageSource: snapshot.source && snapshot.source.trim() ? snapshot.source.trim() : 'explicit',
|
|
231
|
+
stopMessageText: snapshot.text,
|
|
232
|
+
stopMessageMaxRepeats: snapshot.maxRepeats,
|
|
233
|
+
stopMessageUsed: snapshot.used,
|
|
234
|
+
stopMessageUpdatedAt: snapshot.updatedAt,
|
|
235
|
+
stopMessageLastUsedAt: snapshot.lastUsedAt
|
|
236
|
+
};
|
|
237
|
+
}
|
|
51
238
|
function decorateFinalChatWithServerToolContext(chat, execution) {
|
|
52
239
|
if (!execution || !execution.context) {
|
|
53
240
|
return chat;
|
|
@@ -102,3 +289,142 @@ function resolveRouteHint(adapterContext, flowId) {
|
|
|
102
289
|
}
|
|
103
290
|
return routeId;
|
|
104
291
|
}
|
|
292
|
+
function buildServerToolLoopState(adapterContext, flowId, payload) {
|
|
293
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const trackPayload = typeof flowId === 'string' && flowId.trim() && flowId !== 'stop_message_flow';
|
|
297
|
+
const payloadHash = trackPayload ? hashPayload(payload) : '__servertool_auto__';
|
|
298
|
+
if (!payloadHash) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const previous = readServerToolLoopState(adapterContext);
|
|
302
|
+
const sameFlow = previous && previous.flowId === flowId;
|
|
303
|
+
const samePayload = !trackPayload || (previous && previous.payloadHash === payloadHash);
|
|
304
|
+
const prevCount = previous && typeof previous.repeatCount === 'number' && Number.isFinite(previous.repeatCount)
|
|
305
|
+
? Math.max(0, Math.floor(previous.repeatCount))
|
|
306
|
+
: 0;
|
|
307
|
+
const repeatCount = sameFlow && samePayload ? prevCount + 1 : 1;
|
|
308
|
+
return {
|
|
309
|
+
...(flowId ? { flowId } : {}),
|
|
310
|
+
payloadHash,
|
|
311
|
+
repeatCount
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function readServerToolLoopState(adapterContext) {
|
|
315
|
+
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const raw = adapterContext.serverToolLoopState;
|
|
319
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const record = raw;
|
|
323
|
+
const flowId = typeof record.flowId === 'string' ? record.flowId.trim() : undefined;
|
|
324
|
+
const payloadHash = typeof record.payloadHash === 'string' ? record.payloadHash.trim() : undefined;
|
|
325
|
+
const repeatCount = typeof record.repeatCount === 'number' && Number.isFinite(record.repeatCount)
|
|
326
|
+
? Math.max(0, Math.floor(record.repeatCount))
|
|
327
|
+
: undefined;
|
|
328
|
+
if (!payloadHash) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
...(flowId ? { flowId } : {}),
|
|
333
|
+
payloadHash,
|
|
334
|
+
...(repeatCount !== undefined ? { repeatCount } : {})
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function hashPayload(payload) {
|
|
338
|
+
try {
|
|
339
|
+
const stable = stableStringify(payload);
|
|
340
|
+
return createHash('sha1').update(stable).digest('hex');
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function stableStringify(value) {
|
|
347
|
+
if (value === null || typeof value !== 'object') {
|
|
348
|
+
return JSON.stringify(value);
|
|
349
|
+
}
|
|
350
|
+
if (Array.isArray(value)) {
|
|
351
|
+
return `[${value.map((entry) => stableStringify(entry)).join(',')}]`;
|
|
352
|
+
}
|
|
353
|
+
const record = value;
|
|
354
|
+
const keys = Object.keys(record).sort();
|
|
355
|
+
const entries = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
|
|
356
|
+
return `{${entries.join(',')}}`;
|
|
357
|
+
}
|
|
358
|
+
function buildFollowupRequestId(baseRequestId, suffix) {
|
|
359
|
+
const requestId = typeof baseRequestId === 'string' ? baseRequestId : '';
|
|
360
|
+
const suffixText = typeof suffix === 'string' ? suffix : '';
|
|
361
|
+
if (!suffixText) {
|
|
362
|
+
return requestId;
|
|
363
|
+
}
|
|
364
|
+
if (!requestId) {
|
|
365
|
+
return suffixText;
|
|
366
|
+
}
|
|
367
|
+
const normalized = normalizeFollowupRequestId(requestId, suffixText);
|
|
368
|
+
if (!normalized) {
|
|
369
|
+
return suffixText;
|
|
370
|
+
}
|
|
371
|
+
return normalized.endsWith(suffixText) ? normalized : `${normalized}${suffixText}`;
|
|
372
|
+
}
|
|
373
|
+
function getStopMessageSource(adapterContext) {
|
|
374
|
+
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
const raw = adapterContext.stopMessageState;
|
|
378
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
const record = raw;
|
|
382
|
+
const source = typeof record.stopMessageSource === 'string' && record.stopMessageSource.trim()
|
|
383
|
+
? record.stopMessageSource.trim()
|
|
384
|
+
: '';
|
|
385
|
+
return source || undefined;
|
|
386
|
+
}
|
|
387
|
+
function isAdapterClientDisconnected(adapterContext) {
|
|
388
|
+
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
const state = adapterContext.clientConnectionState;
|
|
392
|
+
if (state && typeof state === 'object' && !Array.isArray(state)) {
|
|
393
|
+
const disconnected = state.disconnected;
|
|
394
|
+
if (disconnected === true) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
if (typeof disconnected === 'string' && disconnected.trim().toLowerCase() === 'true') {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const raw = adapterContext.clientDisconnected;
|
|
402
|
+
if (raw === true) {
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
if (typeof raw === 'string' && raw.trim().toLowerCase() === 'true') {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
function normalizeFollowupRequestId(requestId, suffixText) {
|
|
411
|
+
if (!requestId) {
|
|
412
|
+
return '';
|
|
413
|
+
}
|
|
414
|
+
const token = suffixText.startsWith(':') ? suffixText.slice(1) : suffixText;
|
|
415
|
+
if (!token) {
|
|
416
|
+
return requestId;
|
|
417
|
+
}
|
|
418
|
+
const delimiterIndex = requestId.indexOf(':');
|
|
419
|
+
if (delimiterIndex === -1) {
|
|
420
|
+
return requestId;
|
|
421
|
+
}
|
|
422
|
+
const base = requestId.slice(0, delimiterIndex);
|
|
423
|
+
const rawSuffix = requestId.slice(delimiterIndex + 1);
|
|
424
|
+
if (!rawSuffix) {
|
|
425
|
+
return requestId;
|
|
426
|
+
}
|
|
427
|
+
const tokens = rawSuffix.split(':').filter((entry) => entry.length > 0 && entry !== token);
|
|
428
|
+
const rebuilt = tokens.length > 0 ? `${base}:${tokens.join(':')}` : base;
|
|
429
|
+
return rebuilt;
|
|
430
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isCompactionRequest } from '../../conversion/shared/compaction-detect.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isCompactionRequest } from '../../conversion/shared/compaction-detect.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerServerToolHandler } from '../registry.js';
|
|
2
2
|
import { cloneJson } from '../server-side-tools.js';
|
|
3
|
+
import { isCompactionRequest } from './compaction-detect.js';
|
|
3
4
|
const FLOW_ID = 'gemini_empty_reply_continue';
|
|
4
5
|
const handler = async (ctx) => {
|
|
5
6
|
if (!ctx.options.reenterPipeline) {
|
|
@@ -11,6 +12,9 @@ const handler = async (ctx) => {
|
|
|
11
12
|
if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
|
|
12
13
|
return null;
|
|
13
14
|
}
|
|
15
|
+
if (hasCompactionFlag(adapterRecord)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
14
18
|
// 仅针对 gemini-chat 协议 + antigravity.* providerKey 的 /v1/responses 路径启用。
|
|
15
19
|
if (ctx.options.providerProtocol !== 'gemini-chat') {
|
|
16
20
|
return null;
|
|
@@ -22,7 +26,9 @@ const handler = async (ctx) => {
|
|
|
22
26
|
const providerKey = typeof adapterRecord.providerKey === 'string' && adapterRecord.providerKey.trim()
|
|
23
27
|
? adapterRecord.providerKey.trim().toLowerCase()
|
|
24
28
|
: '';
|
|
25
|
-
|
|
29
|
+
const isAntigravity = providerKey.startsWith('antigravity.');
|
|
30
|
+
const isGeminiCli = providerKey.startsWith('gemini-cli.');
|
|
31
|
+
if (!isAntigravity && !isGeminiCli) {
|
|
26
32
|
return null;
|
|
27
33
|
}
|
|
28
34
|
// 仅在 finish_reason=stop 且第一条消息内容为空、无 tool_calls 时触发。
|
|
@@ -36,10 +42,13 @@ const handler = async (ctx) => {
|
|
|
36
42
|
return null;
|
|
37
43
|
}
|
|
38
44
|
const first = firstRaw;
|
|
39
|
-
const
|
|
45
|
+
const finishReasonRaw = typeof first.finish_reason === 'string' && first.finish_reason.trim()
|
|
40
46
|
? first.finish_reason.trim()
|
|
41
47
|
: '';
|
|
42
|
-
|
|
48
|
+
const finishReason = finishReasonRaw.toLowerCase();
|
|
49
|
+
const isStop = finishReason === 'stop';
|
|
50
|
+
const isMaxTokens = finishReason === 'length'; // 映射自 Gemini 的 MAX_TOKENS
|
|
51
|
+
if (!isStop && !isMaxTokens) {
|
|
43
52
|
return null;
|
|
44
53
|
}
|
|
45
54
|
const message = first.message && typeof first.message === 'object' && !Array.isArray(first.message)
|
|
@@ -50,7 +59,9 @@ const handler = async (ctx) => {
|
|
|
50
59
|
}
|
|
51
60
|
const contentRaw = message.content;
|
|
52
61
|
const contentText = typeof contentRaw === 'string' ? contentRaw.trim() : '';
|
|
53
|
-
|
|
62
|
+
// 对于 finish_reason=stop,仅在真正“空回复”时触发;
|
|
63
|
+
// 对于 finish_reason=length(MAX_TOKENS 截断),允许已有内容,视为需要自动续写。
|
|
64
|
+
if (isStop && contentText.length > 0) {
|
|
54
65
|
return null;
|
|
55
66
|
}
|
|
56
67
|
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
@@ -67,6 +78,9 @@ const handler = async (ctx) => {
|
|
|
67
78
|
if (!captured) {
|
|
68
79
|
return null;
|
|
69
80
|
}
|
|
81
|
+
if (isCompactionRequest(captured)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
70
84
|
// 超过最多 3 次空回复:返回一个 HTTP_HANDLER_ERROR 形状的错误,交由上层错误中心处理。
|
|
71
85
|
if (nextCount > 3) {
|
|
72
86
|
const errorChat = {
|
|
@@ -129,7 +143,7 @@ function buildContinueFollowupPayload(source) {
|
|
|
129
143
|
const messages = Array.isArray(rawMessages) ? cloneJson(rawMessages) : [];
|
|
130
144
|
messages.push({
|
|
131
145
|
role: 'user',
|
|
132
|
-
content: '
|
|
146
|
+
content: '继续执行'
|
|
133
147
|
});
|
|
134
148
|
payload.messages = messages;
|
|
135
149
|
if (Array.isArray(source.tools) && source.tools.length) {
|
|
@@ -142,3 +156,13 @@ function buildContinueFollowupPayload(source) {
|
|
|
142
156
|
}
|
|
143
157
|
return payload;
|
|
144
158
|
}
|
|
159
|
+
function hasCompactionFlag(record) {
|
|
160
|
+
const flag = record.compactionRequest;
|
|
161
|
+
if (flag === true) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (typeof flag === 'string' && flag.trim().toLowerCase() === 'true') {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerServerToolHandler } from '../registry.js';
|
|
2
2
|
import { cloneJson } from '../server-side-tools.js';
|
|
3
|
+
import { isCompactionRequest } from './compaction-detect.js';
|
|
3
4
|
const FLOW_ID = 'iflow_model_error_retry';
|
|
4
5
|
const handler = async (ctx) => {
|
|
5
6
|
if (!ctx.options.reenterPipeline) {
|
|
@@ -11,6 +12,9 @@ const handler = async (ctx) => {
|
|
|
11
12
|
if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
|
|
12
13
|
return null;
|
|
13
14
|
}
|
|
15
|
+
if (hasCompactionFlag(adapterRecord)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
14
18
|
// 仅针对 openai-chat 协议 + iflow.* providerKey 的 /v1/responses 路径启用。
|
|
15
19
|
if (ctx.options.providerProtocol !== 'openai-chat') {
|
|
16
20
|
return null;
|
|
@@ -39,6 +43,9 @@ const handler = async (ctx) => {
|
|
|
39
43
|
if (!captured) {
|
|
40
44
|
return null;
|
|
41
45
|
}
|
|
46
|
+
if (isCompactionRequest(captured)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
42
49
|
const followupPayload = buildRetryFollowupPayload(captured);
|
|
43
50
|
if (!followupPayload) {
|
|
44
51
|
return null;
|
|
@@ -91,3 +98,13 @@ function buildRetryFollowupPayload(source) {
|
|
|
91
98
|
}
|
|
92
99
|
return payload;
|
|
93
100
|
}
|
|
101
|
+
function hasCompactionFlag(record) {
|
|
102
|
+
const flag = record.compactionRequest;
|
|
103
|
+
if (flag === true) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (typeof flag === 'string' && flag.trim().toLowerCase() === 'true') {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|