@jsonstudio/llms 0.6.567 → 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 (55) 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/glm-history-image-trim.d.ts +2 -0
  5. package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
  6. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -1
  7. package/dist/conversion/hub/pipeline/hub-pipeline.js +25 -13
  8. package/dist/conversion/hub/process/chat-process.js +65 -11
  9. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +16 -3
  10. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
  11. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  12. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  13. package/dist/conversion/shared/args-mapping.js +11 -3
  14. package/dist/conversion/shared/responses-output-builder.js +42 -21
  15. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  16. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  17. package/dist/conversion/shared/text-markup-normalizer.js +42 -27
  18. package/dist/conversion/shared/tool-filter-pipeline.js +2 -1
  19. package/dist/conversion/shared/tool-harvester.js +43 -12
  20. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  21. package/dist/conversion/shared/tool-mapping.js +33 -19
  22. package/dist/filters/index.d.ts +1 -0
  23. package/dist/filters/index.js +1 -0
  24. package/dist/filters/special/request-tools-normalize.js +14 -4
  25. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  26. package/dist/filters/special/response-apply-patch-toon-decode.js +109 -0
  27. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  28. package/dist/filters/special/response-tool-arguments-toon-decode.js +55 -13
  29. package/dist/guidance/index.js +69 -42
  30. package/dist/router/virtual-router/bootstrap.js +10 -5
  31. package/dist/router/virtual-router/classifier.js +9 -4
  32. package/dist/router/virtual-router/engine-health.d.ts +11 -0
  33. package/dist/router/virtual-router/engine-health.js +217 -4
  34. package/dist/router/virtual-router/engine-logging.d.ts +2 -1
  35. package/dist/router/virtual-router/engine-logging.js +35 -3
  36. package/dist/router/virtual-router/engine.d.ts +17 -1
  37. package/dist/router/virtual-router/engine.js +154 -6
  38. package/dist/router/virtual-router/routing-instructions.d.ts +2 -0
  39. package/dist/router/virtual-router/routing-instructions.js +19 -1
  40. package/dist/router/virtual-router/tool-signals.js +57 -11
  41. package/dist/router/virtual-router/types.d.ts +30 -0
  42. package/dist/router/virtual-router/types.js +1 -1
  43. package/dist/servertool/engine.js +3 -0
  44. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  45. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  46. package/dist/servertool/handlers/stop-message-auto.js +61 -4
  47. package/dist/servertool/server-side-tools.d.ts +1 -0
  48. package/dist/servertool/server-side-tools.js +27 -0
  49. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +16 -0
  50. package/dist/tools/apply-patch-structured.d.ts +20 -0
  51. package/dist/tools/apply-patch-structured.js +239 -0
  52. package/dist/tools/tool-description-utils.d.ts +5 -0
  53. package/dist/tools/tool-description-utils.js +50 -0
  54. package/dist/tools/tool-registry.js +11 -193
  55. package/package.json +2 -2
