@jsonstudio/llms 0.6.473 → 0.6.567

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 (47) hide show
  1. package/dist/conversion/compat/actions/claude-thinking-tools.d.ts +15 -0
  2. package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
  3. package/dist/conversion/compat/profiles/chat-gemini.json +15 -14
  4. package/dist/conversion/compat/profiles/chat-glm.json +194 -194
  5. package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
  6. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  7. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  8. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  9. package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
  10. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  11. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  12. package/dist/conversion/hub/pipeline/hub-pipeline.js +15 -0
  13. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
  14. package/dist/conversion/hub/process/chat-process.js +44 -17
  15. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
  16. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +13 -8
  17. package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
  18. package/dist/conversion/hub/tool-session-compat.js +299 -0
  19. package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
  20. package/dist/conversion/responses/responses-openai-bridge.js +0 -71
  21. package/dist/conversion/shared/gemini-tool-utils.js +8 -0
  22. package/dist/conversion/shared/responses-output-builder.js +6 -68
  23. package/dist/conversion/shared/tool-governor.js +75 -4
  24. package/dist/conversion/shared/tool-mapping.js +14 -8
  25. package/dist/filters/special/request-toolcalls-stringify.js +5 -55
  26. package/dist/filters/special/request-tools-normalize.js +0 -19
  27. package/dist/guidance/index.js +25 -9
  28. package/dist/router/virtual-router/engine-health.d.ts +11 -0
  29. package/dist/router/virtual-router/engine-health.js +210 -0
  30. package/dist/router/virtual-router/engine-logging.d.ts +19 -0
  31. package/dist/router/virtual-router/engine-logging.js +165 -0
  32. package/dist/router/virtual-router/engine-selection.d.ts +32 -0
  33. package/dist/router/virtual-router/engine-selection.js +649 -0
  34. package/dist/router/virtual-router/engine.d.ts +4 -13
  35. package/dist/router/virtual-router/engine.js +64 -535
  36. package/dist/router/virtual-router/message-utils.js +22 -0
  37. package/dist/router/virtual-router/routing-instructions.d.ts +6 -1
  38. package/dist/router/virtual-router/routing-instructions.js +119 -3
  39. package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
  40. package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -0
  41. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -0
  42. package/dist/servertool/handlers/stop-message-auto.js +147 -0
  43. package/dist/servertool/handlers/vision.js +105 -7
  44. package/dist/servertool/server-side-tools.d.ts +2 -0
  45. package/dist/servertool/server-side-tools.js +2 -0
  46. package/dist/tools/tool-registry.js +195 -4
  47. package/package.json +1 -1
@@ -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,9 @@ 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;
29
34
  }
30
35
  export declare function parseRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
31
36
  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,10 @@ 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
203
279
  };
204
280
  let allowReset = false;
205
281
  let disableReset = false;
@@ -312,6 +388,25 @@ export function applyRoutingInstructions(instructions, currentState) {
312
388
  newState.disabledKeys.clear();
313
389
  newState.disabledModels.clear();
314
390
  break;
391
+ case 'stopMessageSet': {
392
+ const text = typeof instruction.stopMessageText === 'string' && instruction.stopMessageText.trim()
393
+ ? instruction.stopMessageText.trim()
394
+ : '';
395
+ const maxRepeats = typeof instruction.stopMessageMaxRepeats === 'number' && Number.isFinite(instruction.stopMessageMaxRepeats)
396
+ ? Math.floor(instruction.stopMessageMaxRepeats)
397
+ : 0;
398
+ if (text && maxRepeats > 0) {
399
+ newState.stopMessageText = text;
400
+ newState.stopMessageMaxRepeats = maxRepeats;
401
+ newState.stopMessageUsed = 0;
402
+ }
403
+ break;
404
+ }
405
+ case 'stopMessageClear':
406
+ newState.stopMessageText = undefined;
407
+ newState.stopMessageMaxRepeats = undefined;
408
+ newState.stopMessageUsed = undefined;
409
+ break;
315
410
  }
316
411
  }
317
412
  return newState;
@@ -351,7 +446,16 @@ export function serializeRoutingInstructionState(state) {
351
446
  disabledModels: Array.from(state.disabledModels.entries()).map(([provider, models]) => ({
352
447
  provider,
353
448
  models: Array.from(models)
354
- }))
449
+ })),
450
+ ...(typeof state.stopMessageText === 'string' && state.stopMessageText.trim()
451
+ ? { stopMessageText: state.stopMessageText }
452
+ : {}),
453
+ ...(typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
454
+ ? { stopMessageMaxRepeats: state.stopMessageMaxRepeats }
455
+ : {}),
456
+ ...(typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
457
+ ? { stopMessageUsed: state.stopMessageUsed }
458
+ : {})
355
459
  };
