@jsonstudio/llms 0.6.473 → 0.6.568

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 (82) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
  2. package/dist/conversion/codecs/openai-openai-codec.js +2 -1
  3. package/dist/conversion/codecs/responses-openai-codec.js +3 -2
  4. package/dist/conversion/compat/actions/claude-thinking-tools.d.ts +15 -0
  5. package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
  6. package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
  7. package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
  8. package/dist/conversion/compat/profiles/chat-gemini.json +15 -14
  9. package/dist/conversion/compat/profiles/chat-glm.json +194 -194
  10. package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
  11. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  12. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  13. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  14. package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
  15. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  16. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -1
  18. package/dist/conversion/hub/pipeline/hub-pipeline.js +40 -13
  19. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
  20. package/dist/conversion/hub/process/chat-process.js +107 -26
  21. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
  22. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +28 -10
  23. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
  24. package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
  25. package/dist/conversion/hub/tool-session-compat.js +299 -0
  26. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  27. package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
  28. package/dist/conversion/responses/responses-openai-bridge.js +0 -71
  29. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  30. package/dist/conversion/shared/args-mapping.js +11 -3
  31. package/dist/conversion/shared/gemini-tool-utils.js +8 -0
  32. package/dist/conversion/shared/responses-output-builder.js +47 -88
  33. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  34. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  35. package/dist/conversion/shared/text-markup-normalizer.js +42 -27
  36. package/dist/conversion/shared/tool-filter-pipeline.js +2 -1
  37. package/dist/conversion/shared/tool-governor.js +75 -4
  38. package/dist/conversion/shared/tool-harvester.js +43 -12
  39. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  40. package/dist/conversion/shared/tool-mapping.js +33 -13
  41. package/dist/filters/index.d.ts +1 -0
  42. package/dist/filters/index.js +1 -0
  43. package/dist/filters/special/request-toolcalls-stringify.js +5 -55
  44. package/dist/filters/special/request-tools-normalize.js +14 -23
  45. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  46. package/dist/filters/special/response-apply-patch-toon-decode.js +109 -0
  47. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  48. package/dist/filters/special/response-tool-arguments-toon-decode.js +55 -13
  49. package/dist/guidance/index.js +70 -27
  50. package/dist/router/virtual-router/bootstrap.js +10 -5
  51. package/dist/router/virtual-router/classifier.js +9 -4
  52. package/dist/router/virtual-router/engine-health.d.ts +22 -0
  53. package/dist/router/virtual-router/engine-health.js +423 -0
  54. package/dist/router/virtual-router/engine-logging.d.ts +20 -0
  55. package/dist/router/virtual-router/engine-logging.js +197 -0
  56. package/dist/router/virtual-router/engine-selection.d.ts +32 -0
  57. package/dist/router/virtual-router/engine-selection.js +649 -0
  58. package/dist/router/virtual-router/engine.d.ts +21 -14
  59. package/dist/router/virtual-router/engine.js +200 -523
  60. package/dist/router/virtual-router/message-utils.js +22 -0
  61. package/dist/router/virtual-router/routing-instructions.d.ts +8 -1
  62. package/dist/router/virtual-router/routing-instructions.js +137 -3
  63. package/dist/router/virtual-router/tool-signals.js +57 -11
  64. package/dist/router/virtual-router/types.d.ts +30 -0
  65. package/dist/router/virtual-router/types.js +1 -1
  66. package/dist/servertool/engine.js +3 -0
  67. package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
  68. package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -0
  69. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  70. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  71. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -0
  72. package/dist/servertool/handlers/stop-message-auto.js +204 -0
  73. package/dist/servertool/handlers/vision.js +105 -7
  74. package/dist/servertool/server-side-tools.d.ts +3 -0
  75. package/dist/servertool/server-side-tools.js +29 -0
  76. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +16 -0
  77. package/dist/tools/apply-patch-structured.d.ts +20 -0
  78. package/dist/tools/apply-patch-structured.js +239 -0
  79. package/dist/tools/tool-description-utils.d.ts +5 -0
  80. package/dist/tools/tool-description-utils.js +50 -0
  81. package/dist/tools/tool-registry.js +14 -5
  82. package/package.json +2 -2
