@jsonstudio/llms 0.6.467 → 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 +1 -1
- 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 -517
- package/dist/router/virtual-router/health-manager.d.ts +23 -0
- package/dist/router/virtual-router/health-manager.js +14 -0
- 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 +129 -3
- package/dist/router/virtual-router/types.d.ts +6 -0
- 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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ProviderHealthConfig, ProviderHealthState } from './types.js';
|
|
2
|
+
interface ProviderInternalState extends ProviderHealthState {
|
|
3
|
+
lastFailureAt?: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class ProviderHealthManager {
|
|
6
|
+
private readonly states;
|
|
7
|
+
private config;
|
|
8
|
+
configure(config?: ProviderHealthConfig): void;
|
|
9
|
+
registerProviders(providerKeys: string[]): void;
|
|
10
|
+
recordFailure(providerKey: string, reason?: string): ProviderInternalState;
|
|
11
|
+
/**
|
|
12
|
+
* 为可恢复错误(例如 429)提供短暂冷静期:在 cooldownMs 内将 providerKey
|
|
13
|
+
* 视为不可用,但不使用 fatalCooldownMs 的长熔断时间。
|
|
14
|
+
*/
|
|
15
|
+
cooldownProvider(providerKey: string, reason?: string, overrideMs?: number): ProviderInternalState;
|
|
16
|
+
recordSuccess(providerKey: string): ProviderInternalState;
|
|
17
|
+
tripProvider(providerKey: string, reason?: string, cooldownOverrideMs?: number): ProviderInternalState;
|
|
18
|
+
isAvailable(providerKey: string): boolean;
|
|
19
|
+
getSnapshot(): ProviderHealthState[];
|
|
20
|
+
getConfig(): Required<ProviderHealthConfig>;
|
|
21
|
+
private getState;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -39,6 +39,20 @@ export class ProviderHealthManager {
|
|
|
39
39
|
// 只有显式 fatal 或 tripProvider 调用时才会进入 tripped 状态。
|
|
40
40
|
return state;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* 为可恢复错误(例如 429)提供短暂冷静期:在 cooldownMs 内将 providerKey
|
|
44
|
+
* 视为不可用,但不使用 fatalCooldownMs 的长熔断时间。
|
|
45
|
+
*/
|
|
46
|
+
cooldownProvider(providerKey, reason, overrideMs) {
|
|
47
|
+
const state = this.getState(providerKey);
|
|
48
|
+
state.failureCount += 1;
|
|
49
|
+
state.state = 'tripped';
|
|
50
|
+
state.reason = reason;
|
|
51
|
+
const ttl = overrideMs ?? this.config.cooldownMs;
|
|
52
|
+
state.cooldownExpiresAt = Date.now() + ttl;
|
|
53
|
+
state.lastFailureAt = Date.now();
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
42
56
|
recordSuccess(providerKey) {
|
|
43
57
|
const state = this.getState(providerKey);
|
|
44
58
|
state.failureCount = 0;
|
|
@@ -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) {
|
|
@@ -113,6 +186,16 @@ function parseSingleInstruction(instruction) {
|
|
|
113
186
|
return { type: 'force', ...normalized };
|
|
114
187
|
}
|
|
115
188
|
}
|
|
189
|
+
else if (isValidIdentifier(instruction)) {
|
|
190
|
+
// 仅 provider 标识(无 .)时,视为 provider 级白名单,等价于 "<**!provider**>"。
|
|
191
|
+
// 这样可以用 "<**antigravity**>" 快速激活当前 routing 中所有 antigravity 相关池子,
|
|
192
|
+
// 并保证路由仅命中该 provider 的所有模型/key。
|
|
193
|
+
return {
|
|
194
|
+
type: 'allow',
|
|
195
|
+
provider: instruction,
|
|
196
|
+
pathLength: 1
|
|
197
|
+
};
|
|
198
|
+
}
|
|
116
199
|
return null;
|
|
117
200
|
}
|
|
118
201
|
function parseTarget(target) {
|
|
@@ -189,7 +272,10 @@ export function applyRoutingInstructions(instructions, currentState) {
|
|
|
189
272
|
allowedProviders: new Set(currentState.allowedProviders),
|
|
190
273
|
disabledProviders: new Set(currentState.disabledProviders),
|
|
191
274
|
disabledKeys: new Map(Array.from(currentState.disabledKeys.entries()).map(([k, v]) => [k, new Set(v)])),
|
|
192
|
-
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
|
|
193
279
|
};
|
|
194
280
|
let allowReset = false;
|
|
195
281
|
let disableReset = false;
|
|
@@ -302,6 +388,25 @@ export function applyRoutingInstructions(instructions, currentState) {
|
|
|
302
388
|
newState.disabledKeys.clear();
|
|
303
389
|
newState.disabledModels.clear();
|
|
304
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;
|
|
305
410
|
}
|
|
306
411
|
}
|
|
307
412
|
return newState;
|
|
@@ -341,7 +446,16 @@ export function serializeRoutingInstructionState(state) {
|
|
|
341
446
|
disabledModels: Array.from(state.disabledModels.entries()).map(([provider, models]) => ({
|
|
342
447
|
provider,
|
|
343
448
|
models: Array.from(models)
|
|
344
|
-
}))
|
|
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
|
+
: {})
|
|
345
459
|
};
|
|
346
460
|
}
|
|
347
461
|
export function deserializeRoutingInstructionState(data) {
|
|
@@ -351,7 +465,10 @@ export function deserializeRoutingInstructionState(data) {
|
|
|
351
465
|
allowedProviders: new Set(),
|
|
352
466
|
disabledProviders: new Set(),
|
|
353
467
|
disabledKeys: new Map(),
|
|
354
|
-
disabledModels: new Map()
|
|
468
|
+
disabledModels: new Map(),
|
|
469
|
+
stopMessageText: undefined,
|
|
470
|
+
stopMessageMaxRepeats: undefined,
|
|
471
|
+
stopMessageUsed: undefined
|
|
355
472
|
};
|
|
356
473
|
if (data.forcedTarget && typeof data.forcedTarget === 'object') {
|
|
357
474
|
state.forcedTarget = data.forcedTarget;
|
|
@@ -379,5 +496,14 @@ export function deserializeRoutingInstructionState(data) {
|
|
|
379
496
|
}
|
|
380
497
|
}
|
|
381
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
|
+
}
|
|
382
508
|
return state;
|
|
383
509
|
}
|
|
@@ -200,6 +200,12 @@ export interface RouterMetadataInput {
|
|
|
200
200
|
* 禁用的 provider keyIndex 列表 (从 1 开始)
|
|
201
201
|
*/
|
|
202
202
|
disabledProviderKeyIndexes?: number[];
|
|
203
|
+
/**
|
|
204
|
+
* 本次请求内需要临时排除的 providerKey 列表。
|
|
205
|
+
* 与 disabledProviders/disabledKeys 不同,这些 key 仅对当前路由决策生效,
|
|
206
|
+
* 不会写入或持久化到 RoutingInstructionState/sticky 存储中。
|
|
207
|
+
*/
|
|
208
|
+
excludedProviderKeys?: string[];
|
|
203
209
|
sessionId?: string;
|
|
204
210
|
conversationId?: string;
|
|
205
211
|
responsesResume?: {
|
|
@@ -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
|
+
}
|