@jsonstudio/llms 0.6.631 → 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.
Files changed (64) hide show
  1. package/dist/conversion/codecs/anthropic-openai-codec.js +0 -5
  2. package/dist/conversion/codecs/openai-openai-codec.js +0 -6
  3. package/dist/conversion/codecs/responses-openai-codec.js +1 -7
  4. package/dist/conversion/hub/node-support.js +5 -4
  5. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +14 -1
  6. package/dist/conversion/hub/pipeline/hub-pipeline.js +82 -18
  7. package/dist/conversion/hub/pipeline/session-identifiers.js +132 -2
  8. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +130 -15
  9. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +47 -0
  10. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +4 -2
  11. package/dist/conversion/hub/process/chat-process.js +2 -0
  12. package/dist/conversion/hub/response/provider-response.js +6 -1
  13. package/dist/conversion/hub/snapshot-recorder.js +8 -1
  14. package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +0 -7
  15. package/dist/conversion/responses/responses-openai-bridge.js +47 -7
  16. package/dist/conversion/shared/compaction-detect.d.ts +2 -0
  17. package/dist/conversion/shared/compaction-detect.js +53 -0
  18. package/dist/conversion/shared/errors.d.ts +1 -1
  19. package/dist/conversion/shared/reasoning-tool-normalizer.js +7 -0
  20. package/dist/conversion/shared/snapshot-hooks.d.ts +2 -0
  21. package/dist/conversion/shared/snapshot-hooks.js +180 -4
  22. package/dist/conversion/shared/snapshot-utils.d.ts +4 -0
  23. package/dist/conversion/shared/snapshot-utils.js +4 -0
  24. package/dist/conversion/shared/tool-filter-pipeline.js +3 -9
  25. package/dist/conversion/shared/tool-governor.d.ts +2 -0
  26. package/dist/conversion/shared/tool-governor.js +101 -13
  27. package/dist/conversion/shared/tool-harvester.js +42 -2
  28. package/dist/conversion/shared/tooling.d.ts +33 -0
  29. package/dist/conversion/shared/tooling.js +27 -0
  30. package/dist/filters/index.d.ts +0 -2
  31. package/dist/filters/index.js +0 -2
  32. package/dist/filters/special/request-tools-normalize.d.ts +11 -0
  33. package/dist/filters/special/request-tools-normalize.js +13 -50
  34. package/dist/filters/special/response-apply-patch-toon-decode.js +410 -67
  35. package/dist/filters/special/response-tool-arguments-stringify.js +25 -16
  36. package/dist/filters/special/response-tool-arguments-toon-decode.js +8 -76
  37. package/dist/filters/utils/snapshot-writer.js +42 -4
  38. package/dist/guidance/index.js +8 -2
  39. package/dist/router/virtual-router/engine-health.js +0 -4
  40. package/dist/router/virtual-router/engine-selection.d.ts +2 -1
  41. package/dist/router/virtual-router/engine-selection.js +101 -9
  42. package/dist/router/virtual-router/engine.d.ts +5 -1
  43. package/dist/router/virtual-router/engine.js +188 -5
  44. package/dist/router/virtual-router/routing-instructions.d.ts +6 -0
  45. package/dist/router/virtual-router/routing-instructions.js +18 -3
  46. package/dist/router/virtual-router/sticky-session-store.d.ts +1 -0
  47. package/dist/router/virtual-router/sticky-session-store.js +36 -0
  48. package/dist/router/virtual-router/types.d.ts +22 -0
  49. package/dist/servertool/engine.js +335 -9
  50. package/dist/servertool/handlers/compaction-detect.d.ts +1 -0
  51. package/dist/servertool/handlers/compaction-detect.js +1 -0
  52. package/dist/servertool/handlers/gemini-empty-reply-continue.js +29 -5
  53. package/dist/servertool/handlers/iflow-model-error-retry.js +17 -0
  54. package/dist/servertool/handlers/stop-message-auto.js +199 -19
  55. package/dist/servertool/server-side-tools.d.ts +0 -1
  56. package/dist/servertool/server-side-tools.js +0 -1
  57. package/dist/servertool/types.d.ts +1 -0
  58. package/dist/tools/apply-patch-structured.js +52 -15
  59. package/dist/tools/tool-registry.js +537 -15
  60. package/dist/utils/toon.d.ts +4 -0
  61. package/dist/utils/toon.js +75 -0
  62. package/package.json +4 -2
  63. package/dist/test-output/virtual-router/results.json +0 -1
  64. package/dist/test-output/virtual-router/summary.json +0 -12