@@ -20,6 +20,28 @@ export function extractMessageText(message) {
20
20
  if (typeof message.content === 'string' && message.content.trim()) {
21
21
  return message.content;
22
22
  }
23
+ const content = message.content;
24
+ if (Array.isArray(content)) {
25
+ const parts = [];
26
+ for (const entry of content) {
27
+ if (typeof entry === 'string' && entry.trim()) {
28
+ parts.push(entry);
29
+ }
30
+ else if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
31
+ const record = entry;
32
+ if (typeof record.text === 'string' && record.text.trim()) {
33
+ parts.push(record.text);
34
+ }
35
+ else if (typeof record.content === 'string' && record.content.trim()) {
36
+ parts.push(record.content);
37
+ }
38
+ }
39
+ }
40
+ const joined = parts.join('\n').trim();
41
+ if (joined) {
42
+ return joined;
43
+ }
44
+ }
23
45
  return '';
24
46
  }
25
47
  export function detectKeyword(text, keywords) {
@@ -1,11 +1,13 @@
1
1
  import type { StandardizedMessage } from '../../conversion/hub/types/standardized.js';
2
2
  export interface RoutingInstruction {
3
- type: 'force' | 'sticky' | 'disable' | 'enable' | 'clear' | 'allow';
3
+ type: 'force' | 'sticky' | 'disable' | 'enable' | 'clear' | 'allow' | 'stopMessageSet' | 'stopMessageClear';
4
4
  provider?: string;
5
5
  keyAlias?: string;
6
6
  keyIndex?: number;
7
7
  model?: string;
8
8
  pathLength?: number;
9
+ stopMessageText?: string;
10
+ stopMessageMaxRepeats?: number;
9
11
  }
10
12
  export interface RoutingInstructionState {
11
13
  forcedTarget?: {
@@ -26,6 +28,11 @@ export interface RoutingInstructionState {
26
28
  disabledProviders: Set<string>;
27
29
  disabledKeys: Map<string, Set<string | number>>;
28
30
  disabledModels: Map<string, Set<string>>;
31
+ stopMessageText?: string;
32
+ stopMessageMaxRepeats?: number;
33
+ stopMessageUsed?: number;
34
+ stopMessageUpdatedAt?: number;
35
+ stopMessageLastUsedAt?: number;
29
36
  }
30
37
  export declare function parseRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
31
38
  export declare function applyRoutingInstructions(instructions: RoutingInstruction[], currentState: RoutingInstructionState): RoutingInstructionState;
@@ -49,6 +49,11 @@ function expandInstructionSegments(instruction) {
49
49
  if (!trimmed) {
50
50
  return [];
51
51
  }
52
+ // stopMessage 指令需要整体解析,不能按逗号拆分,否则类似
53
+ // "<**stopMessage:\"继续\",3**>" 会被错误拆成 ["stopMessage:\"继续\"", "3"]。
54
+ if (/^stopMessage\s*:/i.test(trimmed)) {
55
+ return [trimmed];
56
+ }
52
57
  const prefix = trimmed[0];
53
58
  if (prefix === '!' || prefix === '#' || prefix === '@') {
54
59
  const tokens = splitInstructionTargets(trimmed.substring(1));
@@ -69,6 +74,74 @@ function parseSingleInstruction(instruction) {
69
74
  if (instruction === 'clear') {
70
75
  return { type: 'clear' };
71
76
  }
77
+ if (/^stopMessage\s*:/i.test(instruction)) {
78
+ const body = instruction.slice('stopMessage'.length + 1).trim();
79
+ if (!body) {
80
+ return null;
81
+ }
82
+ if (/^clear$/i.test(body)) {
83
+ return { type: 'stopMessageClear' };
84
+ }
85
+ let text = '';
86
+ let maxRepeats = 1;
87
+ let cursor = body;
88
+ if (cursor[0] === '"') {
89
+ let escaped = false;
90
+ let endIndex = -1;
91
+ for (let i = 1; i < cursor.length; i += 1) {
92
+ const ch = cursor[i];
93
+ if (escaped) {
94
+ escaped = false;
95
+ continue;
96
+ }
97
+ if (ch === '\\') {
98
+ escaped = true;
99
+ continue;
100
+ }
101
+ if (ch === '"') {
102
+ endIndex = i;
103
+ break;
104
+ }
105
+ }
106
+ if (endIndex <= 0) {
107
+ return null;
108
+ }
109
+ const rawText = cursor.slice(1, endIndex);
110
+ text = rawText.replace(/\\"/g, '"');
111
+ cursor = cursor.slice(endIndex + 1).trim();
112
+ if (cursor.startsWith(',')) {
113
+ const countRaw = cursor.slice(1).trim();
114
+ if (countRaw) {
115
+ const parsed = Number.parseInt(countRaw, 10);
116
+ if (!Number.isNaN(parsed) && parsed > 0) {
117
+ maxRepeats = parsed;
118
+ }
119
+ }
120
+ }
121
+ }
122
+ else {
123
+ // 支持无引号的简单形式:stopMessage:继续,3
124
+ const parts = cursor.split(',').map((part) => part.trim()).filter(Boolean);
125
+ if (!parts.length) {
126
+ return null;
127
+ }
128
+ text = parts[0];
129
+ if (parts.length > 1) {
130
+ const parsed = Number.parseInt(parts[1], 10);
131
+ if (!Number.isNaN(parsed) && parsed > 0) {
132
+ maxRepeats = parsed;
133
+ }
134
+ }
135
+ }
136
+ if (!text) {
137
+ return null;
138
+ }
139
+ return {
140
+ type: 'stopMessageSet',
141
+ stopMessageText: text,
142
+ stopMessageMaxRepeats: maxRepeats
143
+ };
144
+ }
72
145
  if (instruction.startsWith('!')) {
73
146
  const target = instruction.substring(1).trim();
74
147
  if (!target) {
@@ -199,7 +272,12 @@ export function applyRoutingInstructions(instructions, currentState) {
199
272
  allowedProviders: new Set(currentState.allowedProviders),
200
273
  disabledProviders: new Set(currentState.disabledProviders),
201
274
  disabledKeys: new Map(Array.from(currentState.disabledKeys.entries()).map(([k, v]) => [k, new Set(v)])),
202
- disabledModels: new Map(Array.from(currentState.disabledModels.entries()).map(([k, v]) => [k, new Set(v)]))
275
+ disabledModels: new Map(Array.from(currentState.disabledModels.entries()).map(([k, v]) => [k, new Set(v)])),
276
+ stopMessageText: currentState.stopMessageText,
277
+ stopMessageMaxRepeats: currentState.stopMessageMaxRepeats,
278
+ stopMessageUsed: currentState.stopMessageUsed,
279
+ stopMessageUpdatedAt: currentState.stopMessageUpdatedAt,
280
+ stopMessageLastUsedAt: currentState.stopMessageLastUsedAt
203
281
  };
204
282
  let allowReset = false;
205
283
  let disableReset = false;
@@ -312,6 +390,29 @@ export function applyRoutingInstructions(instructions, currentState) {
312
390
  newState.disabledKeys.clear();
313
391
  newState.disabledModels.clear();
314
392
  break;
393
+ case 'stopMessageSet': {
394
+ const text = typeof instruction.stopMessageText === 'string' && instruction.stopMessageText.trim()
395
+ ? instruction.stopMessageText.trim()
396
+ : '';
397
+ const maxRepeats = typeof instruction.stopMessageMaxRepeats === 'number' && Number.isFinite(instruction.stopMessageMaxRepeats)
398
+ ? Math.floor(instruction.stopMessageMaxRepeats)
399
+ : 0;
400
+ if (text && maxRepeats > 0) {
401
+ newState.stopMessageText = text;
402
+ newState.stopMessageMaxRepeats = maxRepeats;
403
+ newState.stopMessageUsed = 0;
404
+ newState.stopMessageUpdatedAt = Date.now();
405
+ newState.stopMessageLastUsedAt = undefined;
406
+ }
407
+ break;
408
+ }
409
+ case 'stopMessageClear':
410
+ newState.stopMessageText = undefined;
411
+ newState.stopMessageMaxRepeats = undefined;
412
+ newState.stopMessageUsed = undefined;
413
+ newState.stopMessageUpdatedAt = undefined;
414
+ newState.stopMessageLastUsedAt = undefined;
415
+ break;
315
416
  }
316
417
  }
317
418
  return newState;
@@ -351,7 +452,22 @@ export function serializeRoutingInstructionState(state) {
351
452
  disabledModels: Array.from(state.disabledModels.entries()).map(([provider, models]) => ({
352
453
  provider,
353
454
  models: Array.from(models)
354
- }))
455
+ })),
456
+ ...(typeof state.stopMessageText === 'string' && state.stopMessageText.trim()
457
+ ? { stopMessageText: state.stopMessageText }
458
+ : {}),
459
+ ...(typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
460
+ ? { stopMessageMaxRepeats: state.stopMessageMaxRepeats }
461
+ : {}),
462
+ ...(typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
463
+ ? { stopMessageUsed: state.stopMessageUsed }
464
+ : {}),
465
+ ...(typeof state.stopMessageUpdatedAt === 'number' && Number.isFinite(state.stopMessageUpdatedAt)
466
+ ? { stopMessageUpdatedAt: state.stopMessageUpdatedAt }
467
+ : {}),
468
+ ...(typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)
469
+ ? { stopMessageLastUsedAt: state.stopMessageLastUsedAt }
470
+ : {})
355
471
  };
356
472
  }
357
473
  export function deserializeRoutingInstructionState(data) {
@@ -361,7 +477,10 @@ export function deserializeRoutingInstructionState(data) {
361
477
  allowedProviders: new Set(),
362
478
  disabledProviders: new Set(),
363
479
  disabledKeys: new Map(),
364
- disabledModels: new Map()
480
+ disabledModels: new Map(),
481
+ stopMessageText: undefined,
482
+ stopMessageMaxRepeats: undefined,
483
+ stopMessageUsed: undefined
365
484
  };
366
485
  if (data.forcedTarget && typeof data.forcedTarget === 'object') {
367
486
  state.forcedTarget = data.forcedTarget;
@@ -389,5 +508,20 @@ export function deserializeRoutingInstructionState(data) {
389
508
  }
390
509
  }
391
510
  }
511
+ if (typeof data.stopMessageText === 'string' && data.stopMessageText.trim()) {
512
+ state.stopMessageText = data.stopMessageText;
513
+ }
514
+ if (typeof data.stopMessageMaxRepeats === 'number' && Number.isFinite(data.stopMessageMaxRepeats)) {
515
+ state.stopMessageMaxRepeats = Math.floor(data.stopMessageMaxRepeats);
516
+ }
517
+ if (typeof data.stopMessageUsed === 'number' && Number.isFinite(data.stopMessageUsed)) {
518
+ state.stopMessageUsed = Math.max(0, Math.floor(data.stopMessageUsed));
519
+ }
520
+ if (typeof data.stopMessageUpdatedAt === 'number' && Number.isFinite(data.stopMessageUpdatedAt)) {
521
+ state.stopMessageUpdatedAt = data.stopMessageUpdatedAt;
522
+ }
523
+ if (typeof data.stopMessageLastUsedAt === 'number' && Number.isFinite(data.stopMessageLastUsedAt)) {
524
+ state.stopMessageLastUsedAt = data.stopMessageLastUsedAt;
525
+ }
392
526
  return state;
393
527
  }
@@ -101,7 +101,25 @@ const SHELL_READ_PATTERNS = [
101
101
  'python - <<',
102
102
  'python -c',
103
103
  'node - <<',
104
- 'node -e'
104
+ 'node -e',
105
+ 'sed -n',
106
+ 'sed --quiet',
107
+ 'sed ',
108
+ 'rg ',
109
+ ' ripgrep',
110
+ 'grep ',
111
+ 'egrep ',
112
+ 'fgrep ',
113
+ 'ag ',
114
+ 'ack ',
115
+ 'find ',
116
+ 'nl ',
117
+ 'less',
118
+ 'more',
119
+ 'awk ',
120
+ 'perl -ne',
121
+ 'perl -pe',
122
+ 'strings '
105
123
  ];
106
124
  export function detectVisionTool(request) {
107
125
  if (!Array.isArray(request.tools)) {
@@ -197,21 +215,49 @@ function classifyToolCall(call) {
197
215
  }
198
216
  const argsObject = parseToolArguments(call?.function?.arguments);
199
217
  const commandText = extractCommandText(argsObject);
200
- const nameCategory = categorizeToolName(functionName);
201
218
  const snippet = buildCommandSnippet(commandText);
202
- if (nameCategory === 'write' || nameCategory === 'read' || nameCategory === 'search') {
203
- return { category: nameCategory, name: functionName, commandSnippet: snippet };
204
- }
205
- if (SHELL_TOOL_NAMES.has(functionName)) {
206
- const shellCategory = classifyShellCommand(commandText);
207
- return { category: shellCategory, name: functionName, commandSnippet: snippet };
208
- }
209
- if (commandText) {
219
+ const normalizedName = functionName.toLowerCase();
220
+ const normalizedCmd = commandText.toLowerCase();
221
+ // 1) Web search 优先:函数名或命令文本中命中 web 搜索关键字时,一律归类为 search,优先级最高。
222
+ const isWebSearch = WEB_TOOL_KEYWORDS.some((keyword) => normalizedName.includes(keyword)) ||
223
+ WEB_TOOL_KEYWORDS.some((keyword) => normalizedCmd.includes(keyword));
224
+ // 2) 基于工具名的初步分类(read / write / search / other)
225
+ const nameCategory = categorizeToolName(functionName);
226
+ // 3) shell_command / exec_command 根据内部命令判断读写性质
227
+ let shellCategory = 'other';
228
+ if (SHELL_TOOL_NAMES.has(functionName) || functionName === 'exec_command') {
229
+ shellCategory = classifyShellCommand(commandText);
230
+ }
231
+ // 按优先级合并分类结果:
232
+ // 1. web search
233
+ // 2. 写文件(任一维度命中写)
234
+ // 3. 读文件(任一维度命中读)
235
+ // 4. 其他搜索(非 web search)
236
+ // 5. 其它工具
237
+ // Priority 1: Web search
238
+ if (isWebSearch) {
239
+ return { category: 'search', name: functionName, commandSnippet: snippet };
240
+ }
241
+ // Priority 2: Write (写文件) — 名称或内部命令任一判断为写,都按写处理
242
+ if (nameCategory === 'write' || shellCategory === 'write') {
243
+ return { category: 'write', name: functionName, commandSnippet: snippet };
244
+ }
245
+ // Priority 3: Read (读文件) — 仅在没有写的情况下,再看读
246
+ if (nameCategory === 'read' || shellCategory === 'read') {
247
+ return { category: 'read', name: functionName, commandSnippet: snippet };
248
+ }
249
+ // Priority 4: 其他 search 类工具(非 web search)
250
+ if (nameCategory === 'search') {
251
+ return { category: 'search', name: functionName, commandSnippet: snippet };
252
+ }
253
+ // Priority 5: 兜底用命令文本再判断一次 shell 风格读写(非 shell/exec_command 的工具)
254
+ if (!SHELL_TOOL_NAMES.has(functionName) && functionName !== 'exec_command' && commandText) {
210
255
  const derivedCategory = classifyShellCommand(commandText);
211
- if (derivedCategory !== 'other') {
256
+ if (derivedCategory === 'write' || derivedCategory === 'read') {
212
257
  return { category: derivedCategory, name: functionName, commandSnippet: snippet };
213
258
  }
214
259
  }
260
+ // 最终兜底:other
215
261
  return { category: 'other', name: functionName, commandSnippet: snippet };
216
262
  }
217
263
  function extractToolName(tool) {
@@ -172,6 +172,11 @@ export interface RouterMetadataInput {
172
172
  * 强制路由模式,从消息中的 <**...**> 指令解析得出
173
173
  */
174
174
  routingMode?: RoutingInstructionMode;
175
+ /**
176
+ * 当 disableStickyRoutes=true 时,本次请求仍使用 sticky session 状态,
177
+ * 但不继承 sticky target,允许后续路由重新选择 provider。
178
+ */
179
+ disableStickyRoutes?: boolean;
175
180
  /**
176
181
  * 允许的 provider 白名单
177
182
  */
@@ -343,3 +348,28 @@ export interface ProviderErrorEvent {
343
348
  export interface FeatureBuilder {
344
349
  build(request: StandardizedRequest, metadata: RouterMetadataInput): RoutingFeatures;
345
350
  }
351
+ export interface ProviderCooldownState {
352
+ providerKey: string;
353
+ cooldownExpiresAt: number;
354
+ reason?: string;
355
+ }
356
+ export interface VirtualRouterHealthSnapshot {
357
+ providers: ProviderHealthState[];
358
+ cooldowns: ProviderCooldownState[];
359
+ }
360
+ export interface VirtualRouterHealthStore {
361
+ /**
362
+ * 在 VirtualRouterEngine 初始化时提供上一次持久化的健康快照。
363
+ * 调用方应仅返回仍在有效期内的 cooldown/熔断信息,或返回 null 表示无可恢复状态。
364
+ */
365
+ loadInitialSnapshot(): VirtualRouterHealthSnapshot | null;
366
+ /**
367
+ * 当 VirtualRouterEngine 更新 provider 健康状态或 cooldown 时,可选地持久化最新快照。
368
+ * 实现应保证内部吞掉 I/O 错误,不影响路由主流程。
369
+ */
370
+ persistSnapshot?(snapshot: VirtualRouterHealthSnapshot): void;
371
+ /**
372
+ * 可选:记录原始 ProviderErrorEvent,便于后续离线统计与诊断。
373
+ */
374
+ recordProviderError?(event: ProviderErrorEvent): void;
375
+ }
@@ -8,8 +8,8 @@ export const ROUTE_PRIORITY = [
8
8
  'longcontext',
9
9
  'web_search',
10
10
  'search',
11
- 'coding',
12
11
  'thinking',
12
+ 'coding',
13
13
  'tools',
14
14
  'background',
15
15
  DEFAULT_ROUTE
@@ -94,6 +94,9 @@ function resolveRouteHint(adapterContext, flowId) {
94
94
  if (!routeId) {
95
95
  return undefined;
96
96
  }
97
+ if (routeId.toLowerCase() === 'default') {
98
+ return undefined;
99
+ }
97
100
  if (flowId && routeId.toLowerCase() === flowId.toLowerCase()) {
98
101
  return undefined;
99
102
  }
@@ -0,0 +1,120 @@
1
+ import { registerServerToolHandler } from '../registry.js';
2
+ import { cloneJson } from '../server-side-tools.js';
3
+ const FLOW_ID = 'gemini_empty_reply_continue';
4
+ const handler = async (ctx) => {
5
+ if (!ctx.options.reenterPipeline) {
6
+ return null;
7
+ }
8
+ // 避免在 followup 请求里再次触发,防止循环。
9
+ const adapterRecord = ctx.adapterContext;
10
+ const followupRaw = adapterRecord.serverToolFollowup;
11
+ if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
12
+ return null;
13
+ }
14
+ // 仅针对 gemini-chat 协议 + antigravity.* providerKey 的 /v1/responses 路径启用。
15
+ if (ctx.options.providerProtocol !== 'gemini-chat') {
16
+ return null;
17
+ }
18
+ const entryEndpoint = (ctx.options.entryEndpoint || '').toLowerCase();
19
+ if (!entryEndpoint.includes('/v1/responses')) {
20
+ return null;
21
+ }
22
+ const providerKey = typeof adapterRecord.providerKey === 'string' && adapterRecord.providerKey.trim()
23
+ ? adapterRecord.providerKey.trim().toLowerCase()
24
+ : '';
25
+ if (!providerKey.startsWith('antigravity.')) {
26
+ return null;
27
+ }
28
+ // 仅在 finish_reason=stop 且第一条消息内容为空、无 tool_calls 时触发。
29
+ const base = ctx.base;
30
+ const choices = Array.isArray(base.choices) ? base.choices : [];
31
+ if (!choices.length) {
32
+ return null;
33
+ }
34
+ const firstRaw = choices[0];
35
+ if (!firstRaw || typeof firstRaw !== 'object') {
36
+ return null;
37
+ }
38
+ const first = firstRaw;
39
+ const finishReason = typeof first.finish_reason === 'string' && first.finish_reason.trim()
40
+ ? first.finish_reason.trim()
41
+ : '';
42
+ if (finishReason !== 'stop') {
43
+ return null;
44
+ }
45
+ const message = first.message && typeof first.message === 'object' && !Array.isArray(first.message)
46
+ ? first.message
47
+ : null;
48
+ if (!message) {
49
+ return null;
50
+ }
51
+ const contentRaw = message.content;
52
+ const contentText = typeof contentRaw === 'string'
53
+ ? contentRaw.trim()
54
+ : '';
55
+ if (contentText.length > 0) {
56
+ return null;
57
+ }
58
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
59
+ if (toolCalls.length > 0) {
60
+ return null;
61
+ }
62
+ const captured = getCapturedRequest(ctx.adapterContext);
63
+ if (!captured) {
64
+ return null;
65
+ }
66
+ const followupPayload = buildContinueFollowupPayload(captured);
67
+ if (!followupPayload) {
68
+ return null;
69
+ }
70
+ return {
71
+ chatResponse: ctx.base,
72
+ execution: {
73
+ flowId: FLOW_ID,
74
+ followup: {
75
+ requestIdSuffix: ':continue',
76
+ payload: followupPayload,
77
+ metadata: {
78
+ serverToolFollowup: true,
79
+ stream: false
80
+ }
81
+ }
82
+ }
83
+ };
84
+ };
85
+ registerServerToolHandler('gemini_empty_reply_continue', handler, { trigger: 'auto' });
86
+ function getCapturedRequest(adapterContext) {
87
+ if (!adapterContext || typeof adapterContext !== 'object') {
88
+ return null;
89
+ }
90
+ const captured = adapterContext.capturedChatRequest;
91
+ if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
92
+ return null;
93
+ }
94
+ return captured;
95
+ }
96
+ function buildContinueFollowupPayload(source) {
97
+ if (!source || typeof source !== 'object') {
98
+ return null;
99
+ }
100
+ const payload = {};
101
+ if (typeof source.model === 'string' && source.model.trim()) {
102
+ payload.model = source.model.trim();
103
+ }
104
+ const rawMessages = source.messages;
105
+ const messages = Array.isArray(rawMessages) ? cloneJson(rawMessages) : [];
106
+ messages.push({
107
+ role: 'user',
108
+ content: '继续'
109
+ });
110
+ payload.messages = messages;
111
+ if (Array.isArray(source.tools) && source.tools.length) {
112
+ payload.tools = cloneJson(source.tools);
113
+ }
114
+ const parameters = source.parameters;
115
+ if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
116
+ const params = cloneJson(parameters);
117
+ Object.assign(payload, params);
118
+ }
119
+ return payload;
120
+ }
@@ -0,0 +1,93 @@
1
+ import { registerServerToolHandler } from '../registry.js';
2
+ import { cloneJson } from '../server-side-tools.js';
3
+ const FLOW_ID = 'iflow_model_error_retry';
4
+ const handler = async (ctx) => {
5
+ if (!ctx.options.reenterPipeline) {
6
+ return null;
7
+ }
8
+ const adapterRecord = ctx.adapterContext;
9
+ // 避免在 followup 请求里再次触发,防止循环。
10
+ const followupRaw = adapterRecord.serverToolFollowup;
11
+ if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
12
+ return null;
13
+ }
14
+ // 仅针对 openai-chat 协议 + iflow.* providerKey 的 /v1/responses 路径启用。
15
+ if (ctx.options.providerProtocol !== 'openai-chat') {
16
+ return null;
17
+ }
18
+ const entryEndpoint = (ctx.options.entryEndpoint || '').toLowerCase();
19
+ if (!entryEndpoint.includes('/v1/responses')) {
20
+ return null;
21
+ }
22
+ const providerKey = typeof adapterRecord.providerKey === 'string' && adapterRecord.providerKey.trim()
23
+ ? adapterRecord.providerKey.trim().toLowerCase()
24
+ : '';
25
+ if (!providerKey.startsWith('iflow.')) {
26
+ return null;
27
+ }
28
+ // 仅在上游返回 error_code(HTTP 200 + 业务错误)时触发一次自动重试。
29
+ const base = ctx.base;
30
+ const errorCode = base.error_code;
31
+ const msg = base.msg;
32
+ if (typeof errorCode !== 'number' || errorCode === 0) {
33
+ return null;
34
+ }
35
+ if (typeof msg !== 'string' || !msg.trim().length) {
36
+ return null;
37
+ }
38
+ const captured = getCapturedRequest(ctx.adapterContext);
39
+ if (!captured) {
40
+ return null;
41
+ }
42
+ const followupPayload = buildRetryFollowupPayload(captured);
43
+ if (!followupPayload) {
44
+ return null;
45
+ }
46
+ return {
47
+ chatResponse: ctx.base,
48
+ execution: {
49
+ flowId: FLOW_ID,
50
+ followup: {
51
+ requestIdSuffix: ':retry',
52
+ payload: followupPayload,
53
+ metadata: {
54
+ serverToolFollowup: true
55
+ }
56
+ }
57
+ }
58
+ };
59
+ };
60
+ registerServerToolHandler('iflow_model_error_retry', handler, { trigger: 'auto' });
61
+ function getCapturedRequest(adapterContext) {
62
+ if (!adapterContext || typeof adapterContext !== 'object') {
63
+ return null;
64
+ }
65
+ const captured = adapterContext.capturedChatRequest;
66
+ if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
67
+ return null;
68
+ }
69
+ return captured;
70
+ }
71
+ function buildRetryFollowupPayload(source) {
72
+ if (!source || typeof source !== 'object') {
73
+ return null;
74
+ }
75
+ const payload = {};
76
+ if (typeof source.model === 'string' && source.model.trim()) {
77
+ payload.model = source.model.trim();
78
+ }
79
+ const rawMessages = source.messages;
80
+ if (Array.isArray(rawMessages)) {
81
+ payload.messages = cloneJson(rawMessages);
82
+ }
83
+ const rawTools = source.tools;
84
+ if (Array.isArray(rawTools) && rawTools.length) {
85
+ payload.tools = cloneJson(rawTools);
86
+ }
87
+ const parameters = source.parameters;
88
+ if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
89
+ const params = cloneJson(parameters);
90
+ Object.assign(payload, params);
91
+ }
92
+ return payload;
93
+ }
@@ -0,0 +1 @@
1
+ export {};