@jsonstudio/llms 0.6.586 → 0.6.631
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/hub/pipeline/hub-pipeline.js +21 -1
- package/dist/conversion/hub/process/chat-process.js +34 -3
- package/dist/conversion/hub/response/response-runtime.js +47 -1
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +59 -4
- package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +8 -0
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +93 -12
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +201 -30
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +73 -11
- package/dist/conversion/hub/standardized-bridge.js +11 -2
- package/dist/conversion/hub/types/chat-envelope.d.ts +10 -0
- package/dist/conversion/hub/types/standardized.d.ts +2 -1
- package/dist/conversion/shared/gemini-tool-utils.js +2 -2
- package/dist/conversion/shared/text-markup-normalizer.js +8 -1
- package/dist/conversion/shared/tool-filter-pipeline.d.ts +1 -1
- package/dist/filters/index.d.ts +1 -0
- package/dist/router/virtual-router/engine.js +16 -5
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +28 -4
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
|
+
import { isJsonObject } from '../types/json.js';
|
|
2
3
|
import { VirtualRouterEngine } from '../../../router/virtual-router/engine.js';
|
|
3
4
|
import { providerErrorCenter } from '../../../router/virtual-router/error-center.js';
|
|
4
5
|
import { defaultSseCodecRegistry } from '../../../sse/index.js';
|
|
@@ -311,7 +312,11 @@ export class HubPipeline {
|
|
|
311
312
|
const contextNode = metadataNode && metadataNode.context && typeof metadataNode.context === 'object'
|
|
312
313
|
? metadataNode.context
|
|
313
314
|
: undefined;
|
|
314
|
-
|
|
315
|
+
const fromContextNode = coerceAliasMap(contextNode?.anthropicToolNameMap);
|
|
316
|
+
if (fromContextNode) {
|
|
317
|
+
return fromContextNode;
|
|
318
|
+
}
|
|
319
|
+
return readAliasMapFromSemantics(chatEnvelope);
|
|
315
320
|
}
|
|
316
321
|
resolveProtocolHooks(protocol) {
|
|
317
322
|
switch (protocol) {
|
|
@@ -398,6 +403,11 @@ export class HubPipeline {
|
|
|
398
403
|
if (responsesResume) {
|
|
399
404
|
adapterContext.responsesResume = responsesResume;
|
|
400
405
|
}
|
|
406
|
+
// 透传 gemini_empty_reply_continue 的重试计数,便于在多次空回复后终止自动续写。
|
|
407
|
+
const emptyReplyCount = metadata.geminiEmptyReplyCount;
|
|
408
|
+
if (typeof emptyReplyCount === 'number' && Number.isFinite(emptyReplyCount)) {
|
|
409
|
+
adapterContext.geminiEmptyReplyCount = emptyReplyCount;
|
|
410
|
+
}
|
|
401
411
|
if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
|
|
402
412
|
adapterContext.compatibilityProfile = target.compatibilityProfile;
|
|
403
413
|
}
|
|
@@ -674,3 +684,13 @@ function coerceAliasMap(candidate) {
|
|
|
674
684
|
}
|
|
675
685
|
return Object.keys(normalized).length ? normalized : undefined;
|
|
676
686
|
}
|
|
687
|
+
function readAliasMapFromSemantics(chatEnvelope) {
|
|
688
|
+
if (!chatEnvelope?.semantics || typeof chatEnvelope.semantics !== 'object') {
|
|
689
|
+
return undefined;
|
|
690
|
+
}
|
|
691
|
+
const node = chatEnvelope.semantics.anthropic;
|
|
692
|
+
if (!node || !isJsonObject(node)) {
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
return coerceAliasMap(node.toolAliasMap);
|
|
696
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js';
|
|
2
2
|
import { ToolGovernanceEngine } from '../tool-governance/index.js';
|
|
3
3
|
import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
|
|
4
|
+
import { isJsonObject } from '../types/json.js';
|
|
4
5
|
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
5
6
|
export async function runHubChatProcess(options) {
|
|
6
7
|
const startTime = Date.now();
|
|
@@ -403,9 +404,15 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
403
404
|
if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
|
|
404
405
|
return request;
|
|
405
406
|
}
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
407
|
+
const semanticsWebSearch = extractWebSearchSemantics(request.semantics);
|
|
408
|
+
if (semanticsWebSearch?.disable === true) {
|
|
409
|
+
return request;
|
|
410
|
+
}
|
|
411
|
+
const injectPolicy = semanticsWebSearch?.force === true
|
|
412
|
+
? 'always'
|
|
413
|
+
: rawConfig.injectPolicy === 'always' || rawConfig.injectPolicy === 'selective'
|
|
414
|
+
? rawConfig.injectPolicy
|
|
415
|
+
: 'selective';
|
|
409
416
|
const intent = detectWebSearchIntent(request);
|
|
410
417
|
if (injectPolicy === 'selective') {
|
|
411
418
|
// 仅当当前这一轮用户输入明确表达“联网搜索”意图时才注入 web_search。
|
|
@@ -512,6 +519,30 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
512
519
|
tools: [...existingTools, webSearchTool]
|
|
513
520
|
};
|
|
514
521
|
}
|
|
522
|
+
function extractWebSearchSemantics(semantics) {
|
|
523
|
+
if (!semantics || typeof semantics !== 'object') {
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
const extras = semantics.providerExtras;
|
|
527
|
+
if (!extras || !isJsonObject(extras)) {
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
const hint = extras.webSearch;
|
|
531
|
+
if (typeof hint === 'boolean') {
|
|
532
|
+
return hint ? { force: true } : { disable: true };
|
|
533
|
+
}
|
|
534
|
+
if (isJsonObject(hint)) {
|
|
535
|
+
const normalized = {};
|
|
536
|
+
if (hint.force === true) {
|
|
537
|
+
normalized.force = true;
|
|
538
|
+
}
|
|
539
|
+
if (hint.disable === true) {
|
|
540
|
+
normalized.disable = true;
|
|
541
|
+
}
|
|
542
|
+
return Object.keys(normalized).length ? normalized : undefined;
|
|
543
|
+
}
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
515
546
|
function detectWebSearchIntent(request) {
|
|
516
547
|
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
517
548
|
if (!messages.length) {
|
|
@@ -21,6 +21,15 @@ function flattenAnthropicContent(content) {
|
|
|
21
21
|
}
|
|
22
22
|
return '';
|
|
23
23
|
}
|
|
24
|
+
function sanitizeAnthropicToolUseId(raw) {
|
|
25
|
+
if (typeof raw === 'string') {
|
|
26
|
+
const trimmed = raw.trim();
|
|
27
|
+
if (trimmed && /^[A-Za-z0-9_-]+$/.test(trimmed)) {
|
|
28
|
+
return trimmed;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return `call_${Math.random().toString(36).slice(2, 10)}`;
|
|
32
|
+
}
|
|
24
33
|
function createToolNameResolver(options) {
|
|
25
34
|
const reverse = new Map();
|
|
26
35
|
const aliasMap = options?.aliasMap;
|
|
@@ -281,6 +290,39 @@ export function buildOpenAIChatFromAnthropicMessage(payload, options) {
|
|
|
281
290
|
}
|
|
282
291
|
return chatResponse;
|
|
283
292
|
}
|
|
293
|
+
function mapShellCommandArgsForAnthropic(raw) {
|
|
294
|
+
const result = {};
|
|
295
|
+
const source = (raw && typeof raw === 'object' && !Array.isArray(raw)) ? raw : {};
|
|
296
|
+
const commandRaw = typeof source.command === 'string' && source.command.trim().length
|
|
297
|
+
? source.command
|
|
298
|
+
: typeof source.cmd === 'string' && source.cmd.trim().length
|
|
299
|
+
? source.cmd
|
|
300
|
+
: '';
|
|
301
|
+
const command = commandRaw.trim();
|
|
302
|
+
if (command) {
|
|
303
|
+
result.command = command;
|
|
304
|
+
}
|
|
305
|
+
const timeoutRaw = source.timeout_ms ?? source.timeout;
|
|
306
|
+
if (typeof timeoutRaw === 'number' && Number.isFinite(timeoutRaw)) {
|
|
307
|
+
result.timeout = timeoutRaw;
|
|
308
|
+
}
|
|
309
|
+
else if (typeof timeoutRaw === 'string' && timeoutRaw.trim().length) {
|
|
310
|
+
const parsed = Number(timeoutRaw.trim());
|
|
311
|
+
if (Number.isFinite(parsed)) {
|
|
312
|
+
result.timeout = parsed;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (typeof source.description === 'string' && source.description.trim().length) {
|
|
316
|
+
result.description = source.description;
|
|
317
|
+
}
|
|
318
|
+
if (typeof source.run_in_background === 'boolean') {
|
|
319
|
+
result.run_in_background = source.run_in_background;
|
|
320
|
+
}
|
|
321
|
+
if (typeof source.dangerouslyDisableSandbox === 'boolean') {
|
|
322
|
+
result.dangerouslyDisableSandbox = source.dangerouslyDisableSandbox;
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
284
326
|
export function buildAnthropicResponseFromChat(chatResponse, options) {
|
|
285
327
|
const choice = Array.isArray(chatResponse?.choices) ? chatResponse.choices[0] : undefined;
|
|
286
328
|
const message = choice && typeof choice === 'object' ? choice.message : undefined;
|
|
@@ -327,6 +369,7 @@ export function buildAnthropicResponseFromChat(chatResponse, options) {
|
|
|
327
369
|
const fn = call.function || {};
|
|
328
370
|
if (typeof fn?.name !== 'string')
|
|
329
371
|
continue;
|
|
372
|
+
const canonicalName = normalizeAnthropicToolName(fn.name) ?? fn.name;
|
|
330
373
|
const serializedName = outboundAliasSerializer(fn.name);
|
|
331
374
|
let parsedArgs = {};
|
|
332
375
|
const args = fn.arguments;
|
|
@@ -341,9 +384,12 @@ export function buildAnthropicResponseFromChat(chatResponse, options) {
|
|
|
341
384
|
else {
|
|
342
385
|
parsedArgs = args ?? {};
|
|
343
386
|
}
|
|
387
|
+
if ((canonicalName || '').trim() === 'shell_command') {
|
|
388
|
+
parsedArgs = mapShellCommandArgsForAnthropic(parsedArgs);
|
|
389
|
+
}
|
|
344
390
|
contentBlocks.push({
|
|
345
391
|
type: 'tool_use',
|
|
346
|
-
id:
|
|
392
|
+
id: sanitizeAnthropicToolUseId(call.id),
|
|
347
393
|
name: serializedName,
|
|
348
394
|
input: parsedArgs
|
|
349
395
|
});
|
|
@@ -34,6 +34,43 @@ const ANTHROPIC_TOP_LEVEL_FIELDS = new Set([
|
|
|
34
34
|
]);
|
|
35
35
|
const PASSTHROUGH_METADATA_PREFIX = 'rcc_passthrough_';
|
|
36
36
|
const PASSTHROUGH_PARAMETERS = ['tool_choice'];
|
|
37
|
+
function ensureSemantics(chat) {
|
|
38
|
+
if (!chat.semantics || typeof chat.semantics !== 'object') {
|
|
39
|
+
chat.semantics = {};
|
|
40
|
+
}
|
|
41
|
+
return chat.semantics;
|
|
42
|
+
}
|
|
43
|
+
function ensureAnthropicSemanticsNode(chat) {
|
|
44
|
+
const semantics = ensureSemantics(chat);
|
|
45
|
+
if (!semantics.anthropic || !isJsonObject(semantics.anthropic)) {
|
|
46
|
+
semantics.anthropic = {};
|
|
47
|
+
}
|
|
48
|
+
return semantics.anthropic;
|
|
49
|
+
}
|
|
50
|
+
function markExplicitEmptyTools(chat) {
|
|
51
|
+
const semantics = ensureSemantics(chat);
|
|
52
|
+
if (!semantics.tools || !isJsonObject(semantics.tools)) {
|
|
53
|
+
semantics.tools = {};
|
|
54
|
+
}
|
|
55
|
+
semantics.tools.explicitEmpty = true;
|
|
56
|
+
}
|
|
57
|
+
function readAnthropicSemantics(chat) {
|
|
58
|
+
if (!chat.semantics || typeof chat.semantics !== 'object') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const node = chat.semantics.anthropic;
|
|
62
|
+
return node && isJsonObject(node) ? node : undefined;
|
|
63
|
+
}
|
|
64
|
+
function hasExplicitEmptyToolsSemantics(chat) {
|
|
65
|
+
if (!chat.semantics || typeof chat.semantics !== 'object') {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const toolsNode = chat.semantics.tools;
|
|
69
|
+
if (!toolsNode || !isJsonObject(toolsNode)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return Boolean(toolsNode.explicitEmpty);
|
|
73
|
+
}
|
|
37
74
|
function sanitizeAnthropicPayload(payload) {
|
|
38
75
|
for (const key of Object.keys(payload)) {
|
|
39
76
|
if (!ANTHROPIC_TOP_LEVEL_FIELDS.has(key)) {
|
|
@@ -91,6 +128,7 @@ export class AnthropicSemanticMapper {
|
|
|
91
128
|
const metadata = chatEnvelope.metadata ?? { context: canonicalContext };
|
|
92
129
|
chatEnvelope.metadata = metadata;
|
|
93
130
|
metadata.context = canonicalContext;
|
|
131
|
+
let semanticsNode;
|
|
94
132
|
const resolveExtraFields = () => {
|
|
95
133
|
if (!isJsonObject(metadata.extraFields)) {
|
|
96
134
|
metadata.extraFields = {};
|
|
@@ -101,10 +139,13 @@ export class AnthropicSemanticMapper {
|
|
|
101
139
|
const systemBlocks = cloneAnthropicSystemBlocks(payload.system);
|
|
102
140
|
if (systemBlocks) {
|
|
103
141
|
protocolState.systemBlocks = systemBlocks;
|
|
142
|
+
semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
|
|
143
|
+
semanticsNode.systemBlocks = jsonClone(systemBlocks);
|
|
104
144
|
}
|
|
105
145
|
if (payload.tools && Array.isArray(payload.tools) && payload.tools.length === 0) {
|
|
106
146
|
metadata.toolsFieldPresent = true;
|
|
107
147
|
resolveExtraFields().toolsFieldPresent = true;
|
|
148
|
+
markExplicitEmptyTools(chatEnvelope);
|
|
108
149
|
}
|
|
109
150
|
const aliasMap = buildAnthropicToolAliasMap(payload.tools);
|
|
110
151
|
if (aliasMap) {
|
|
@@ -113,6 +154,8 @@ export class AnthropicSemanticMapper {
|
|
|
113
154
|
canonicalContext.anthropicToolNameMap = aliasMap;
|
|
114
155
|
metadata.anthropicToolNameMap = aliasMap;
|
|
115
156
|
extraFields.anthropicToolNameMap = aliasMap;
|
|
157
|
+
semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
|
|
158
|
+
semanticsNode.toolAliasMap = jsonClone(aliasMap);
|
|
116
159
|
}
|
|
117
160
|
if (Array.isArray(payload.messages) && payload.messages.length) {
|
|
118
161
|
const shapes = payload.messages.map((entry) => {
|
|
@@ -137,6 +180,8 @@ export class AnthropicSemanticMapper {
|
|
|
137
180
|
: {};
|
|
138
181
|
mirrorNode.messageContentShape = shapes;
|
|
139
182
|
extraFields.anthropicMirror = mirrorNode;
|
|
183
|
+
semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
|
|
184
|
+
semanticsNode.mirror = jsonClone(mirrorNode);
|
|
140
185
|
}
|
|
141
186
|
if (missing.length) {
|
|
142
187
|
metadata.missingFields = Array.isArray(metadata.missingFields)
|
|
@@ -147,6 +192,8 @@ export class AnthropicSemanticMapper {
|
|
|
147
192
|
(payload.metadata && isJsonObject(payload.metadata) ? jsonClone(payload.metadata) : undefined);
|
|
148
193
|
if (providerMetadata) {
|
|
149
194
|
metadata.providerMetadata = providerMetadata;
|
|
195
|
+
semanticsNode = semanticsNode ?? ensureAnthropicSemanticsNode(chatEnvelope);
|
|
196
|
+
semanticsNode.providerMetadata = jsonClone(providerMetadata);
|
|
150
197
|
}
|
|
151
198
|
const mergedParameters = { ...(chatEnvelope.parameters ?? {}) };
|
|
152
199
|
const mergeParameters = (source) => {
|
|
@@ -217,6 +264,8 @@ export class AnthropicSemanticMapper {
|
|
|
217
264
|
messages: chat.messages,
|
|
218
265
|
tools: chat.tools
|
|
219
266
|
};
|
|
267
|
+
const semanticsNode = readAnthropicSemantics(chat);
|
|
268
|
+
const explicitEmptyTools = (chat.metadata?.toolsFieldPresent === true) || hasExplicitEmptyToolsSemantics(chat);
|
|
220
269
|
const trimmedParameters = chat.parameters && typeof chat.parameters === 'object' ? chat.parameters : undefined;
|
|
221
270
|
if (trimmedParameters) {
|
|
222
271
|
for (const [key, value] of Object.entries(trimmedParameters)) {
|
|
@@ -245,16 +294,19 @@ export class AnthropicSemanticMapper {
|
|
|
245
294
|
if (baseRequest.max_output_tokens && !baseRequest.max_tokens) {
|
|
246
295
|
baseRequest.max_tokens = baseRequest.max_output_tokens;
|
|
247
296
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
297
|
+
// 出站阶段不再直接透传其它协议的 providerMetadata,避免跨协议打洞;
|
|
298
|
+
// Anthropic 自身入口的 metadata 已在入站阶段通过 collectParameters/encodeMetadataPassthrough
|
|
299
|
+
// 按白名单收集,这里仅依赖这些显式映射结果。
|
|
300
|
+
if (explicitEmptyTools && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
|
|
252
301
|
baseRequest.tools = [];
|
|
253
302
|
}
|
|
254
303
|
const protocolState = getProtocolState(chat.metadata, 'anthropic');
|
|
255
304
|
if (protocolState?.systemBlocks !== undefined) {
|
|
256
305
|
baseRequest.system = jsonClone(protocolState.systemBlocks);
|
|
257
306
|
}
|
|
307
|
+
else if (semanticsNode?.systemBlocks !== undefined) {
|
|
308
|
+
baseRequest.system = jsonClone(semanticsNode.systemBlocks);
|
|
309
|
+
}
|
|
258
310
|
if (chat.metadata &&
|
|
259
311
|
typeof chat.metadata === 'object' &&
|
|
260
312
|
chat.metadata.extraFields &&
|
|
@@ -262,6 +314,9 @@ export class AnthropicSemanticMapper {
|
|
|
262
314
|
chat.metadata.extraFields.anthropicMirror) {
|
|
263
315
|
baseRequest.__anthropicMirror = jsonClone(chat.metadata.extraFields.anthropicMirror ?? {});
|
|
264
316
|
}
|
|
317
|
+
else if (semanticsNode?.mirror && isJsonObject(semanticsNode.mirror)) {
|
|
318
|
+
baseRequest.__anthropicMirror = jsonClone(semanticsNode.mirror);
|
|
319
|
+
}
|
|
265
320
|
const payloadSource = buildAnthropicRequestFromOpenAIChat(baseRequest);
|
|
266
321
|
const payload = sanitizeAnthropicPayload(JSON.parse(JSON.stringify(payloadSource)));
|
|
267
322
|
if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SemanticMapper } from '../format-adapters/index.js';
|
|
2
|
+
import type { AdapterContext, ChatEnvelope } from '../types/chat-envelope.js';
|
|
3
|
+
import type { FormatEnvelope } from '../types/format-envelope.js';
|
|
4
|
+
export declare function maybeAugmentApplyPatchErrorContent(content: string, toolName?: string): string;
|
|
5
|
+
export declare class ChatSemanticMapper implements SemanticMapper {
|
|
6
|
+
toChat(format: FormatEnvelope, ctx: AdapterContext): Promise<ChatEnvelope>;
|
|
7
|
+
fromChat(chat: ChatEnvelope, ctx: AdapterContext): Promise<FormatEnvelope>;
|
|
8
|
+
}
|
|
@@ -59,6 +59,22 @@ function normalizeToolContent(content) {
|
|
|
59
59
|
return String(content ?? '');
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
export function maybeAugmentApplyPatchErrorContent(content, toolName) {
|
|
63
|
+
if (!content)
|
|
64
|
+
return content;
|
|
65
|
+
const lower = content.toLowerCase();
|
|
66
|
+
const isApplyPatch = (typeof toolName === 'string' && toolName.trim() === 'apply_patch') ||
|
|
67
|
+
lower.includes('apply_patch verification failed');
|
|
68
|
+
if (!isApplyPatch) {
|
|
69
|
+
return content;
|
|
70
|
+
}
|
|
71
|
+
// 避免重复追加提示。
|
|
72
|
+
if (content.includes('[apply_patch hint]')) {
|
|
73
|
+
return content;
|
|
74
|
+
}
|
|
75
|
+
const hint = '\n\n[apply_patch hint] 在使用 apply_patch 之前,请先读取目标文件的最新内容,并基于该内容生成补丁;同时确保补丁格式符合工具规范(统一补丁格式或结构化参数),避免上下文不匹配或语法错误。';
|
|
76
|
+
return content + hint;
|
|
77
|
+
}
|
|
62
78
|
function recordToolCallIssues(message, messageIndex, missing) {
|
|
63
79
|
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : undefined;
|
|
64
80
|
if (!toolCalls?.length)
|
|
@@ -158,11 +174,13 @@ function normalizeChatMessages(raw) {
|
|
|
158
174
|
norm.missingFields.push({ path: `messages[${index}].tool_call_id`, reason: 'missing_tool_call_id' });
|
|
159
175
|
return;
|
|
160
176
|
}
|
|
177
|
+
const nameValue = typeof value.name === 'string' && value.name.trim().length ? value.name : undefined;
|
|
161
178
|
const outputEntry = {
|
|
162
179
|
tool_call_id: toolCallId,
|
|
163
180
|
content: normalizeToolContent(value.content ?? value.output),
|
|
164
|
-
name:
|
|
181
|
+
name: nameValue
|
|
165
182
|
};
|
|
183
|
+
outputEntry.content = maybeAugmentApplyPatchErrorContent(outputEntry.content, outputEntry.name);
|
|
166
184
|
norm.toolOutputs.push(outputEntry);
|
|
167
185
|
}
|
|
168
186
|
});
|
|
@@ -183,10 +201,13 @@ function normalizeStandaloneToolOutputs(raw, missing) {
|
|
|
183
201
|
missing.push({ path: `tool_outputs[${index}].tool_call_id`, reason: 'missing_tool_call_id' });
|
|
184
202
|
return;
|
|
185
203
|
}
|
|
204
|
+
const nameValue = typeof entry.name === 'string' && entry.name.trim().length ? entry.name : undefined;
|
|
205
|
+
const rawContent = normalizeToolContent(entry.content ?? entry.output);
|
|
206
|
+
const content = maybeAugmentApplyPatchErrorContent(rawContent, nameValue);
|
|
186
207
|
outputs.push({
|
|
187
208
|
tool_call_id: toolCallId,
|
|
188
|
-
content
|
|
189
|
-
name:
|
|
209
|
+
content,
|
|
210
|
+
name: nameValue
|
|
190
211
|
});
|
|
191
212
|
});
|
|
192
213
|
return outputs;
|
|
@@ -225,15 +246,67 @@ function collectExtraFields(body) {
|
|
|
225
246
|
}
|
|
226
247
|
return Object.keys(extras).length ? extras : undefined;
|
|
227
248
|
}
|
|
228
|
-
function
|
|
229
|
-
if (!
|
|
249
|
+
function extractOpenAIExtraFieldsFromSemantics(semantics) {
|
|
250
|
+
if (!semantics || !semantics.providerExtras || !isJsonObject(semantics.providerExtras)) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
const openaiExtras = semantics.providerExtras.openaiChat;
|
|
254
|
+
if (!openaiExtras || !isJsonObject(openaiExtras)) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
const stored = openaiExtras.extraFields;
|
|
258
|
+
if (!stored || !isJsonObject(stored)) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
return stored;
|
|
262
|
+
}
|
|
263
|
+
function hasExplicitEmptyToolsSemantics(semantics) {
|
|
264
|
+
if (!semantics || !semantics.tools || !isJsonObject(semantics.tools)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
const flag = semantics.tools.explicitEmpty;
|
|
268
|
+
return flag === true;
|
|
269
|
+
}
|
|
270
|
+
function buildOpenAISemantics(options) {
|
|
271
|
+
const semantics = {};
|
|
272
|
+
if (options.systemSegments && options.systemSegments.length) {
|
|
273
|
+
semantics.system = {
|
|
274
|
+
textBlocks: options.systemSegments.map((segment) => segment)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (options.extraFields && Object.keys(options.extraFields).length) {
|
|
278
|
+
semantics.providerExtras = {
|
|
279
|
+
openaiChat: {
|
|
280
|
+
extraFields: jsonClone(options.extraFields)
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (options.explicitEmptyTools) {
|
|
285
|
+
semantics.tools = {
|
|
286
|
+
explicitEmpty: true
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return Object.keys(semantics).length ? semantics : undefined;
|
|
290
|
+
}
|
|
291
|
+
function applyExtraFields(body, metadata, semantics) {
|
|
292
|
+
const sources = [];
|
|
293
|
+
const semanticsExtras = extractOpenAIExtraFieldsFromSemantics(semantics);
|
|
294
|
+
if (semanticsExtras) {
|
|
295
|
+
sources.push(semanticsExtras);
|
|
296
|
+
}
|
|
297
|
+
if (metadata?.extraFields && isJsonObject(metadata.extraFields)) {
|
|
298
|
+
sources.push(metadata.extraFields);
|
|
299
|
+
}
|
|
300
|
+
if (!sources.length) {
|
|
230
301
|
return;
|
|
231
302
|
}
|
|
232
|
-
for (const
|
|
233
|
-
|
|
234
|
-
|
|
303
|
+
for (const source of sources) {
|
|
304
|
+
for (const [key, value] of Object.entries(source)) {
|
|
305
|
+
if (body[key] !== undefined) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
body[key] = jsonClone(value);
|
|
235
309
|
}
|
|
236
|
-
body[key] = jsonClone(value);
|
|
237
310
|
}
|
|
238
311
|
}
|
|
239
312
|
export class ChatSemanticMapper {
|
|
@@ -285,24 +358,32 @@ export class ChatSemanticMapper {
|
|
|
285
358
|
catch {
|
|
286
359
|
// noop: best-effort policy application
|
|
287
360
|
}
|
|
288
|
-
|
|
361
|
+
const explicitEmptyTools = Array.isArray(payload.tools) && payload.tools.length === 0;
|
|
362
|
+
if (explicitEmptyTools) {
|
|
289
363
|
metadata.toolsFieldPresent = true;
|
|
290
364
|
}
|
|
365
|
+
const semantics = buildOpenAISemantics({
|
|
366
|
+
systemSegments: normalized.systemSegments,
|
|
367
|
+
extraFields,
|
|
368
|
+
explicitEmptyTools
|
|
369
|
+
});
|
|
291
370
|
return {
|
|
292
371
|
messages: normalized.messages,
|
|
293
372
|
tools: normalizeTools(payload.tools, normalized.missingFields),
|
|
294
373
|
toolOutputs: toolOutputs.length ? toolOutputs : undefined,
|
|
295
374
|
parameters: extractParameters(payload),
|
|
375
|
+
semantics,
|
|
296
376
|
metadata
|
|
297
377
|
};
|
|
298
378
|
}
|
|
299
379
|
async fromChat(chat, ctx) {
|
|
380
|
+
const shouldEmitEmptyTools = hasExplicitEmptyToolsSemantics(chat.semantics) || chat.metadata?.toolsFieldPresent === true;
|
|
300
381
|
const payload = {
|
|
301
382
|
messages: chat.messages,
|
|
302
|
-
tools: chat.tools ?? (
|
|
383
|
+
tools: chat.tools ?? (shouldEmitEmptyTools ? [] : undefined),
|
|
303
384
|
...(chat.parameters || {})
|
|
304
385
|
};
|
|
305
|
-
applyExtraFields(payload, chat.metadata);
|
|
386
|
+
applyExtraFields(payload, chat.metadata, chat.semantics);
|
|
306
387
|
try {
|
|
307
388
|
const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-chat' });
|
|
308
389
|
const actions = resolvePolicyActions(bridgePolicy, 'request_outbound');
|
|
@@ -25,6 +25,67 @@ function coerceThoughtSignature(value) {
|
|
|
25
25
|
}
|
|
26
26
|
return undefined;
|
|
27
27
|
}
|
|
28
|
+
function ensureGeminiSemanticsNode(chat) {
|
|
29
|
+
if (!chat.semantics || typeof chat.semantics !== 'object') {
|
|
30
|
+
chat.semantics = {};
|
|
31
|
+
}
|
|
32
|
+
if (!chat.semantics.gemini || !isJsonObject(chat.semantics.gemini)) {
|
|
33
|
+
chat.semantics.gemini = {};
|
|
34
|
+
}
|
|
35
|
+
return chat.semantics.gemini;
|
|
36
|
+
}
|
|
37
|
+
function ensureSystemSemantics(chat) {
|
|
38
|
+
if (!chat.semantics || typeof chat.semantics !== 'object') {
|
|
39
|
+
chat.semantics = {};
|
|
40
|
+
}
|
|
41
|
+
if (!chat.semantics.system || !isJsonObject(chat.semantics.system)) {
|
|
42
|
+
chat.semantics.system = {};
|
|
43
|
+
}
|
|
44
|
+
return chat.semantics.system;
|
|
45
|
+
}
|
|
46
|
+
function markGeminiExplicitEmptyTools(chat) {
|
|
47
|
+
if (!chat.semantics || typeof chat.semantics !== 'object') {
|
|
48
|
+
chat.semantics = {};
|
|
49
|
+
}
|
|
50
|
+
if (!chat.semantics.tools || !isJsonObject(chat.semantics.tools)) {
|
|
51
|
+
chat.semantics.tools = {};
|
|
52
|
+
}
|
|
53
|
+
chat.semantics.tools.explicitEmpty = true;
|
|
54
|
+
}
|
|
55
|
+
function readGeminiSemantics(chat) {
|
|
56
|
+
if (!chat.semantics || typeof chat.semantics !== 'object') {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const node = chat.semantics.gemini;
|
|
60
|
+
return node && isJsonObject(node) ? node : undefined;
|
|
61
|
+
}
|
|
62
|
+
function hasExplicitEmptyToolsSemantics(chat) {
|
|
63
|
+
if (!chat.semantics || typeof chat.semantics !== 'object') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const toolsNode = chat.semantics.tools;
|
|
67
|
+
if (!toolsNode || !isJsonObject(toolsNode)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return Boolean(toolsNode.explicitEmpty);
|
|
71
|
+
}
|
|
72
|
+
function readSystemTextBlocksFromSemantics(chat) {
|
|
73
|
+
if (!chat.semantics || typeof chat.semantics !== 'object') {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const systemNode = chat.semantics.system;
|
|
77
|
+
if (!systemNode || !isJsonObject(systemNode)) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const rawBlocks = systemNode.textBlocks;
|
|
81
|
+
if (!Array.isArray(rawBlocks)) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const normalized = rawBlocks
|
|
85
|
+
.map((entry) => (typeof entry === 'string' ? entry : undefined))
|
|
86
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0);
|
|
87
|
+
return normalized.length ? normalized : undefined;
|
|
88
|
+
}
|
|
28
89
|
function extractThoughtSignatureFromToolCall(tc) {
|
|
29
90
|
if (!tc || typeof tc !== 'object') {
|
|
30
91
|
return undefined;
|
|
@@ -104,18 +165,42 @@ function normalizeToolContent(value) {
|
|
|
104
165
|
return String(value ?? '');
|
|
105
166
|
}
|
|
106
167
|
}
|
|
107
|
-
function convertToolMessageToOutput(message) {
|
|
168
|
+
function convertToolMessageToOutput(message, allowedIds) {
|
|
108
169
|
const rawId = (message.tool_call_id ?? message.id);
|
|
109
170
|
const callId = typeof rawId === 'string' && rawId.trim().length ? rawId.trim() : undefined;
|
|
110
171
|
if (!callId) {
|
|
111
172
|
return null;
|
|
112
173
|
}
|
|
174
|
+
if (allowedIds && !allowedIds.has(callId)) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
113
177
|
return {
|
|
114
178
|
tool_call_id: callId,
|
|
115
179
|
content: normalizeToolContent(message.content),
|
|
116
180
|
name: typeof message.name === 'string' ? message.name : undefined
|
|
117
181
|
};
|
|
118
182
|
}
|
|
183
|
+
function selectAntigravityClaudeThinkingMessages(messages) {
|
|
184
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
185
|
+
return messages ?? [];
|
|
186
|
+
}
|
|
187
|
+
// 为了与 Responses 入口对齐,Claude-thinking 在发往 Antigravity 时仅保留
|
|
188
|
+
// 当前这一轮的 user 消息,丢弃历史 model/assistant 片段(例如错误日志中的「{」)。
|
|
189
|
+
let lastUserIndex = -1;
|
|
190
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
191
|
+
const msg = messages[i];
|
|
192
|
+
if (!msg || typeof msg !== 'object')
|
|
193
|
+
continue;
|
|
194
|
+
if (msg.role === 'user') {
|
|
195
|
+
lastUserIndex = i;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (lastUserIndex === -1) {
|
|
200
|
+
return messages;
|
|
201
|
+
}
|
|
202
|
+
return [messages[lastUserIndex]];
|
|
203
|
+
}
|
|
119
204
|
function buildFunctionResponseEntry(output) {
|
|
120
205
|
const parsedPayload = safeParseJson(output.content);
|
|
121
206
|
const normalizedPayload = ensureFunctionResponsePayload(cloneAsJsonValue(parsedPayload));
|
|
@@ -288,19 +373,46 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
288
373
|
const emittedToolOutputs = new Set();
|
|
289
374
|
const adapterContext = metadata?.context;
|
|
290
375
|
const rawProviderId = adapterContext?.providerId;
|
|
376
|
+
const entryEndpointRaw = adapterContext?.entryEndpoint;
|
|
377
|
+
const entryEndpoint = typeof entryEndpointRaw === 'string' ? entryEndpointRaw.trim().toLowerCase() : '';
|
|
378
|
+
const isAnthropicEntry = entryEndpoint === '/v1/messages';
|
|
291
379
|
const normalizedProviderId = typeof rawProviderId === 'string' ? rawProviderId.toLowerCase() : '';
|
|
292
380
|
const providerIdPrefix = normalizedProviderId.split('.')[0];
|
|
381
|
+
const isAntigravityClaudeThinking = providerIdPrefix === 'antigravity' &&
|
|
382
|
+
typeof chat.parameters?.model === 'string' &&
|
|
383
|
+
chat.parameters.model.includes('claude-sonnet-4-5-thinking');
|
|
293
384
|
// 保持对通用 gemini-cli 的保护(避免上游直接执行 functionCall),
|
|
294
385
|
// 但对于 antigravity.* 明确允许通过 Gemini functionCall 协议执行工具,
|
|
295
386
|
// 以便完整打通 tools → functionCall → functionResponse 链路。
|
|
296
387
|
const omitFunctionCallPartsForCli = providerIdPrefix === 'gemini-cli';
|
|
297
|
-
|
|
388
|
+
const semanticsNode = readGeminiSemantics(chat);
|
|
389
|
+
const systemTextBlocksFromSemantics = readSystemTextBlocksFromSemantics(chat);
|
|
390
|
+
const sourceMessages = chat.messages;
|
|
391
|
+
// 收集当前 ChatEnvelope 中 assistant/tool_calls 的 id,用于过滤孤立的 tool_result:
|
|
392
|
+
// 只有在本轮对话中存在对应 tool_call 的 tool_result 才允许映射为 Gemini functionResponse。
|
|
393
|
+
const assistantToolCallIds = new Set();
|
|
394
|
+
for (const msg of sourceMessages) {
|
|
395
|
+
if (!msg || typeof msg !== 'object')
|
|
396
|
+
continue;
|
|
397
|
+
if (msg.role !== 'assistant')
|
|
398
|
+
continue;
|
|
399
|
+
const tcs = Array.isArray(msg.tool_calls)
|
|
400
|
+
? msg.tool_calls
|
|
401
|
+
: [];
|
|
402
|
+
for (const tc of tcs) {
|
|
403
|
+
const id = typeof tc.id === 'string' ? tc.id.trim() : '';
|
|
404
|
+
if (id) {
|
|
405
|
+
assistantToolCallIds.add(id);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
for (const message of sourceMessages) {
|
|
298
410
|
if (!message || typeof message !== 'object')
|
|
299
411
|
continue;
|
|
300
412
|
if (message.role === 'system')
|
|
301
413
|
continue;
|
|
302
414
|
if (message.role === 'tool') {
|
|
303
|
-
const toolOutput = convertToolMessageToOutput(message);
|
|
415
|
+
const toolOutput = convertToolMessageToOutput(message, assistantToolCallIds);
|
|
304
416
|
if (toolOutput) {
|
|
305
417
|
contents.push(buildFunctionResponseEntry(toolOutput));
|
|
306
418
|
emittedToolOutputs.add(toolOutput.tool_call_id);
|
|
@@ -384,15 +496,21 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
384
496
|
contents
|
|
385
497
|
};
|
|
386
498
|
const geminiState = getProtocolState(metadata, 'gemini');
|
|
387
|
-
if (
|
|
499
|
+
if (semanticsNode?.systemInstruction !== undefined) {
|
|
500
|
+
request.systemInstruction = jsonClone(semanticsNode.systemInstruction);
|
|
501
|
+
}
|
|
502
|
+
else if (geminiState?.systemInstruction !== undefined) {
|
|
388
503
|
request.systemInstruction = jsonClone(geminiState.systemInstruction);
|
|
389
504
|
}
|
|
390
|
-
else
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
505
|
+
else {
|
|
506
|
+
const fallbackSystemInstructions = systemTextBlocksFromSemantics;
|
|
507
|
+
if (fallbackSystemInstructions && fallbackSystemInstructions.length) {
|
|
508
|
+
const sysBlocks = fallbackSystemInstructions
|
|
509
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
510
|
+
.map((value) => ({ text: value }));
|
|
511
|
+
if (sysBlocks.length) {
|
|
512
|
+
request.systemInstruction = { role: 'system', parts: sysBlocks };
|
|
513
|
+
}
|
|
396
514
|
}
|
|
397
515
|
}
|
|
398
516
|
if (chat.tools && chat.tools.length) {
|
|
@@ -403,17 +521,43 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
403
521
|
}
|
|
404
522
|
}
|
|
405
523
|
const generationConfig = buildGenerationConfigFromParameters(chat.parameters || {});
|
|
524
|
+
if (semanticsNode?.generationConfig && isJsonObject(semanticsNode.generationConfig)) {
|
|
525
|
+
for (const [key, value] of Object.entries(semanticsNode.generationConfig)) {
|
|
526
|
+
if (generationConfig[key] !== undefined) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
generationConfig[key] = jsonClone(value);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
406
532
|
if (Object.keys(generationConfig).length) {
|
|
407
533
|
request.generationConfig = generationConfig;
|
|
408
534
|
}
|
|
409
|
-
if (
|
|
410
|
-
request.
|
|
535
|
+
if (semanticsNode?.safetySettings !== undefined) {
|
|
536
|
+
request.safetySettings = jsonClone(semanticsNode.safetySettings);
|
|
537
|
+
}
|
|
538
|
+
if (chat.parameters?.tool_config && isJsonObject(chat.parameters.tool_config)) {
|
|
539
|
+
request.toolConfig = jsonClone(chat.parameters.tool_config);
|
|
540
|
+
}
|
|
541
|
+
else if (semanticsNode?.toolConfig && isJsonObject(semanticsNode.toolConfig)) {
|
|
542
|
+
request.toolConfig = jsonClone(semanticsNode.toolConfig);
|
|
543
|
+
}
|
|
544
|
+
// 为了保持协议解耦,只在 Gemini 自身或开放式 Chat 入口下透传 providerMetadata;
|
|
545
|
+
// 对于 Anthropic (/v1/messages) 等其它协议的入口,不再将其 metadata 整块转发给 Gemini,
|
|
546
|
+
// 避免跨协议泄漏上游专有字段。
|
|
547
|
+
if (!isAnthropicEntry) {
|
|
548
|
+
if (semanticsNode?.providerMetadata && isJsonObject(semanticsNode.providerMetadata)) {
|
|
549
|
+
request.metadata = jsonClone(semanticsNode.providerMetadata);
|
|
550
|
+
}
|
|
551
|
+
else if (metadata?.providerMetadata && isJsonObject(metadata.providerMetadata)) {
|
|
552
|
+
request.metadata = jsonClone(metadata.providerMetadata);
|
|
553
|
+
}
|
|
411
554
|
}
|
|
412
555
|
if (chat.parameters && chat.parameters.stream !== undefined) {
|
|
413
556
|
request.metadata = request.metadata ?? {};
|
|
414
557
|
request.metadata.__rcc_stream = chat.parameters.stream;
|
|
415
558
|
}
|
|
416
|
-
if (chat.metadata?.toolsFieldPresent
|
|
559
|
+
if ((chat.metadata?.toolsFieldPresent || hasExplicitEmptyToolsSemantics(chat)) &&
|
|
560
|
+
(!Array.isArray(chat.tools) || chat.tools.length === 0)) {
|
|
417
561
|
request.metadata = request.metadata ?? {};
|
|
418
562
|
request.metadata.__rcc_tools_field_present = '1';
|
|
419
563
|
}
|
|
@@ -427,8 +571,9 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
427
571
|
request.metadata[key] = value;
|
|
428
572
|
}
|
|
429
573
|
}
|
|
430
|
-
// Apply claude-thinking compat
|
|
431
|
-
// for
|
|
574
|
+
// Apply claude-thinking compat at Gemini mapping time to ensure it is always active
|
|
575
|
+
// for Claude models, regardless of compatibilityProfile wiring. Provider层负责进一步的
|
|
576
|
+
// 传输层收紧(如 session_id / generationConfig),这里不做非标裁剪。
|
|
432
577
|
const compatRequest = applyClaudeThinkingToolSchemaCompat(request, adapterContext);
|
|
433
578
|
return compatRequest;
|
|
434
579
|
}
|
|
@@ -514,16 +659,10 @@ export class GeminiSemanticMapper {
|
|
|
514
659
|
let parameters = collectParameters(payload);
|
|
515
660
|
const metadata = { context: ctx };
|
|
516
661
|
const systemSegments = collectSystemSegments(payload.systemInstruction);
|
|
517
|
-
if (systemSegments.length) {
|
|
518
|
-
metadata.systemInstructions = systemSegments;
|
|
519
|
-
}
|
|
520
662
|
if (payload.systemInstruction !== undefined) {
|
|
521
663
|
const rawSystem = jsonClone(payload.systemInstruction);
|
|
522
664
|
ensureProtocolState(metadata, 'gemini').systemInstruction = rawSystem;
|
|
523
665
|
}
|
|
524
|
-
if (payload.safetySettings) {
|
|
525
|
-
metadata.safetySettings = jsonClone(payload.safetySettings);
|
|
526
|
-
}
|
|
527
666
|
if (missing.length) {
|
|
528
667
|
metadata.missingFields = missing;
|
|
529
668
|
}
|
|
@@ -559,32 +698,64 @@ export class GeminiSemanticMapper {
|
|
|
559
698
|
parameters = { ...(parameters || {}), ...passthrough.passthrough };
|
|
560
699
|
}
|
|
561
700
|
const providerMetadataSource = passthrough.metadata ?? payload.metadata;
|
|
701
|
+
let providerMetadata;
|
|
702
|
+
let explicitEmptyTools = Array.isArray(payload.tools) && payload.tools.length === 0;
|
|
562
703
|
if (providerMetadataSource) {
|
|
563
|
-
const
|
|
704
|
+
const cloned = jsonClone(providerMetadataSource);
|
|
564
705
|
let toolsFieldPresent = false;
|
|
565
|
-
if (isJsonObject(
|
|
566
|
-
delete
|
|
567
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
568
|
-
const sentinel =
|
|
706
|
+
if (isJsonObject(cloned)) {
|
|
707
|
+
delete cloned.__rcc_stream;
|
|
708
|
+
if (Object.prototype.hasOwnProperty.call(cloned, '__rcc_tools_field_present')) {
|
|
709
|
+
const sentinel = cloned.__rcc_tools_field_present;
|
|
569
710
|
toolsFieldPresent = sentinel === '1' || sentinel === true;
|
|
570
|
-
delete
|
|
711
|
+
delete cloned.__rcc_tools_field_present;
|
|
571
712
|
}
|
|
572
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
573
|
-
delete
|
|
713
|
+
if (Object.prototype.hasOwnProperty.call(cloned, '__rcc_raw_system')) {
|
|
714
|
+
delete cloned.__rcc_raw_system;
|
|
574
715
|
}
|
|
575
716
|
}
|
|
576
717
|
if (toolsFieldPresent) {
|
|
577
718
|
metadata.toolsFieldPresent = true;
|
|
719
|
+
explicitEmptyTools = true;
|
|
578
720
|
}
|
|
721
|
+
providerMetadata = cloned;
|
|
579
722
|
metadata.providerMetadata = providerMetadata;
|
|
580
723
|
}
|
|
581
|
-
|
|
724
|
+
const chatEnvelope = {
|
|
582
725
|
messages,
|
|
583
726
|
tools,
|
|
584
727
|
toolOutputs,
|
|
585
728
|
parameters,
|
|
586
729
|
metadata
|
|
587
730
|
};
|
|
731
|
+
if (systemSegments.length) {
|
|
732
|
+
const systemNode = ensureSystemSemantics(chatEnvelope);
|
|
733
|
+
systemNode.textBlocks = systemSegments.map((segment) => segment);
|
|
734
|
+
}
|
|
735
|
+
let semanticsNode;
|
|
736
|
+
const ensureSemanticsNode = () => {
|
|
737
|
+
semanticsNode = semanticsNode ?? ensureGeminiSemanticsNode(chatEnvelope);
|
|
738
|
+
return semanticsNode;
|
|
739
|
+
};
|
|
740
|
+
if (payload.systemInstruction !== undefined) {
|
|
741
|
+
ensureSemanticsNode().systemInstruction = jsonClone(payload.systemInstruction);
|
|
742
|
+
}
|
|
743
|
+
if (payload.safetySettings) {
|
|
744
|
+
ensureSemanticsNode().safetySettings = jsonClone(payload.safetySettings);
|
|
745
|
+
}
|
|
746
|
+
if (payload.generationConfig && isJsonObject(payload.generationConfig)) {
|
|
747
|
+
ensureSemanticsNode().generationConfig = jsonClone(payload.generationConfig);
|
|
748
|
+
}
|
|
749
|
+
if (payload.toolConfig && isJsonObject(payload.toolConfig)) {
|
|
750
|
+
ensureSemanticsNode().toolConfig = jsonClone(payload.toolConfig);
|
|
751
|
+
}
|
|
752
|
+
if (providerMetadata) {
|
|
753
|
+
ensureSemanticsNode().providerMetadata = jsonClone(providerMetadata);
|
|
754
|
+
}
|
|
755
|
+
if (explicitEmptyTools) {
|
|
756
|
+
markGeminiExplicitEmptyTools(chatEnvelope);
|
|
757
|
+
}
|
|
758
|
+
return chatEnvelope;
|
|
588
759
|
}
|
|
589
760
|
async fromChat(chat, ctx) {
|
|
590
761
|
try {
|
|
@@ -2,6 +2,7 @@ import { isJsonObject, jsonClone } from '../types/json.js';
|
|
|
2
2
|
import { createBridgeActionState, runBridgeActionPipeline } from '../../shared/bridge-actions.js';
|
|
3
3
|
import { resolveBridgePolicy, resolvePolicyActions } from '../../shared/bridge-policies.js';
|
|
4
4
|
import { captureResponsesContext, buildChatRequestFromResponses, buildResponsesRequestFromChat } from '../../responses/responses-openai-bridge.js';
|
|
5
|
+
import { maybeAugmentApplyPatchErrorContent } from './chat-mapper.js';
|
|
5
6
|
const RESPONSES_PARAMETER_KEYS = [
|
|
6
7
|
'model',
|
|
7
8
|
'temperature',
|
|
@@ -46,10 +47,12 @@ function mapToolOutputs(entries, missing) {
|
|
|
46
47
|
content = String(entry.output);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
const nameValue = typeof entry.name === 'string' ? entry.name : undefined;
|
|
51
|
+
const augmented = maybeAugmentApplyPatchErrorContent(content, nameValue);
|
|
49
52
|
outputs.push({
|
|
50
53
|
tool_call_id: String(callId),
|
|
51
|
-
content,
|
|
52
|
-
name:
|
|
54
|
+
content: augmented,
|
|
55
|
+
name: nameValue
|
|
53
56
|
});
|
|
54
57
|
});
|
|
55
58
|
return outputs.length ? outputs : undefined;
|
|
@@ -205,6 +208,67 @@ function isSubmitToolOutputsEndpoint(ctx) {
|
|
|
205
208
|
const entry = typeof ctx.entryEndpoint === 'string' ? ctx.entryEndpoint.trim().toLowerCase() : '';
|
|
206
209
|
return entry === RESPONSES_SUBMIT_ENDPOINT;
|
|
207
210
|
}
|
|
211
|
+
function attachResponsesSemantics(existing, context, resume) {
|
|
212
|
+
if (!context && !resume) {
|
|
213
|
+
return existing;
|
|
214
|
+
}
|
|
215
|
+
const next = existing ? { ...existing } : {};
|
|
216
|
+
const currentNode = next.responses && isJsonObject(next.responses) ? { ...next.responses } : {};
|
|
217
|
+
if (context) {
|
|
218
|
+
currentNode.context = jsonClone(context);
|
|
219
|
+
}
|
|
220
|
+
if (resume) {
|
|
221
|
+
currentNode.resume = jsonClone(resume);
|
|
222
|
+
}
|
|
223
|
+
next.responses = currentNode;
|
|
224
|
+
return next;
|
|
225
|
+
}
|
|
226
|
+
function extractResponsesSemanticsNode(chat) {
|
|
227
|
+
if (!chat?.semantics || typeof chat.semantics !== 'object') {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
const node = chat.semantics.responses;
|
|
231
|
+
return node && isJsonObject(node) ? node : undefined;
|
|
232
|
+
}
|
|
233
|
+
function readResponsesContextFromSemantics(chat) {
|
|
234
|
+
const node = extractResponsesSemanticsNode(chat);
|
|
235
|
+
if (!node) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const contextNode = node.context;
|
|
239
|
+
if (!contextNode || !isJsonObject(contextNode)) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
return jsonClone(contextNode);
|
|
243
|
+
}
|
|
244
|
+
function readResponsesResumeFromSemantics(chat) {
|
|
245
|
+
const node = extractResponsesSemanticsNode(chat);
|
|
246
|
+
if (!node) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
const resumeNode = node.resume;
|
|
250
|
+
if (!resumeNode || !isJsonObject(resumeNode)) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
return jsonClone(resumeNode);
|
|
254
|
+
}
|
|
255
|
+
function selectResponsesContextSnapshot(chat, envelopeMetadata) {
|
|
256
|
+
const semanticsContext = readResponsesContextFromSemantics(chat);
|
|
257
|
+
const metadataContextCandidate = chat.metadata?.responsesContext;
|
|
258
|
+
const metadataContext = metadataContextCandidate && isJsonObject(metadataContextCandidate)
|
|
259
|
+
? jsonClone(metadataContextCandidate)
|
|
260
|
+
: undefined;
|
|
261
|
+
const context = semanticsContext ??
|
|
262
|
+
metadataContext ??
|
|
263
|
+
{
|
|
264
|
+
metadata: envelopeMetadata
|
|
265
|
+
};
|
|
266
|
+
const mergedMetadata = mergeMetadata(context.metadata ?? undefined, envelopeMetadata);
|
|
267
|
+
if (mergedMetadata) {
|
|
268
|
+
context.metadata = mergedMetadata;
|
|
269
|
+
}
|
|
270
|
+
return context;
|
|
271
|
+
}
|
|
208
272
|
function resolveSubmitResponseId(ctx, responsesContext) {
|
|
209
273
|
const resumeMeta = ctx.responsesResume && typeof ctx.responsesResume === 'object'
|
|
210
274
|
? ctx.responsesResume
|
|
@@ -441,25 +505,23 @@ export class ResponsesSemanticMapper {
|
|
|
441
505
|
if (responsesContext.responseFormat) {
|
|
442
506
|
metadata.responseFormat = jsonClone(responsesContext.responseFormat);
|
|
443
507
|
}
|
|
508
|
+
metadata.responsesContext = jsonClone(responsesContext);
|
|
509
|
+
const resumeNode = ctx.responsesResume && isJsonObject(ctx.responsesResume)
|
|
510
|
+
? ctx.responsesResume
|
|
511
|
+
: undefined;
|
|
512
|
+
const semantics = attachResponsesSemantics(undefined, responsesContext, resumeNode);
|
|
444
513
|
return {
|
|
445
514
|
messages,
|
|
446
515
|
tools: normalizeTools(toolsNormalized, missingFields),
|
|
447
516
|
toolOutputs,
|
|
448
517
|
parameters,
|
|
518
|
+
semantics,
|
|
449
519
|
metadata
|
|
450
520
|
};
|
|
451
521
|
}
|
|
452
522
|
async fromChat(chat, ctx) {
|
|
453
|
-
const capturedContext = chat.metadata?.responsesContext;
|
|
454
523
|
const envelopeMetadata = chat.metadata && isJsonObject(chat.metadata) ? chat.metadata : undefined;
|
|
455
|
-
const responsesContext =
|
|
456
|
-
? {
|
|
457
|
-
...capturedContext,
|
|
458
|
-
metadata: mergeMetadata(capturedContext.metadata, envelopeMetadata)
|
|
459
|
-
}
|
|
460
|
-
: {
|
|
461
|
-
metadata: envelopeMetadata
|
|
462
|
-
};
|
|
524
|
+
const responsesContext = selectResponsesContextSnapshot(chat, envelopeMetadata);
|
|
463
525
|
if (isSubmitToolOutputsEndpoint(ctx)) {
|
|
464
526
|
const submitPayload = buildSubmitToolOutputsPayload(chat, ctx, responsesContext);
|
|
465
527
|
return {
|
|
@@ -5,6 +5,7 @@ export function chatEnvelopeToStandardized(chat, options) {
|
|
|
5
5
|
const model = extractModel(parameters);
|
|
6
6
|
const messages = chat.messages.map((message) => normalizeChatMessage(message));
|
|
7
7
|
const tools = normalizeTools(chat.tools);
|
|
8
|
+
const semantics = cloneSemantics(chat.semantics);
|
|
8
9
|
const metadataCaptured = {};
|
|
9
10
|
const hubState = {};
|
|
10
11
|
if (Array.isArray(chat.metadata?.missingFields) && chat.metadata?.missingFields.length) {
|
|
@@ -35,7 +36,8 @@ export function chatEnvelopeToStandardized(chat, options) {
|
|
|
35
36
|
capturedContext: metadataCaptured,
|
|
36
37
|
requestId: options.requestId,
|
|
37
38
|
stream: parameters.stream === true
|
|
38
|
-
}
|
|
39
|
+
},
|
|
40
|
+
semantics
|
|
39
41
|
};
|
|
40
42
|
return standardized;
|
|
41
43
|
}
|
|
@@ -93,7 +95,8 @@ export function standardizedToChatEnvelope(request, options) {
|
|
|
93
95
|
messages,
|
|
94
96
|
tools,
|
|
95
97
|
parameters,
|
|
96
|
-
metadata
|
|
98
|
+
metadata,
|
|
99
|
+
semantics: cloneSemantics(request.semantics)
|
|
97
100
|
};
|
|
98
101
|
}
|
|
99
102
|
function extractModel(parameters) {
|
|
@@ -103,6 +106,12 @@ function extractModel(parameters) {
|
|
|
103
106
|
}
|
|
104
107
|
throw new Error('ChatEnvelope parameters must include model string');
|
|
105
108
|
}
|
|
109
|
+
function cloneSemantics(value) {
|
|
110
|
+
if (!value) {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
return jsonClone(value);
|
|
114
|
+
}
|
|
106
115
|
function normalizeChatMessage(message) {
|
|
107
116
|
const normalized = {
|
|
108
117
|
role: message.role,
|
|
@@ -56,11 +56,21 @@ export interface AdapterContext {
|
|
|
56
56
|
responsesResume?: JsonObject;
|
|
57
57
|
[key: string]: JsonValue;
|
|
58
58
|
}
|
|
59
|
+
export interface ChatSemantics extends JsonObject {
|
|
60
|
+
session?: JsonObject;
|
|
61
|
+
system?: JsonObject;
|
|
62
|
+
tools?: JsonObject;
|
|
63
|
+
responses?: JsonObject;
|
|
64
|
+
anthropic?: JsonObject;
|
|
65
|
+
gemini?: JsonObject;
|
|
66
|
+
providerExtras?: JsonObject;
|
|
67
|
+
}
|
|
59
68
|
export interface ChatEnvelope {
|
|
60
69
|
messages: ChatMessage[];
|
|
61
70
|
tools?: ChatToolDefinition[];
|
|
62
71
|
toolOutputs?: ChatToolOutput[];
|
|
63
72
|
parameters?: JsonObject;
|
|
73
|
+
semantics?: ChatSemantics;
|
|
64
74
|
metadata: {
|
|
65
75
|
context: AdapterContext;
|
|
66
76
|
missingFields?: MissingField[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatMessageContentPart } from './chat-envelope.js';
|
|
1
|
+
import type { ChatMessageContentPart, ChatSemantics } from './chat-envelope.js';
|
|
2
2
|
import type { JsonObject } from './json.js';
|
|
3
3
|
export type ToolChoice = 'none' | 'auto' | 'required' | {
|
|
4
4
|
type: 'function';
|
|
@@ -74,6 +74,7 @@ export interface StandardizedRequest {
|
|
|
74
74
|
tools?: StandardizedTool[];
|
|
75
75
|
parameters: StandardizedParameters;
|
|
76
76
|
metadata: StandardizedMetadata;
|
|
77
|
+
semantics?: ChatSemantics;
|
|
77
78
|
}
|
|
78
79
|
export interface ProcessedRequest extends StandardizedRequest {
|
|
79
80
|
processed: {
|
|
@@ -13,11 +13,11 @@ function cloneParameters(value) {
|
|
|
13
13
|
const cloned = {};
|
|
14
14
|
for (const [key, entry] of Object.entries(value)) {
|
|
15
15
|
// Gemini function_declarations.parameters only support a subset of JSON Schema.
|
|
16
|
-
// Drop meta/unsupported fields that cause INVALID_ARGUMENT, such as $schema/exclusiveMinimum.
|
|
16
|
+
// Drop meta/unsupported fields that cause INVALID_ARGUMENT, such as $schema/exclusiveMinimum/propertyNames.
|
|
17
17
|
if (typeof key === 'string') {
|
|
18
18
|
if (key.startsWith('$'))
|
|
19
19
|
continue;
|
|
20
|
-
if (key === 'exclusiveMinimum' || key === 'exclusiveMaximum')
|
|
20
|
+
if (key === 'exclusiveMinimum' || key === 'exclusiveMaximum' || key === 'propertyNames')
|
|
21
21
|
continue;
|
|
22
22
|
}
|
|
23
23
|
cloned[key] = cloneParameters(entry);
|
|
@@ -262,9 +262,16 @@ export function extractXMLToolCallsFromText(text) {
|
|
|
262
262
|
name = cand;
|
|
263
263
|
}
|
|
264
264
|
catch { /* ignore */ }
|
|
265
|
+
// 如果前一行抽不到合法函数名,但 arg_key 明显是 toon/command,
|
|
266
|
+
// 视为 CLI 通路下的 exec_command 工具,避免把半截 <tool_call> 当成纯文本丢弃。
|
|
267
|
+
const rawKey = (pm[1] || '').trim();
|
|
268
|
+
const normalizedKey = normalizeKey(rawKey).toLowerCase();
|
|
269
|
+
if (!name && (normalizedKey === 'toon' || normalizedKey === 'command')) {
|
|
270
|
+
name = 'exec_command';
|
|
271
|
+
}
|
|
265
272
|
if (!name)
|
|
266
273
|
continue;
|
|
267
|
-
const k = normalizeKey(
|
|
274
|
+
const k = normalizeKey(rawKey);
|
|
268
275
|
let vRaw = (pm[2] || '').trim();
|
|
269
276
|
const argsObj = {};
|
|
270
277
|
if (k) {
|
package/dist/filters/index.d.ts
CHANGED
|
@@ -133,10 +133,11 @@ export class VirtualRouterEngine {
|
|
|
133
133
|
// 自动 sticky:对需要上下文 save/restore 的 Responses 会话,强制同一个 provider.key.model。
|
|
134
134
|
// 其它协议不启用粘滞,仅显式 routing 指令才会写入 stickyTarget。
|
|
135
135
|
const providerProtocol = metadata?.providerProtocol;
|
|
136
|
+
const serverToolRequired = metadata?.serverToolRequired === true;
|
|
136
137
|
const disableSticky = metadata &&
|
|
137
138
|
typeof metadata === 'object' &&
|
|
138
139
|
metadata.disableStickyRoutes === true;
|
|
139
|
-
const shouldAutoStickyForResponses = providerProtocol === 'openai-responses' && !disableSticky;
|
|
140
|
+
const shouldAutoStickyForResponses = providerProtocol === 'openai-responses' && serverToolRequired && !disableSticky;
|
|
140
141
|
if (shouldAutoStickyForResponses) {
|
|
141
142
|
const stickyKeyForState = this.resolveStickyKey(metadata);
|
|
142
143
|
if (stickyKeyForState) {
|
|
@@ -293,14 +294,24 @@ export class VirtualRouterEngine {
|
|
|
293
294
|
return this.healthManager.getConfig();
|
|
294
295
|
}
|
|
295
296
|
resolveStickyKey(metadata) {
|
|
297
|
+
const providerProtocol = metadata.providerProtocol;
|
|
298
|
+
// 对 Responses 协议的自动粘滞,仅在“单次会话链路”内生效:
|
|
299
|
+
// - Resume/submit 调用:stickyKey = previousRequestId(指向首轮请求);
|
|
300
|
+
// - 普通 /v1/responses 调用:stickyKey = 本次 requestId;
|
|
301
|
+
// 这样不会把 Responses 的自动粘滞扩散到整个 session,仅在需要 save/restore
|
|
302
|
+
// 的请求链路中复用 provider.key.model。
|
|
303
|
+
if (providerProtocol === 'openai-responses') {
|
|
304
|
+
const resume = metadata.responsesResume;
|
|
305
|
+
if (resume && typeof resume.previousRequestId === 'string' && resume.previousRequestId.trim()) {
|
|
306
|
+
return resume.previousRequestId.trim();
|
|
307
|
+
}
|
|
308
|
+
return metadata.requestId;
|
|
309
|
+
}
|
|
310
|
+
// 其它协议沿用会话级 sticky 语义:sessionId / conversationId → requestId。
|
|
296
311
|
const sessionScope = this.resolveSessionScope(metadata);
|
|
297
312
|
if (sessionScope) {
|
|
298
313
|
return sessionScope;
|
|
299
314
|
}
|
|
300
|
-
const resume = metadata.responsesResume;
|
|
301
|
-
if (resume && typeof resume.previousRequestId === 'string' && resume.previousRequestId.trim()) {
|
|
302
|
-
return resume.previousRequestId.trim();
|
|
303
|
-
}
|
|
304
315
|
return metadata.requestId;
|
|
305
316
|
}
|
|
306
317
|
resolveSessionScope(metadata) {
|
|
@@ -49,9 +49,7 @@ const handler = async (ctx) => {
|
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
const contentRaw = message.content;
|
|
52
|
-
const contentText = typeof contentRaw === 'string'
|
|
53
|
-
? contentRaw.trim()
|
|
54
|
-
: '';
|
|
52
|
+
const contentText = typeof contentRaw === 'string' ? contentRaw.trim() : '';
|
|
55
53
|
if (contentText.length > 0) {
|
|
56
54
|
return null;
|
|
57
55
|
}
|
|
@@ -59,10 +57,35 @@ const handler = async (ctx) => {
|
|
|
59
57
|
if (toolCalls.length > 0) {
|
|
60
58
|
return null;
|
|
61
59
|
}
|
|
60
|
+
// 统计连续空回复次数,超过上限后不再自动续写,而是返回一个可重试错误。
|
|
61
|
+
const previousCountRaw = adapterRecord.geminiEmptyReplyCount;
|
|
62
|
+
const previousCount = typeof previousCountRaw === 'number' && Number.isFinite(previousCountRaw) && previousCountRaw >= 0
|
|
63
|
+
? previousCountRaw
|
|
64
|
+
: 0;
|
|
65
|
+
const nextCount = previousCount + 1;
|
|
62
66
|
const captured = getCapturedRequest(ctx.adapterContext);
|
|
63
67
|
if (!captured) {
|
|
64
68
|
return null;
|
|
65
69
|
}
|
|
70
|
+
// 超过最多 3 次空回复:返回一个 HTTP_HANDLER_ERROR 形状的错误,交由上层错误中心处理。
|
|
71
|
+
if (nextCount > 3) {
|
|
72
|
+
const errorChat = {
|
|
73
|
+
id: base.id,
|
|
74
|
+
object: base.object,
|
|
75
|
+
model: base.model,
|
|
76
|
+
error: {
|
|
77
|
+
message: 'fetch failed: gemini_empty_reply_continue exceeded max empty replies',
|
|
78
|
+
code: 'HTTP_HANDLER_ERROR',
|
|
79
|
+
type: 'servertool_empty_reply'
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
chatResponse: errorChat,
|
|
84
|
+
execution: {
|
|
85
|
+
flowId: FLOW_ID
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
66
89
|
const followupPayload = buildContinueFollowupPayload(captured);
|
|
67
90
|
if (!followupPayload) {
|
|
68
91
|
return null;
|
|
@@ -76,7 +99,8 @@ const handler = async (ctx) => {
|
|
|
76
99
|
payload: followupPayload,
|
|
77
100
|
metadata: {
|
|
78
101
|
serverToolFollowup: true,
|
|
79
|
-
stream: false
|
|
102
|
+
stream: false,
|
|
103
|
+
geminiEmptyReplyCount: nextCount
|
|
80
104
|
}
|
|
81
105
|
}
|
|
82
106
|
}
|