@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.
@@ -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
- return coerceAliasMap(contextNode?.anthropicToolNameMap);
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 injectPolicy = rawConfig.injectPolicy === 'always' || rawConfig.injectPolicy === 'selective'
407
- ? rawConfig.injectPolicy
408
- : 'selective';
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: typeof call.id === 'string' ? call.id : `call_${Math.random().toString(36).slice(2, 8)}`,
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
- if (isJsonObject(chat.metadata?.providerMetadata)) {
249
- baseRequest.metadata = jsonClone(chat.metadata?.providerMetadata);
250
- }
251
- if (chat.metadata?.toolsFieldPresent && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
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: typeof value.name === 'string' && value.name.trim().length ? value.name : undefined
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: normalizeToolContent(entry.content ?? entry.output),
189
- name: typeof entry.name === 'string' && entry.name.trim().length ? entry.name : undefined
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 applyExtraFields(body, metadata) {
229
- if (!metadata || !metadata.extraFields || !isJsonObject(metadata.extraFields)) {
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 [key, value] of Object.entries(metadata.extraFields)) {
233
- if (body[key] !== undefined) {
234
- continue;
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
- if (Array.isArray(payload.tools) && payload.tools.length === 0) {
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 ?? (chat.metadata?.toolsFieldPresent ? [] : undefined),
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
- for (const message of chat.messages) {
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 (geminiState?.systemInstruction !== undefined) {
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 if (metadata?.systemInstructions && Array.isArray(metadata.systemInstructions)) {
391
- const sysBlocks = metadata.systemInstructions
392
- .filter((value) => typeof value === 'string' && value.trim().length > 0)
393
- .map((value) => ({ text: value }));
394
- if (sysBlocks.length) {
395
- request.systemInstruction = { role: 'system', parts: sysBlocks };
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 (metadata?.providerMetadata && isJsonObject(metadata.providerMetadata)) {
410
- request.metadata = jsonClone(metadata.providerMetadata);
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 && (!Array.isArray(chat.tools) || chat.tools.length === 0)) {
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 directly at Gemini mapping time to ensure it is always active
431
- // for antigravity.*.claude-sonnet-4-5-thinking, regardless of compatibilityProfile wiring.
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 providerMetadata = jsonClone(providerMetadataSource);
704
+ const cloned = jsonClone(providerMetadataSource);
564
705
  let toolsFieldPresent = false;
565
- if (isJsonObject(providerMetadata)) {
566
- delete providerMetadata.__rcc_stream;
567
- if (Object.prototype.hasOwnProperty.call(providerMetadata, '__rcc_tools_field_present')) {
568
- const sentinel = providerMetadata.__rcc_tools_field_present;
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 providerMetadata.__rcc_tools_field_present;
711
+ delete cloned.__rcc_tools_field_present;
571
712
  }
572
- if (Object.prototype.hasOwnProperty.call(providerMetadata, '__rcc_raw_system')) {
573
- delete providerMetadata.__rcc_raw_system;
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
- return {
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: typeof entry.name === 'string' ? entry.name : undefined
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 = isJsonObject(capturedContext)
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((pm[1] || '').trim());
274
+ const k = normalizeKey(rawKey);
268
275
  let vRaw = (pm[2] || '').trim();
269
276
  const argsObj = {};
270
277
  if (k) {
@@ -1,4 +1,4 @@
1
- import { ToolFilterHints } from '../../filters/index.js';
1
+ import type { ToolFilterHints } from '../../filters/index.js';
2
2
  interface RequestFilterOptions {
3
3
  entryEndpoint?: string;
4
4
  requestId?: string;
@@ -1,3 +1,4 @@
1
+ export type { ToolFilterHints, ToolFilterDecision, ToolFilterAction } from './types.js';
1
2
  export * from './types.js';
2
3
  export * from './engine.js';
3
4
  export * from './builtin/whitelist-filter.js';
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.586",
3
+ "version": "0.6.631",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",