356
460
  }
357
461
  export function deserializeRoutingInstructionState(data) {
@@ -361,7 +465,10 @@ export function deserializeRoutingInstructionState(data) {
361
465
  allowedProviders: new Set(),
362
466
  disabledProviders: new Set(),
363
467
  disabledKeys: new Map(),
364
- disabledModels: new Map()
468
+ disabledModels: new Map(),
469
+ stopMessageText: undefined,
470
+ stopMessageMaxRepeats: undefined,
471
+ stopMessageUsed: undefined
365
472
  };
366
473
  if (data.forcedTarget && typeof data.forcedTarget === 'object') {
367
474
  state.forcedTarget = data.forcedTarget;
@@ -389,5 +496,14 @@ export function deserializeRoutingInstructionState(data) {
389
496
  }
390
497
  }
391
498
  }
499
+ if (typeof data.stopMessageText === 'string' && data.stopMessageText.trim()) {
500
+ state.stopMessageText = data.stopMessageText;
501
+ }
502
+ if (typeof data.stopMessageMaxRepeats === 'number' && Number.isFinite(data.stopMessageMaxRepeats)) {
503
+ state.stopMessageMaxRepeats = Math.floor(data.stopMessageMaxRepeats);
504
+ }
505
+ if (typeof data.stopMessageUsed === 'number' && Number.isFinite(data.stopMessageUsed)) {
506
+ state.stopMessageUsed = Math.max(0, Math.floor(data.stopMessageUsed));
507
+ }
392
508
  return state;
393
509
  }