@@ -108,3 +108,39 @@ export function saveRoutingInstructionStateAsync(key, state) {
108
108
  // ignore sync write failures
109
109
  }
110
110
  }
111
+ export function saveRoutingInstructionStateSync(key, state) {
112
+ if (!isPersistentKey(key)) {
113
+ return;
114
+ }
115
+ const dir = resolveSessionDir();
116
+ const filename = keyToFilename(key);
117
+ if (!dir || !filename) {
118
+ return;
119
+ }
120
+ const filepath = path.join(dir, filename);
121
+ if (!state) {
122
+ try {
123
+ fs.unlinkSync(filepath);
124
+ }
125
+ catch {
126
+ // ignore unlink failures
127
+ }
128
+ return;
129
+ }
130
+ const payload = {
131
+ version: 1,
132
+ state: serializeRoutingInstructionState(state)
133
+ };
134
+ try {
135
+ fs.mkdirSync(dir, { recursive: true });
136
+ }
137
+ catch {
138
+ // ignore mkdir errors
139
+ }
140
+ try {
141
+ fs.writeFileSync(filepath, JSON.stringify(payload), { encoding: 'utf8' });
142
+ }
143
+ catch {
144
+ // ignore sync write failures
145
+ }
146
+ }
@@ -297,6 +297,19 @@ export interface RoutingDiagnostics {
297
297
  poolId?: string;
298
298
  confidence: number;
299
299
  }
300
+ export interface StopMessageStateSnapshot {
301
+ stopMessageText: string;
302
+ stopMessageMaxRepeats: number;
303
+ /**
304
+ * stopMessage 来源:
305
+ * - 'explicit':来自用户显式指令
306
+ * - 'auto':系统基于空响应/错误自动推导
307
+ */
308
+ stopMessageSource?: string;
309
+ stopMessageUsed?: number;
310
+ stopMessageUpdatedAt?: number;
311
+ stopMessageLastUsedAt?: number;
312
+ }
300
313
  export interface RoutingStatusSnapshot {
301
314
  routes: Record<string, {
302
315
  providers: string[];
@@ -373,3 +386,12 @@ export interface VirtualRouterHealthStore {
373
386
  */
374
387
  recordProviderError?(event: ProviderErrorEvent): void;
375
388
  }
389
+ export interface ProviderQuotaViewEntry {
390
+ providerKey: string;
391
+ inPool: boolean;
392
+ reason?: string;
393
+ priorityTier?: number;
394
+ cooldownUntil?: number | null;
395
+ blacklistUntil?: number | null;
396
+ }
397
+ export type ProviderQuotaView = (providerKey: string) => ProviderQuotaViewEntry | null;
@@ -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 followup = await options.reenterPipeline({
36
- entryEndpoint: '/v1/chat/completions',
37
- requestId: `${options.requestId}${engineResult.execution.followup.requestIdSuffix}`,
38
- body: engineResult.execution.followup.payload,
39
- metadata
40
- });
41
- const followupBody = followup.body && typeof followup.body === 'object'
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
- : engineResult.finalChatResponse;
44
- const decorated = decorateFinalChatWithServerToolContext(followupBody, engineResult.execution);
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
- if (!providerKey.startsWith('antigravity.')) {
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 finishReason = typeof first.finish_reason === 'string' && first.finish_reason.trim()
45
+ const finishReasonRaw = typeof first.finish_reason === 'string' && first.finish_reason.trim()
40
46
  ? first.finish_reason.trim()
41
47
  : '';
42
- if (finishReason !== 'stop') {
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
- if (contentText.length > 0) {
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
+ }