@jsonstudio/llms 0.6.473 → 0.6.568

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
  2. package/dist/conversion/codecs/openai-openai-codec.js +2 -1
  3. package/dist/conversion/codecs/responses-openai-codec.js +3 -2
  4. package/dist/conversion/compat/actions/claude-thinking-tools.d.ts +15 -0
  5. package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
  6. package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
  7. package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
  8. package/dist/conversion/compat/profiles/chat-gemini.json +15 -14
  9. package/dist/conversion/compat/profiles/chat-glm.json +194 -194
  10. package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
  11. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  12. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  13. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  14. package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
  15. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  16. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -1
  18. package/dist/conversion/hub/pipeline/hub-pipeline.js +40 -13
  19. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
  20. package/dist/conversion/hub/process/chat-process.js +107 -26
  21. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
  22. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +28 -10
  23. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
  24. package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
  25. package/dist/conversion/hub/tool-session-compat.js +299 -0
  26. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  27. package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
  28. package/dist/conversion/responses/responses-openai-bridge.js +0 -71
  29. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  30. package/dist/conversion/shared/args-mapping.js +11 -3
  31. package/dist/conversion/shared/gemini-tool-utils.js +8 -0
  32. package/dist/conversion/shared/responses-output-builder.js +47 -88
  33. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  34. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  35. package/dist/conversion/shared/text-markup-normalizer.js +42 -27
  36. package/dist/conversion/shared/tool-filter-pipeline.js +2 -1
  37. package/dist/conversion/shared/tool-governor.js +75 -4
  38. package/dist/conversion/shared/tool-harvester.js +43 -12
  39. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  40. package/dist/conversion/shared/tool-mapping.js +33 -13
  41. package/dist/filters/index.d.ts +1 -0
  42. package/dist/filters/index.js +1 -0
  43. package/dist/filters/special/request-toolcalls-stringify.js +5 -55
  44. package/dist/filters/special/request-tools-normalize.js +14 -23
  45. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  46. package/dist/filters/special/response-apply-patch-toon-decode.js +109 -0
  47. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  48. package/dist/filters/special/response-tool-arguments-toon-decode.js +55 -13
  49. package/dist/guidance/index.js +70 -27
  50. package/dist/router/virtual-router/bootstrap.js +10 -5
  51. package/dist/router/virtual-router/classifier.js +9 -4
  52. package/dist/router/virtual-router/engine-health.d.ts +22 -0
  53. package/dist/router/virtual-router/engine-health.js +423 -0
  54. package/dist/router/virtual-router/engine-logging.d.ts +20 -0
  55. package/dist/router/virtual-router/engine-logging.js +197 -0
  56. package/dist/router/virtual-router/engine-selection.d.ts +32 -0
  57. package/dist/router/virtual-router/engine-selection.js +649 -0
  58. package/dist/router/virtual-router/engine.d.ts +21 -14
  59. package/dist/router/virtual-router/engine.js +200 -523
  60. package/dist/router/virtual-router/message-utils.js +22 -0
  61. package/dist/router/virtual-router/routing-instructions.d.ts +8 -1
  62. package/dist/router/virtual-router/routing-instructions.js +137 -3
  63. package/dist/router/virtual-router/tool-signals.js +57 -11
  64. package/dist/router/virtual-router/types.d.ts +30 -0
  65. package/dist/router/virtual-router/types.js +1 -1
  66. package/dist/servertool/engine.js +3 -0
  67. package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
  68. package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -0
  69. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  70. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  71. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -0
  72. package/dist/servertool/handlers/stop-message-auto.js +204 -0
  73. package/dist/servertool/handlers/vision.js +105 -7
  74. package/dist/servertool/server-side-tools.d.ts +3 -0
  75. package/dist/servertool/server-side-tools.js +29 -0
  76. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +16 -0
  77. package/dist/tools/apply-patch-structured.d.ts +20 -0
  78. package/dist/tools/apply-patch-structured.js +239 -0
  79. package/dist/tools/tool-description-utils.d.ts +5 -0
  80. package/dist/tools/tool-description-utils.js +50 -0
  81. package/dist/tools/tool-registry.js +14 -5
  82. package/package.json +2 -2