@@ -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 @@
1
+ export {};
@@ -0,0 +1,147 @@
1
+ import { registerServerToolHandler } from '../registry.js';
2
+ import { cloneJson } from '../server-side-tools.js';
3
+ import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync } from '../../router/virtual-router/sticky-session-store.js';
4
+ const FLOW_ID = 'stop_message_flow';
5
+ const handler = async (ctx) => {
6
+ const record = ctx.adapterContext;
7
+ const followupRaw = record.serverToolFollowup;
8
+ if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
9
+ return null;
10
+ }
11
+ const clientDisconnectedRaw = record.clientDisconnected;
12
+ if (clientDisconnectedRaw === true ||
13
+ (typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
14
+ return null;
15
+ }
16
+ const stickyKey = resolveStickyKey(record);
17
+ if (!stickyKey) {
18
+ return null;
19
+ }
20
+ const state = loadRoutingInstructionStateSync(stickyKey);
21
+ if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
22
+ return null;
23
+ }
24
+ const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
25
+ const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
26
+ ? Math.max(1, Math.floor(state.stopMessageMaxRepeats))
27
+ : 0;
28
+ if (!text || maxRepeats <= 0) {
29
+ return null;
30
+ }
31
+ const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
32
+ ? Math.max(0, Math.floor(state.stopMessageUsed))
33
+ : 0;
34
+ if (used >= maxRepeats) {
35
+ return null;
36
+ }
37
+ if (!isStopFinishReason(ctx.base)) {
38
+ return null;
39
+ }
40
+ const captured = getCapturedRequest(ctx.adapterContext);
41
+ if (!captured) {
42
+ return null;
43
+ }
44
+ state.stopMessageUsed = used + 1;
45
+ saveRoutingInstructionStateAsync(stickyKey, state);
46
+ const followupPayload = buildStopMessageFollowupPayload(captured, text);
47
+ if (!followupPayload) {
48
+ return null;
49
+ }
50
+ return {
51
+ chatResponse: ctx.base,
52
+ execution: {
53
+ flowId: FLOW_ID,
54
+ followup: {
55
+ requestIdSuffix: ':stop_followup',
56
+ payload: followupPayload,
57
+ metadata: {
58
+ serverToolFollowup: true,
59
+ stream: false
60
+ }
61
+ }
62
+ }
63
+ };
64
+ };
65
+ registerServerToolHandler('stop_message_auto', handler, { trigger: 'auto' });
66
+ function resolveStickyKey(record) {
67
+ const sessionId = typeof record.sessionId === 'string' && record.sessionId.trim() ? record.sessionId.trim() : '';
68
+ if (sessionId) {
69
+ return `session:${sessionId}`;
70
+ }
71
+ const conversationId = typeof record.conversationId === 'string' && record.conversationId.trim()
72
+ ? record.conversationId.trim()
73
+ : '';
74
+ if (conversationId) {
75
+ return `conversation:${conversationId}`;
76
+ }
77
+ return undefined;
78
+ }
79
+ function isStopFinishReason(base) {
80
+ if (!base || typeof base !== 'object' || Array.isArray(base)) {
81
+ return false;
82
+ }
83
+ const payload = base;
84
+ const choicesRaw = payload.choices;
85
+ if (!Array.isArray(choicesRaw) || !choicesRaw.length) {
86
+ return false;
87
+ }
88
+ const first = choicesRaw[0];
89
+ if (!first || typeof first !== 'object' || Array.isArray(first)) {
90
+ return false;
91
+ }
92
+ const finishReasonRaw = first.finish_reason;
93
+ const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
94
+ ? finishReasonRaw.trim().toLowerCase()
95
+ : '';
96
+ if (finishReason !== 'stop') {
97
+ return false;
98
+ }
99
+ const message = first.message &&
100
+ typeof first.message === 'object' &&
101
+ !Array.isArray(first.message)
102
+ ? first.message
103
+ : null;
104
+ if (!message) {
105
+ return false;
106
+ }
107
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
108
+ if (toolCalls.length > 0) {
109
+ return false;
110
+ }
111
+ return true;
112
+ }
113
+ function getCapturedRequest(adapterContext) {
114
+ if (!adapterContext || typeof adapterContext !== 'object') {
115
+ return null;
116
+ }
117
+ const captured = adapterContext.capturedChatRequest;
118
+ if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
119
+ return null;
120
+ }
121
+ return captured;
122
+ }
123
+ function buildStopMessageFollowupPayload(source, text) {
124
+ if (!source || typeof source !== 'object') {
125
+ return null;
126
+ }
127
+ const payload = {};
128
+ if (typeof source.model === 'string' && source.model.trim()) {
129
+ payload.model = source.model.trim();
130
+ }
131
+ const rawMessages = source.messages;
132
+ const messages = Array.isArray(rawMessages) ? cloneJson(rawMessages) : [];
133
+ messages.push({
134
+ role: 'user',
135
+ content: text
136
+ });
137
+ payload.messages = messages;
138
+ if (Array.isArray(source.tools) && source.tools.length) {
139
+ payload.tools = cloneJson(source.tools);
140
+ }
141
+ const parameters = source.parameters;
142
+ if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
143
+ const params = cloneJson(parameters);
144
+ Object.assign(payload, params);
145
+ }
146
+ return payload;
147
+ }
@@ -60,7 +60,26 @@ function shouldRunVisionFlow(ctx) {
60
60
  if (followupFlag) {
61
61
  return false;
62
62
  }
63
- return record.hasImageAttachment === true || record.hasImageAttachment === 'true';
63
+ const hasImageAttachment = record.hasImageAttachment === true || record.hasImageAttachment === 'true';
64
+ if (!hasImageAttachment) {
65
+ return false;
66
+ }
67
+ // 若当前已经使用具备内建多模态能力的 Provider(例如 Gemini/Claude/ChatGPT 路径),
68
+ // 且未显式 forceVision,则不再触发额外的 vision 二跳,避免同一轮请求跑两次。
69
+ const forceVision = record.forceVision === true || record.forceVision === 'true';
70
+ if (forceVision) {
71
+ return true;
72
+ }
73
+ const providerType = typeof record.providerType === 'string' ? record.providerType.toLowerCase() : '';
74
+ const providerProtocol = typeof record.providerProtocol === 'string' ? record.providerProtocol.toLowerCase() : '';
75
+ const inlineMultimodal = providerType === 'gemini' ||
76
+ providerType === 'responses' ||
77
+ providerProtocol === 'gemini-chat' ||
78
+ providerProtocol === 'openai-responses';
79
+ if (inlineMultimodal) {
80
+ return false;
81
+ }
82
+ return true;
64
83
  }