@@ -360,7 +360,9 @@ export function buildOpenAIChatFromGeminiResponse(payload) {
360
360
  if (Number.isFinite(totalTokens))
361
361
  usage.total_tokens = totalTokens;
362
362
  const combinedText = textParts.join('\n');
363
- const normalized = combinedText.length ? normalizeChatMessageContent(combinedText) : { contentText: undefined, reasoningText: undefined };
363
+ const normalized = combinedText.length
364
+ ? normalizeChatMessageContent(combinedText)
365
+ : { contentText: undefined, reasoningText: undefined };
364
366
  const baseContent = normalized.contentText ?? combinedText ?? '';
365
367
  const toolResultBlock = toolResultTexts.length ? toolResultTexts.join('\n') : '';
366
368
  const finalContent = toolResultBlock && baseContent
@@ -525,16 +527,43 @@ export function buildGeminiFromOpenAIChat(chatResp) {
525
527
  let argsStruct;
526
528
  const rawArgs = fn.arguments;
527
529
  if (typeof rawArgs === 'string') {
530
+ const trimmed = rawArgs.trim();
531
+ if (trimmed.startsWith('{')) {
532
+ try {
533
+ const parsed = JSON.parse(rawArgs);
534
+ if (isObject(parsed)) {
535
+ argsStruct = parsed;
536
+ }
537
+ else {
538
+ argsStruct = { _raw: rawArgs };
539
+ }
540
+ }
541
+ catch {
542
+ argsStruct = { _raw: rawArgs };
543
+ }
544
+ }
545
+ else {
546
+ argsStruct = { _raw: rawArgs };
547
+ }
548
+ }
549
+ else if (isObject(rawArgs)) {
550
+ argsStruct = rawArgs;
551
+ }
552
+ else if (Array.isArray(rawArgs)) {
528
553
  try {
529
- argsStruct = JSON.parse(rawArgs);
554
+ argsStruct = { _raw: JSON.stringify(rawArgs) };
530
555
  }
531
556
  catch {
532
- argsStruct = { _raw: rawArgs };
557
+ argsStruct = { _raw: String(rawArgs) };
533
558
  }
534
559
  }
560
+ else if (rawArgs != null) {
561
+ argsStruct = { _raw: String(rawArgs) };
562
+ }
535
563
  else {
536
- argsStruct = rawArgs ?? {};
564
+ argsStruct = {};
537
565
  }
566
+ // Gemini request/response wire uses `args` for functionCall payload.
538
567
  const functionCall = { name, args: argsStruct };
539
568
  const id = typeof tc.id === 'string' ? String(tc.id) : undefined;
540
569
  if (id)
@@ -89,8 +89,9 @@ export class OpenAIOpenAIConversionCodec {
89
89
  // Response-side filters (idempotent w.r.t existing logic)
90
90
  engine.registerFilter(new ResponseToolTextCanonicalizeFilter()); // response_pre
91
91
  try {
92
- const { ResponseToolArgumentsToonDecodeFilter } = await import('../../filters/index.js');
92
+ const { ResponseToolArgumentsToonDecodeFilter, ResponseApplyPatchToonDecodeFilter } = await import('../../filters/index.js');
93
93
  engine.registerFilter(new ResponseToolArgumentsToonDecodeFilter()); // response_pre, runs before stringify
94
+ engine.registerFilter(new ResponseApplyPatchToonDecodeFilter()); // response_pre, runs before stringify
94
95
  }
95
96
  catch { /* optional */ }
96
97
  engine.registerFilter(new ResponseToolArgumentsStringifyFilter()); // response_post
@@ -147,11 +147,12 @@ export class ResponsesOpenAIConversionCodec {
147
147
  debug: { emit: () => { } }
148
148
  };
149
149
  const engine = new FilterEngine();
150
- // Response-side filters:文本标准化 → TOON decode(可选)→ shell/basics → finish_reason 不变式
150
+ // Response-side filters:文本标准化 → TOON decode(可选)→ apply_patch 结构化补丁规范化 → shell/basics → finish_reason 不变式
151
151
  engine.registerFilter(new ResponseToolTextCanonicalizeFilter()); // response_pre
152
152
  try {
153
- const { ResponseToolArgumentsToonDecodeFilter } = await import('../../filters/index.js');
153
+ const { ResponseToolArgumentsToonDecodeFilter, ResponseApplyPatchToonDecodeFilter } = await import('../../filters/index.js');
154
154
  engine.registerFilter(new ResponseToolArgumentsToonDecodeFilter()); // response_pre, runs before stringify
155
+ engine.registerFilter(new ResponseApplyPatchToonDecodeFilter()); // response_pre, runs before stringify
155
156
  }
156
157
  catch { /* optional */ }
157
158
  engine.registerFilter(new ResponseToolArgumentsStringifyFilter()); // response_post
@@ -0,0 +1,2 @@
1
+ import type { JsonObject } from '../../hub/types/json.js';
2
+ export declare function applyGlmHistoryImageTrim(payload: JsonObject): JsonObject;
@@ -0,0 +1,88 @@
1
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2
+ function shouldDropInlineImagePart(part) {
3
+ const rawType = typeof part.type === 'string' ? part.type.toLowerCase() : '';
4
+ if (rawType !== 'image' && rawType !== 'image_url' && rawType !== 'input_image') {
5
+ return false;
6
+ }
7
+ const imageUrlBlock = isRecord(part.image_url)
8
+ ? part.image_url
9
+ : part;
10
+ const urlRaw = typeof imageUrlBlock.url === 'string'
11
+ ? imageUrlBlock.url
12
+ : typeof imageUrlBlock.data === 'string'
13
+ ? imageUrlBlock.data
14
+ : '';
15
+ const url = urlRaw.trim();
16
+ if (!url) {
17
+ return false;
18
+ }
19
+ // GLM 4.7 在历史消息中携带 data:image/base64 时会返回 1210,
20
+ // 因此仅在历史中丢弃这类 inline image 片段。
21
+ return url.startsWith('data:image');
22
+ }
23
+ export function applyGlmHistoryImageTrim(payload) {
24
+ const root = structuredClone(payload);
25
+ const modelRaw = root.model;
26
+ const modelId = typeof modelRaw === 'string' ? modelRaw.trim().toLowerCase() : '';
27
+ if (!modelId || !modelId.startsWith('glm-4.7')) {
28
+ return root;
29
+ }
30
+ const messagesValue = root.messages;
31
+ if (!Array.isArray(messagesValue)) {
32
+ return root;
33
+ }
34
+ const messages = messagesValue.filter(msg => isRecord(msg));
35
+ if (!messages.length) {
36
+ return root;
37
+ }
38
+ // 仅在历史消息中进行裁剪:保留最后一条 user 完整内容。
39
+ let lastUserIdx = -1;
40
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
41
+ const msg = messages[i];
42
+ const role = typeof msg.role === 'string' ? msg.role.toLowerCase() : '';
43
+ if (role === 'user') {
44
+ lastUserIdx = i;
45
+ break;
46
+ }
47
+ }
48
+ if (lastUserIdx === -1) {
49
+ return root;
50
+ }
51
+ const nextMessages = [];
52
+ for (let i = 0; i < messages.length; i += 1) {
53
+ const msg = messages[i];
54
+ const role = typeof msg.role === 'string' ? msg.role.toLowerCase() : '';
55
+ if (i < lastUserIdx && role === 'user') {
56
+ const contentValue = msg.content;
57
+ if (!Array.isArray(contentValue)) {
58
+ nextMessages.push(msg);
59
+ continue;
60
+ }
61
+ const newContent = [];
62
+ for (const part of contentValue) {
63
+ if (!isRecord(part)) {
64
+ newContent.push(part);
65
+ continue;
66
+ }
67
+ if (shouldDropInlineImagePart(part)) {
68
+ // 丢弃历史中的 data:image/* 片段
69
+ // eslint-disable-next-line no-continue
70
+ continue;
71
+ }
72
+ newContent.push(part);
73
+ }
74
+ if (!newContent.length) {
75
+ // 历史消息只剩下 inline image 时,直接移除整条消息。
76
+ // 避免向 GLM 发送纯图片历史导致 1210。
77
+ // eslint-disable-next-line no-continue
78
+ continue;
79
+ }
80
+ const cloned = { ...msg, content: newContent };
81
+ nextMessages.push(cloned);
82
+ continue;
83
+ }
84
+ nextMessages.push(msg);
85
+ }
86
+ root.messages = nextMessages;
87
+ return root;
88
+ }
@@ -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) => {
@@ -242,19 +244,22 @@ export class HubPipeline {
242
244
  }
243
245
  // 为响应侧 servertool/web_search 提供一次性 Chat 请求快照,便于在 Hub 内部实现
244
246
  // 第三跳(将工具结果注入消息历史后重新调用主模型)。
247
+ //
248
+ // 注意:这里不再根据 processMode(passthrough/chat) 做分支判断——即使某些
249
+ // route 将 processMode 标记为 passthrough,我们仍然需要保留一次规范化后的
250
+ // Chat 请求快照,供 stopMessage / gemini_empty_reply_continue 等被动触发型
251
+ // servertool 在响应阶段使用。
245
252
  let capturedChatRequest;
246
- if (normalized.processMode !== 'passthrough') {
247
- try {
248
- capturedChatRequest = JSON.parse(JSON.stringify({
249
- model: workingRequest.model,
250
- messages: workingRequest.messages,
251
- tools: workingRequest.tools,
252
- parameters: workingRequest.parameters
253
- }));
254
- }
255
- catch {
256
- capturedChatRequest = undefined;
257
- }
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;
258
263
  }
259
264
  const metadata = {
260
265
  ...normalized.metadata,
@@ -397,6 +402,13 @@ export class HubPipeline {
397
402
  if (conversationId) {
398
403
  adapterContext.conversationId = conversationId;
399
404
  }
405
+ const responsesResume = metadata.responsesResume &&
406
+ typeof metadata.responsesResume === 'object'
407
+ ? metadata.responsesResume
408
+ : undefined;
409
+ if (responsesResume) {
410
+ adapterContext.responsesResume = responsesResume;
411
+ }
400
412
  if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
401
413
  adapterContext.compatibilityProfile = target.compatibilityProfile;
402
414
  }
@@ -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,6 +212,63 @@ function castSingleTool(tool) {
205
212
  }
206
213
  };
207
214
  }
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;
231
+ }
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];
239
+ if (!message || typeof message !== 'object') {
240
+ continue;
241
+ }
242
+ if (message.role !== 'user') {
243
+ continue;
244
+ }
245
+ const content = message.content;
246
+ if (!Array.isArray(content) || !content.length) {
247
+ continue;
248
+ }
249
+ const filtered = [];
250
+ let removed = false;
251
+ for (const part of content) {
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
+ }
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
+ }
208
272
  function containsImageAttachment(messages) {
209
273
  if (!Array.isArray(messages) || !messages.length) {
210
274
  return false;
@@ -278,17 +342,7 @@ function castCustomTool(tool) {
278
342
  function: {
279
343
  name: 'apply_patch',
280
344
  description,
281
- parameters: {
282
- type: 'object',
283
- properties: {
284
- patch: {
285
- type: 'string',
286
- description: 'Unified diff patch content (FREEFORM, not JSON)'
287
- }
288
- },
289
- required: ['patch'],
290
- additionalProperties: false
291
- },
345
+ parameters: ensureApplyPatchSchema(),
292
346
  strict: true
293
347
  }
294
348
  };
@@ -286,6 +286,14 @@ function appendChatContentToGeminiParts(message, targetParts) {
286
286
  function buildGeminiRequestFromChat(chat, metadata) {
287
287
  const contents = [];
288
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';
289
297
  for (const message of chat.messages) {
290
298
  if (!message || typeof message !== 'object')
291
299
  continue;
@@ -304,10 +312,15 @@ function buildGeminiRequestFromChat(chat, metadata) {
304
312
  parts: []
305
313
  };
306
314
  appendChatContentToGeminiParts(message, entry.parts);
307
- const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
315
+ const toolCalls = Array.isArray(message.tool_calls)
316
+ ? message.tool_calls
317
+ : [];
308
318
  for (const tc of toolCalls) {
309
319
  if (!tc || typeof tc !== 'object')
310
320
  continue;
321
+ if (omitFunctionCallPartsForCli) {
322
+ continue;
323
+ }
311
324
  const fn = tc.function || {};
312
325
  const name = typeof fn.name === 'string' ? fn.name : undefined;
313
326
  if (!name)
@@ -324,7 +337,8 @@ function buildGeminiRequestFromChat(chat, metadata) {
324
337
  else {
325
338
  argsStruct = fn.arguments ?? {};
326
339
  }
327
- const part = { functionCall: { name, args: cloneAsJsonValue(argsStruct) } };
340
+ const functionCall = { name, args: cloneAsJsonValue(argsStruct) };
341
+ const part = { functionCall };
328
342
  if (typeof tc.id === 'string') {
329
343
  part.functionCall.id = tc.id;
330
344
  }
@@ -409,7 +423,6 @@ function buildGeminiRequestFromChat(chat, metadata) {
409
423
  }
410
424
  // Apply claude-thinking compat directly at Gemini mapping time to ensure it is always active
411
425
  // for antigravity.*.claude-sonnet-4-5-thinking, regardless of compatibilityProfile wiring.
412
- const adapterContext = metadata?.context;
413
426
  const compatRequest = applyClaudeThinkingToolSchemaCompat(request, adapterContext);
414
427
  return compatRequest;
415
428
  }
@@ -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',
@@ -53,6 +53,7 @@ export interface AdapterContext {
53
53
  originalModelId?: string;
54
54
  clientModelId?: string;
55
55
  toolCallIdStyle?: 'fc' | 'preserve';
56
+ responsesResume?: JsonObject;
56
57
  [key: string]: JsonValue;
57
58
  }
58
59
  export interface ChatEnvelope {
@@ -647,6 +647,56 @@ function createAnthropicToolNameResolver(source) {
647
647
  return lookup.get(trimmed) ?? lookup.get(trimmed.toLowerCase()) ?? trimmed;
648
648
  };
649
649
  }
650
+ function normalizeAnthropicToolChoice(value) {
651
+ if (value === undefined || value === null) {
652
+ return undefined;
653
+ }
654
+ if (isPlainRecord(value)) {
655
+ // Already an object – best-effort clone while trimming type, and also support
656
+ // Chat-style { type: 'function', function: { name } } by mapping to Anthropic's
657
+ // { type: 'tool', name } shape.
658
+ const cloned = cloneAnthropicSchema(value);
659
+ const rawType = typeof cloned.type === 'string' ? String(cloned.type).trim() : '';
660
+ if (rawType) {
661
+ cloned.type = rawType;
662
+ return cloned;
663
+ }
664
+ const selectorType = typeof cloned.type === 'string' ? String(cloned.type).trim() : '';
665
+ const fn = cloned.function;
666
+ if (selectorType === 'function' &&
667
+ fn &&
668
+ typeof fn === 'object' &&
669
+ typeof fn.name === 'string' &&
670
+ String(fn.name).trim().length) {
671
+ return { type: 'tool', name: String(fn.name).trim() };
672
+ }
673
+ return cloned;
674
+ }
675
+ if (typeof value === 'string') {
676
+ const trimmed = value.trim();
677
+ if (!trimmed.length) {
678
+ return undefined;
679
+ }
680
+ const lower = trimmed.toLowerCase();
681
+ if (lower === 'auto') {
682
+ return { type: 'auto' };
683
+ }
684
+ if (lower === 'none') {
685
+ return { type: 'none' };
686
+ }
687
+ if (lower === 'any') {
688
+ return { type: 'any' };
689
+ }
690
+ if (lower === 'required') {
691
+ // "required" in canonical Chat roughly maps to Anthropic's "any" semantics:
692
+ // the model must choose some tool if available.
693
+ return { type: 'any' };
694
+ }
695
+ // Fallback: preserve custom mode as-is in type field.
696
+ return { type: trimmed };
697
+ }
698
+ return undefined;
699
+ }
650
700
  export function buildAnthropicRequestFromOpenAIChat(chatReq) {
651
701
  const requestBody = isObject(chatReq) ? chatReq : {};
652
702
  const model = String(requestBody?.model || 'unknown');
@@ -882,6 +932,10 @@ export function buildAnthropicRequestFromOpenAIChat(chatReq) {
882
932
  if (anthropicTools !== undefined) {
883
933
  out.tools = anthropicTools;
884
934
  }
935
+ const normalizedToolChoice = normalizeAnthropicToolChoice(requestBody.tool_choice);
936
+ if (normalizedToolChoice !== undefined) {
937
+ out.tool_choice = normalizedToolChoice;
938
+ }
885
939
  try {
886
940
  if (requestBody.metadata && typeof requestBody.metadata === 'object') {
887
941
  out.metadata = JSON.parse(JSON.stringify(requestBody.metadata));
@@ -1,4 +1,4 @@
1
- // Shared tool + argument mapping helpers (schema-driven)
1
+ import { buildShellDescription, hasApplyPatchToolDeclared, isShellToolName } from '../../tools/tool-description-utils.js';
2
2
  function isObject(v) {
3
3
  return !!v && typeof v === 'object' && !Array.isArray(v);
4
4
  }
@@ -155,6 +155,7 @@ export function normalizeTools(tools) {
155
155
  if (!Array.isArray(tools))
156
156
  return [];
157
157
  const out = [];
158
+ const applyPatchAvailable = hasApplyPatchToolDeclared(tools);
158
159
  for (const t of tools) {
159
160
  if (!t || typeof t !== 'object')
160
161
  continue;
@@ -175,7 +176,7 @@ export function normalizeTools(tools) {
175
176
  }
176
177
  // Enforce schema for known tools with minimal, compatible constraints
177
178
  let finalParams;
178
- if (typeof name === 'string' && name.trim().toLowerCase() === 'shell') {
179
+ if (isShellToolName(name)) {
179
180
  // Do NOT downgrade an existing schema; prefer string command, allow argv array as fallback
180
181
  const base = isObject(params) ? params : {};
181
182
  const props = isObject(base.properties) ? base.properties : {};
@@ -205,7 +206,14 @@ export function normalizeTools(tools) {
205
206
  else {
206
207
  finalParams = { type: 'object', properties: {}, additionalProperties: true };
207
208
  }
208
- const norm = { type: 'function', function: { name, ...(desc ? { description: desc } : {}), parameters: finalParams } };
209
+ const functionNode = { name, ...(desc ? { description: desc } : {}), parameters: finalParams };
210
+ if (isShellToolName(name)) {
211
+ const display = (typeof name === 'string' && name.trim().length > 0)
212
+ ? name.trim()
213
+ : ((typeof topName === 'string' && topName.trim().length > 0) ? topName.trim() : 'shell');
214
+ functionNode.description = buildShellDescription(display, applyPatchAvailable);
215
+ }
216
+ const norm = { type: 'function', function: functionNode };
209
217
  if (norm.function?.name)
210
218
  out.push(norm);
211
219
  }