@@ -15,6 +15,7 @@ import { applyGeminiWebSearchCompat } from '../../../compat/actions/gemini-web-s
15
15
  import { applyIflowWebSearchRequestTransform } from '../../../compat/actions/iflow-web-search.js';
16
16
  import { applyGlmImageContentTransform } from '../../../compat/actions/glm-image-content.js';
17
17
  import { applyGlmVisionPromptTransform } from '../../../compat/actions/glm-vision-prompt.js';
18
+ import { applyClaudeThinkingToolSchemaCompat } from '../../../compat/actions/claude-thinking-tools.js';
18
19
  const RATE_LIMIT_ERROR = 'ERR_COMPAT_RATE_LIMIT_DETECTED';
19
20
  const INTERNAL_STATE = Symbol('compat.internal_state');
20
21
  export function runRequestCompatPipeline(profileId, payload, options) {
@@ -177,6 +178,11 @@ function applyMapping(root, mapping, state) {
177
178
  replaceRoot(root, applyIflowWebSearchRequestTransform(root, state.adapterContext));
178
179
  }
179
180
  break;
181
+ case 'claude_thinking_tool_schema':
182
+ if (state.direction === 'request') {
183
+ replaceRoot(root, applyClaudeThinkingToolSchemaCompat(root, state.adapterContext));
184
+ }
185
+ break;
180
186
  case 'glm_image_content':
181
187
  if (state.direction === 'request') {
182
188
  replaceRoot(root, applyGlmImageContentTransform(root));
@@ -108,6 +108,8 @@ export type MappingInstruction = {
108
108
  action: 'gemini_web_search_request';
109
109
  } | {
110
110
  action: 'iflow_web_search_request';
111
+ } | {
112
+ action: 'claude_thinking_tool_schema';
111
113
  };
112
114
  export type FilterInstruction = {
113
115
  action: 'rate_limit_text';
@@ -1,10 +1,15 @@
1
1
  import { Readable } from 'node:stream';
2
2
  import type { StandardizedRequest, ProcessedRequest } from '../types/standardized.js';
3
3
  import type { JsonObject } from '../types/json.js';
4
- import type { VirtualRouterConfig, RoutingDecision, RoutingDiagnostics, TargetMetadata } from '../../../router/virtual-router/types.js';
4
+ import type { VirtualRouterConfig, RoutingDecision, RoutingDiagnostics, TargetMetadata, VirtualRouterHealthStore } from '../../../router/virtual-router/types.js';
5
5
  import { type HubProcessNodeResult } from '../process/chat-process.js';
6
6
  export interface HubPipelineConfig {
7
7
  virtualRouter: VirtualRouterConfig;
8
+ /**
9
+ * 可选:供 VirtualRouterEngine 使用的健康状态持久化存储。
10
+ * 当提供时,VirtualRouterEngine 将在初始化时恢复上一次快照,并在 cooldown/熔断变化时调用 persistSnapshot。
11
+ */
12
+ healthStore?: VirtualRouterHealthStore;
8
13
  }
9
14
  export interface HubPipelineRequestMetadata extends Record<string, unknown> {
10
15
  entryEndpoint?: string;
@@ -28,7 +28,9 @@ export class HubPipeline {
28
28
  unsubscribeProviderErrors;
29
29
  constructor(config) {
30
30
  this.config = config;
31
- this.routerEngine = new VirtualRouterEngine();
31
+ this.routerEngine = new VirtualRouterEngine({
32
+ healthStore: config.healthStore
33
+ });
32
34
  this.routerEngine.initialize(config.virtualRouter);
33
35
  try {
34
36
  this.unsubscribeProviderErrors = providerErrorCenter.subscribe((event) => {
@@ -122,6 +124,8 @@ export class HubPipeline {
122
124
  ? normalizedMeta.responsesResume
123
125
  : undefined;
124
126
  const stdMetadata = workingRequest?.metadata;
127
+ const hasImageAttachment = (stdMetadata?.hasImageAttachment === true || stdMetadata?.hasImageAttachment === 'true') ||
128
+ (normalizedMeta?.hasImageAttachment === true || normalizedMeta?.hasImageAttachment === 'true');
125
129
  const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
126
130
  stdMetadata?.serverToolRequired === true;
127
131
  const sessionIdentifiers = extractSessionIdentifiersFromMetadata(normalized.metadata);
@@ -240,22 +244,26 @@ export class HubPipeline {
240
244
  }
241
245
  // 为响应侧 servertool/web_search 提供一次性 Chat 请求快照,便于在 Hub 内部实现
242
246
  // 第三跳(将工具结果注入消息历史后重新调用主模型)。
247
+ //
248
+ // 注意:这里不再根据 processMode(passthrough/chat) 做分支判断——即使某些
249
+ // route 将 processMode 标记为 passthrough,我们仍然需要保留一次规范化后的
250
+ // Chat 请求快照,供 stopMessage / gemini_empty_reply_continue 等被动触发型
251
+ // servertool 在响应阶段使用。
243
252
  let capturedChatRequest;
244
- if (normalized.processMode !== 'passthrough') {
245
- try {
246
- capturedChatRequest = JSON.parse(JSON.stringify({
247
- model: workingRequest.model,
248
- messages: workingRequest.messages,
249
- tools: workingRequest.tools,
250
- parameters: workingRequest.parameters
251
- }));
252
- }
253
- catch {
254
- capturedChatRequest = undefined;
255
- }
253
+ try {
254
+ capturedChatRequest = JSON.parse(JSON.stringify({
255
+ model: workingRequest.model,
256
+ messages: workingRequest.messages,
257
+ tools: workingRequest.tools,
258
+ parameters: workingRequest.parameters
259
+ }));
260
+ }
261
+ catch {
262
+ capturedChatRequest = undefined;
256
263
  }
257
264
  const metadata = {
258
265
  ...normalized.metadata,
266
+ ...(hasImageAttachment ? { hasImageAttachment: true } : {}),
259
267
  ...(capturedChatRequest ? { capturedChatRequest } : {}),
260
268
  entryEndpoint: normalized.entryEndpoint,
261
269
  providerProtocol: outboundProtocol,
@@ -382,6 +390,25 @@ export class HubPipeline {
382
390
  adapterContext.serverToolFollowup = metadata
383
391
  .serverToolFollowup;
384
392
  }
393
+ const sessionId = typeof metadata.sessionId === 'string'
394
+ ? metadata.sessionId.trim()
395
+ : '';
396
+ if (sessionId) {
397
+ adapterContext.sessionId = sessionId;
398
+ }
399
+ const conversationId = typeof metadata.conversationId === 'string'
400
+ ? metadata.conversationId.trim()
401
+ : '';
402
+ if (conversationId) {
403
+ adapterContext.conversationId = conversationId;
404
+ }
405
+ const responsesResume = metadata.responsesResume &&
406
+ typeof metadata.responsesResume === 'object'
407
+ ? metadata.responsesResume
408
+ : undefined;
409
+ if (responsesResume) {
410
+ adapterContext.responsesResume = responsesResume;
411
+ }
385
412
  if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
386
413
  adapterContext.compatibilityProfile = target.compatibilityProfile;
387
414
  }
@@ -98,6 +98,21 @@ function collectToolOutputs(payload) {
98
98
  if (!id) {
99
99
  return;
100
100
  }
101
+ // 针对 apply_patch 工具的失败结果做醒目日志,便于监控
102
+ try {
103
+ const name = typeof entry.name === 'string' ? entry.name.trim() : undefined;
104
+ const output = typeof entry.output === 'string' ? entry.output : undefined;
105
+ if (name === 'apply_patch' &&
106
+ output &&
107
+ output.toLowerCase().includes('apply_patch verification failed')) {
108
+ const firstLine = output.split('\n')[0] ?? output;
109
+ // eslint-disable-next-line no-console
110
+ console.error(`\x1b[31m[apply_patch][tool_error] tool_call_id=${id} ${firstLine}\x1b[0m`);
111
+ }
112
+ }
113
+ catch {
114
+ // logging best-effort
115
+ }
101
116
  if (seen.has(id)) {
102
117
  return;
103
118
  }
@@ -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 { detectLastAssistantToolCategory } from '../../../router/virtual-router/tool-signals.js';
4
+ import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
4
5
  const toolGovernanceEngine = new ToolGovernanceEngine();
5
6
  export async function runHubChatProcess(options) {
6
7
  const startTime = Date.now();
@@ -72,6 +73,12 @@ async function applyRequestToolGovernance(request, context) {
72
73
  governanceTimestamp: Date.now()
73
74
  }
74
75
  };
76
+ // 清理历史图片:仅保留「最新一条 user 消息」中的图片分段,
77
+ // 避免历史对话中的图片在后续多轮工具 / 普通对话中继续作为多模态负载发给不支持图片的模型。
78
+ merged = {
79
+ ...merged,
80
+ messages: stripHistoricalImageAttachments(merged.messages)
81
+ };
75
82
  if (containsImageAttachment(merged.messages)) {
76
83
  if (!merged.metadata) {
77
84
  merged.metadata = {
@@ -205,30 +212,114 @@ function castSingleTool(tool) {
205
212
  }
206
213
  };
207
214
  }
208
- function containsImageAttachment(messages) {
209
- if (!Array.isArray(messages)) {
210
- return false;
215
+ function stripHistoricalImageAttachments(messages) {
216
+ if (!Array.isArray(messages) || !messages.length) {
217
+ return messages;
218
+ }
219
+ // 找到最新一条 user 消息,仅允许该消息保留图片分段;
220
+ // 更早的 user 消息中若存在图片,则移除其 image* 分段,保留纯文本与非图片内容。
221
+ let latestUserIndex = -1;
222
+ for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
223
+ const candidate = messages[idx];
224
+ if (candidate && typeof candidate === 'object' && candidate.role === 'user') {
225
+ latestUserIndex = idx;
226
+ break;
227
+ }
228
+ }
229
+ if (latestUserIndex < 0) {
230
+ return messages;
211
231
  }
212
- for (const message of messages) {
232
+ let changed = false;
233
+ const next = messages.slice();
234
+ for (let idx = 0; idx < messages.length; idx += 1) {
235
+ if (idx === latestUserIndex) {
236
+ continue;
237
+ }
238
+ const message = messages[idx];
213
239
  if (!message || typeof message !== 'object') {
214
240
  continue;
215
241
  }
242
+ if (message.role !== 'user') {
243
+ continue;
244
+ }
216
245
  const content = message.content;
217
- if (!Array.isArray(content)) {
246
+ if (!Array.isArray(content) || !content.length) {
218
247
  continue;
219
248
  }
249
+ const filtered = [];
250
+ let removed = false;
220
251
  for (const part of content) {
221
- if (!part || typeof part !== 'object') {
222
- continue;
223
- }
224
- const typeValue = part.type;
225
- if (typeof typeValue !== 'string') {
226
- continue;
227
- }
228
- const normalized = typeValue.toLowerCase();
229
- if (normalized.includes('image')) {
230
- return true;
252
+ if (part && typeof part === 'object' && !Array.isArray(part)) {
253
+ const typeValue = part.type;
254
+ if (typeof typeValue === 'string' && typeValue.toLowerCase().includes('image')) {
255
+ removed = true;
256
+ continue;
257
+ }
231
258
  }
259
+ filtered.push(part);
260
+ }
261
+ if (removed) {
262
+ const cloned = {
263
+ ...message,
264
+ content: filtered
265
+ };
266
+ next[idx] = cloned;
267
+ changed = true;
268
+ }
269
+ }
270
+ return changed ? next : messages;
271
+ }
272
+ function containsImageAttachment(messages) {
273
+ if (!Array.isArray(messages) || !messages.length) {
274
+ return false;
275
+ }
276
+ // 仅检查当前请求中「最新一条 user 消息」是否携带图片,避免历史对话中的图片导致后续轮次反复触发。
277
+ let latestUser;
278
+ for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
279
+ const candidate = messages[idx];
280
+ if (candidate && typeof candidate === 'object' && candidate.role === 'user') {
281
+ latestUser = candidate;
282
+ break;
283
+ }
284
+ }
285
+ if (!latestUser) {
286
+ return false;
287
+ }
288
+ const content = latestUser.content;
289
+ if (!Array.isArray(content)) {
290
+ return false;
291
+ }
292
+ for (const part of content) {
293
+ if (!part || typeof part !== 'object') {
294
+ continue;
295
+ }
296
+ const typeValue = part.type;
297
+ if (typeof typeValue !== 'string') {
298
+ continue;
299
+ }
300
+ const normalized = typeValue.toLowerCase();
301
+ if (!normalized.includes('image')) {
302
+ continue;
303
+ }
304
+ const record = part;
305
+ let imageCandidate = '';
306
+ if (typeof record.image_url === 'string') {
307
+ imageCandidate = record.image_url;
308
+ }
309
+ else if (record.image_url && typeof record.image_url.url === 'string') {
310
+ imageCandidate = record.image_url.url ?? '';
311
+ }
312
+ else if (typeof record.url === 'string') {
313
+ imageCandidate = record.url;
314
+ }
315
+ else if (typeof record.uri === 'string') {
316
+ imageCandidate = record.uri;
317
+ }
318
+ else if (typeof record.data === 'string') {
319
+ imageCandidate = record.data;
320
+ }
321
+ if (imageCandidate.trim().length > 0) {
322
+ return true;
232
323
  }
233
324
  }
234
325
  return false;
@@ -251,17 +342,7 @@ function castCustomTool(tool) {
251
342
  function: {
252
343
  name: 'apply_patch',
253
344
  description,
254
- parameters: {
255
- type: 'object',
256
- properties: {
257
- patch: {
258
- type: 'string',
259
- description: 'Unified diff patch content (FREEFORM, not JSON)'
260
- }
261
- },
262
- required: ['patch'],
263
- additionalProperties: false
264
- },
345
+ parameters: ensureApplyPatchSchema(),
265
346
  strict: true
266
347
  }
267
348
  };
@@ -200,6 +200,14 @@ export class AnthropicSemanticMapper {
200
200
  return chatEnvelope;
201
201
  }
202
202
  async fromChat(chat, ctx) {
203
+ // Ensure tool_use / tool_result ordering and per-session history for /v1/messages style entrypoints.
204
+ try {
205
+ const { applyToolSessionCompat } = await import('../tool-session-compat.js');
206
+ await applyToolSessionCompat(chat, ctx);
207
+ }
208
+ catch {
209
+ // best-effort compat; do not block outbound mapping
210
+ }
203
211
  const model = chat.parameters?.model;
204
212
  if (typeof model !== 'string' || !model.trim()) {
205
213
  throw new Error('ChatEnvelope.parameters.model is required for anthropic-messages outbound conversion');
@@ -6,6 +6,7 @@ import { encodeMetadataPassthrough, extractMetadataPassthrough } from '../../sha
6
6
  import { mapBridgeToolsToChat, mapChatToolsToBridge } from '../../shared/tool-mapping.js';
7
7
  import { prepareGeminiToolsForBridge, buildGeminiToolsFromBridge } from '../../shared/gemini-tool-utils.js';
8
8
  import { ensureProtocolState, getProtocolState } from '../../shared/protocol-state.js';
9
+ import { applyClaudeThinkingToolSchemaCompat } from '../../compat/actions/claude-thinking-tools.js';
9
10
  const GENERATION_CONFIG_KEYS = [
10
11
  { source: 'temperature', target: 'temperature' },
11
12
  { source: 'topP', target: 'top_p' },
@@ -285,6 +286,14 @@ function appendChatContentToGeminiParts(message, targetParts) {
285
286
  function buildGeminiRequestFromChat(chat, metadata) {
286
287
  const contents = [];
287
288
  const emittedToolOutputs = new Set();
289
+ const adapterContext = metadata?.context;
290
+ const rawProviderId = adapterContext?.providerId;
291
+ const normalizedProviderId = typeof rawProviderId === 'string' ? rawProviderId.toLowerCase() : '';
292
+ const providerIdPrefix = normalizedProviderId.split('.')[0];
293
+ // 保持对通用 gemini-cli 的保护(避免上游直接执行 functionCall),
294
+ // 但对于 antigravity.* 明确允许通过 Gemini functionCall 协议执行工具,
295
+ // 以便完整打通 tools → functionCall → functionResponse 链路。
296
+ const omitFunctionCallPartsForCli = providerIdPrefix === 'gemini-cli';
288
297
  for (const message of chat.messages) {
289
298
  if (!message || typeof message !== 'object')
290
299
  continue;
@@ -303,10 +312,15 @@ function buildGeminiRequestFromChat(chat, metadata) {
303
312
  parts: []
304
313
  };
305
314
  appendChatContentToGeminiParts(message, entry.parts);
306
- const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
315
+ const toolCalls = Array.isArray(message.tool_calls)
316
+ ? message.tool_calls
317
+ : [];
307
318
  for (const tc of toolCalls) {
308
319
  if (!tc || typeof tc !== 'object')
309
320
  continue;
321
+ if (omitFunctionCallPartsForCli) {
322
+ continue;
323
+ }
310
324
  const fn = tc.function || {};
311
325
  const name = typeof fn.name === 'string' ? fn.name : undefined;
312
326
  if (!name)
@@ -323,7 +337,8 @@ function buildGeminiRequestFromChat(chat, metadata) {
323
337
  else {
324
338
  argsStruct = fn.arguments ?? {};
325
339
  }
326
- const part = { functionCall: { name, args: cloneAsJsonValue(argsStruct) } };
340
+ const functionCall = { name, args: cloneAsJsonValue(argsStruct) };
341
+ const part = { functionCall };
327
342
  if (typeof tc.id === 'string') {
328
343
  part.functionCall.id = tc.id;
329
344
  }
@@ -385,13 +400,6 @@ function buildGeminiRequestFromChat(chat, metadata) {
385
400
  if (Object.keys(generationConfig).length) {
386
401
  request.generationConfig = generationConfig;
387
402
  }
388
- if (metadata?.extraFields && isJsonObject(metadata.extraFields)) {
389
- for (const [key, value] of Object.entries(metadata.extraFields)) {
390
- if (request[key] === undefined) {
391
- request[key] = value;
392
- }
393
- }
394
- }
395
403
  if (metadata?.providerMetadata && isJsonObject(metadata.providerMetadata)) {
396
404
  request.metadata = jsonClone(metadata.providerMetadata);
397
405
  }
@@ -413,7 +421,10 @@ function buildGeminiRequestFromChat(chat, metadata) {
413
421
  request.metadata[key] = value;
414
422
  }
415
423
  }
416
- return request;
424
+ // Apply claude-thinking compat directly at Gemini mapping time to ensure it is always active
425
+ // for antigravity.*.claude-sonnet-4-5-thinking, regardless of compatibilityProfile wiring.
426
+ const compatRequest = applyClaudeThinkingToolSchemaCompat(request, adapterContext);
427
+ return compatRequest;
417
428
  }
418
429
  function buildGenerationConfigFromParameters(parameters) {
419
430
  const config = {};
@@ -570,6 +581,13 @@ export class GeminiSemanticMapper {
570
581
  };
571
582
  }
572
583
  async fromChat(chat, ctx) {
584
+ try {
585
+ const { applyToolSessionCompat } = await import('../tool-session-compat.js');
586
+ await applyToolSessionCompat(chat, ctx);
587
+ }
588
+ catch {
589
+ // best-effort compat; do not block outbound mapping
590
+ }
573
591
  const envelopePayload = buildGeminiRequestFromChat(chat, chat.metadata);
574
592
  try {
575
593
  const bridgePolicy = resolveBridgePolicy({ protocol: 'gemini-chat' });
@@ -53,6 +53,41 @@ function mapToolOutputs(entries, missing) {
53
53
  });
54
54
  return outputs.length ? outputs : undefined;
55
55
  }
56
+ function deriveResumeToolOutputs(ctx) {
57
+ if (!ctx || typeof ctx !== 'object') {
58
+ return undefined;
59
+ }
60
+ const resume = ctx.responsesResume;
61
+ if (!resume || typeof resume !== 'object') {
62
+ return undefined;
63
+ }
64
+ const detailed = Array.isArray(resume.toolOutputsDetailed)
65
+ ? resume.toolOutputsDetailed
66
+ : undefined;
67
+ if (!detailed || detailed.length === 0) {
68
+ return undefined;
69
+ }
70
+ const outputs = [];
71
+ detailed.forEach((entry, index) => {
72
+ if (!entry || typeof entry !== 'object') {
73
+ return;
74
+ }
75
+ const callIdRaw = typeof entry.callId === 'string' && entry.callId.trim().length
76
+ ? entry.callId.trim()
77
+ : typeof entry.originalId === 'string' && entry.originalId.trim().length
78
+ ? entry.originalId.trim()
79
+ : `resume_tool_${index}`;
80
+ if (!callIdRaw) {
81
+ return;
82
+ }
83
+ const outputText = typeof entry.outputText === 'string' ? entry.outputText : '';
84
+ outputs.push({
85
+ tool_call_id: callIdRaw,
86
+ content: outputText
87
+ });
88
+ });
89
+ return outputs.length ? outputs : undefined;
90
+ }
56
91
  function collectParameters(payload, streamHint) {
57
92
  const params = {};
58
93
  for (const key of RESPONSES_PARAMETER_KEYS) {
@@ -169,16 +204,30 @@ export class ResponsesSemanticMapper {
169
204
  const { request, toolsNormalized } = buildChatRequestFromResponses(payload, responsesContext);
170
205
  const missingFields = [];
171
206
  const messages = normalizeMessages(request.messages, missingFields);
172
- const toolOutputs = mapToolOutputs(payload.tool_outputs, missingFields);
207
+ let toolOutputs = mapToolOutputs(payload.tool_outputs, missingFields);
208
+ if (!toolOutputs || toolOutputs.length === 0) {
209
+ const resumeToolOutputs = deriveResumeToolOutputs(ctx);
210
+ if (resumeToolOutputs && resumeToolOutputs.length) {
211
+ toolOutputs = resumeToolOutputs;
212
+ }
213
+ }
173
214
  const parameters = collectParameters(payload, responsesContext.stream);
174
215
  const metadata = { context: ctx };
175
216
  try {
176
217
  const bridgePolicy = resolveBridgePolicy({ protocol: 'openai-responses', moduleType: 'openai-responses' });
177
218
  const actions = resolvePolicyActions(bridgePolicy, 'request_inbound');
178
219
  if (actions?.length) {
220
+ const capturedToolResults = Array.isArray(toolOutputs)
221
+ ? toolOutputs.map((entry) => ({
222
+ tool_call_id: entry.tool_call_id,
223
+ output: entry.content,
224
+ name: entry.name
225
+ }))
226
+ : undefined;
179
227
  const actionState = createBridgeActionState({
180
228
  rawRequest: payload,
181
- metadata: metadata
229
+ metadata: metadata,
230
+ capturedToolResults
182
231
  });
183
232
  runBridgeActionPipeline({
184
233
  stage: 'request_inbound',
@@ -0,0 +1,26 @@
1
+ import type { ChatEnvelope } from './types/chat-envelope.js';
2
+ import type { AdapterContext } from './types/chat-envelope.js';
3
+ type ToolHistoryStatus = 'ok' | 'error' | 'unknown';
4
+ export interface ToolHistoryMessageRecord {
5
+ role: 'user' | 'assistant' | 'tool';
6
+ tool_use?: {
7
+ id: string;
8
+ name?: string;
9
+ };
10
+ tool_result?: {
11
+ id: string;
12
+ name?: string;
13
+ status: ToolHistoryStatus;
14
+ };
15
+ ts: string;
16
+ }
17
+ export interface ToolSessionHistory {
18
+ lastMessages: ToolHistoryMessageRecord[];
19
+ pendingToolUses: Record<string, {
20
+ name?: string;
21
+ ts: string;
22
+ }>;
23
+ updatedAt: string;
24
+ }
25
+ export declare function applyToolSessionCompat(chat: ChatEnvelope, ctx: AdapterContext): Promise<void>;
26
+ export {};