65
84
  function getCapturedRequest(adapterContext) {
66
85
  if (!adapterContext || typeof adapterContext !== 'object') {
@@ -80,15 +99,15 @@ function buildVisionAnalysisPayload(source) {
80
99
  if (typeof source.model === 'string' && source.model.trim()) {
81
100
  payload.model = source.model.trim();
82
101
  }
83
- if (Array.isArray(source.messages)) {
84
- payload.messages = cloneJson(source.messages);
85
- }
86
- else {
102
+ const rawMessages = source.messages;
103
+ if (!Array.isArray(rawMessages) || !rawMessages.length) {
87
104
  return null;
88
105
  }
89
- if (Array.isArray(source.tools) && source.tools.length) {
90
- payload.tools = cloneJson(source.tools);
106
+ const visionMessages = buildVisionAnalysisMessages(rawMessages);
107
+ if (!visionMessages.length) {
108
+ return null;
91
109
  }
110
+ payload.messages = visionMessages;
92
111
  const parameters = source.parameters;
93
112
  if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
94
113
  const params = cloneJson(parameters);
@@ -96,6 +115,85 @@ function buildVisionAnalysisPayload(source) {
96
115
  }
97
116
  return payload;
98
117
  }
118
+ function buildVisionAnalysisMessages(sourceMessages) {
119
+ const latestUser = extractLatestUserMessageForVision(sourceMessages);
120
+ if (!latestUser) {
121
+ return [];
122
+ }
123
+ const userMessage = buildVisionUserMessage(latestUser);
124
+ if (!userMessage) {
125
+ return [];
126
+ }
127
+ const messages = [];
128
+ const systemMessage = buildVisionSystemMessage();
129
+ if (systemMessage) {
130
+ messages.push(systemMessage);
131
+ }
132
+ messages.push(userMessage);
133
+ return messages;
134
+ }
135
+ function extractLatestUserMessageForVision(sourceMessages) {
136
+ for (let idx = sourceMessages.length - 1; idx >= 0; idx -= 1) {
137
+ const msg = sourceMessages[idx];
138
+ if (!msg || typeof msg !== 'object' || Array.isArray(msg)) {
139
+ continue;
140
+ }
141
+ const role = msg.role;
142
+ if (typeof role === 'string' && role.trim().toLowerCase() === 'user') {
143
+ return cloneJson(msg);
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+ function buildVisionSystemMessage() {
149
+ const content = '你是一名专业的图像分析助手。无论输入是界面截图、文档、图表、代码编辑器、应用窗口还是普通照片,都需要先用结构化、详细的自然语言完整描述画面内容(关键区域、文字信息、布局层次、颜色与对比度、元素之间的关系等),然后总结出与用户任务最相关的关键信息和潜在问题,最后给出具体、可执行的改进建议或结论,避免泛泛而谈。';
150
+ return {
151
+ role: 'system',
152
+ content
153
+ };
154
+ }
155
+ function buildVisionUserMessage(source) {
156
+ const roleRaw = source.role;
157
+ const role = typeof roleRaw === 'string' && roleRaw.trim().length
158
+ ? roleRaw.trim()
159
+ : 'user';
160
+ const rawContent = source.content;
161
+ const message = { role };
162
+ if (Array.isArray(rawContent)) {
163
+ const textParts = [];
164
+ const imageParts = [];
165
+ for (const part of rawContent) {
166
+ if (!part || typeof part !== 'object' || Array.isArray(part)) {
167
+ textParts.push(part);
168
+ continue;
169
+ }
170
+ const record = part;
171
+ const typeValue = typeof record.type === 'string' ? record.type.toLowerCase() : '';
172
+ if (typeValue.includes('image')) {
173
+ imageParts.push(part);
174
+ }
175
+ else {
176
+ textParts.push(part);
177
+ }
178
+ }
179
+ const combined = [];
180
+ if (textParts.length)
181
+ combined.push(...textParts);
182
+ if (imageParts.length)
183
+ combined.push(...imageParts);
184
+ if (!combined.length) {
185
+ return null;
186
+ }
187
+ message.content = combined;
188
+ }
189
+ else if (typeof rawContent === 'string' && rawContent.trim().length) {
190
+ message.content = rawContent.trim();
191
+ }
192
+ else {
193
+ return null;
194
+ }
195
+ return message;
196
+ }
99
197
  function buildVisionFollowupPayload(source, summary) {
100
198
  if (!source || typeof source !== 'object') {
101
199
  return null;
@@ -2,6 +2,8 @@ import type { JsonObject } from '../conversion/hub/types/json.js';
2
2
  import type { ServerSideToolEngineOptions, ServerSideToolEngineResult, ToolCall } from './types.js';
3
3
  import './handlers/web-search.js';
4
4
  import './handlers/vision.js';
5
+ import './handlers/gemini-empty-reply-continue.js';
6
+ import './handlers/stop-message-auto.js';
5
7
  export declare function runServerSideToolEngine(options: ServerSideToolEngineOptions): Promise<ServerSideToolEngineResult>;
6
8
  export declare function extractToolCalls(chatResponse: JsonObject): ToolCall[];
7
9
  export declare function cloneJson<T>(value: T): T;
@@ -1,6 +1,8 @@
1
1
  import { getServerToolHandler, listAutoServerToolHandlers } from './registry.js';
2
2
  import './handlers/web-search.js';
3
3
  import './handlers/vision.js';
4
+ import './handlers/gemini-empty-reply-continue.js';
5
+ import './handlers/stop-message-auto.js';
4
6
  export async function runServerSideToolEngine(options) {
5
7
  const base = asObject(options.chatResponse);
6
8
  if (!base) {