@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.
- package/dist/conversion/compat/actions/claude-thinking-tools.d.ts +15 -0
- package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +15 -14
- package/dist/conversion/compat/profiles/chat-glm.json +194 -194
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +15 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
- package/dist/conversion/hub/process/chat-process.js +44 -17
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +13 -8
- package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
- package/dist/conversion/hub/tool-session-compat.js +299 -0
- package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
- package/dist/conversion/responses/responses-openai-bridge.js +0 -71
- package/dist/conversion/shared/gemini-tool-utils.js +8 -0
- package/dist/conversion/shared/responses-output-builder.js +6 -68
- package/dist/conversion/shared/tool-governor.js +75 -4
- package/dist/conversion/shared/tool-mapping.js +14 -8
- package/dist/filters/special/request-toolcalls-stringify.js +5 -55
- package/dist/filters/special/request-tools-normalize.js +0 -19
- package/dist/guidance/index.js +25 -9
- package/dist/router/virtual-router/engine-health.d.ts +11 -0
- package/dist/router/virtual-router/engine-health.js +210 -0
- package/dist/router/virtual-router/engine-logging.d.ts +19 -0
- package/dist/router/virtual-router/engine-logging.js +165 -0
- package/dist/router/virtual-router/engine-selection.d.ts +32 -0
- package/dist/router/virtual-router/engine-selection.js +649 -0
- package/dist/router/virtual-router/engine.d.ts +4 -13
- package/dist/router/virtual-router/engine.js +64 -535
- package/dist/router/virtual-router/message-utils.js +22 -0
- package/dist/router/virtual-router/routing-instructions.d.ts +6 -1
- package/dist/router/virtual-router/routing-instructions.js +119 -3
- package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -0
- package/dist/servertool/handlers/stop-message-auto.d.ts +1 -0
- package/dist/servertool/handlers/stop-message-auto.js +147 -0
- package/dist/servertool/handlers/vision.js +105 -7
- package/dist/servertool/server-side-tools.d.ts +2 -0
- package/dist/servertool/server-side-tools.js +2 -0
- package/dist/tools/tool-registry.js +195 -4
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
102
|
+
const rawMessages = source.messages;
|
|
103
|
+
if (!Array.isArray(rawMessages) || !rawMessages.length) {
|
|
87
104
|
return null;
|
|
88
105
|
}
|
|
89
|
-
|
|
90
|
-
|
|
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) {
|