@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
@@ -0,0 +1,299 @@
1
+ import fs from 'node:fs/promises';
2
+ import fsSync from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { extractSessionIdentifiersFromMetadata } from './pipeline/session-identifiers.js';
6
+ const TOOL_HISTORY_ROOT = path.join(os.homedir(), '.routecodex', 'tool-history');
7
+ const TOOL_UNKNOWN_PREFIX = '[RouteCodex] Tool call result unknown';
8
+ function ensureArray(value) {
9
+ return Array.isArray(value) ? value : [];
10
+ }
11
+ function normalizeToolCallId(call) {
12
+ if (!call || typeof call !== 'object') {
13
+ return undefined;
14
+ }
15
+ const raw = call.id ??
16
+ call.tool_call_id ??
17
+ call.call_id;
18
+ if (typeof raw !== 'string') {
19
+ return undefined;
20
+ }
21
+ const trimmed = raw.trim();
22
+ return trimmed.length ? trimmed : undefined;
23
+ }
24
+ function normalizeToolMessageId(message) {
25
+ if (!message || typeof message !== 'object') {
26
+ return undefined;
27
+ }
28
+ const raw = message.tool_call_id ??
29
+ message.call_id ??
30
+ message.id;
31
+ if (typeof raw !== 'string') {
32
+ return undefined;
33
+ }
34
+ const trimmed = raw.trim();
35
+ return trimmed.length ? trimmed : undefined;
36
+ }
37
+ function findFirstNonToolIndex(messages, startIndex) {
38
+ let index = startIndex;
39
+ while (index < messages.length) {
40
+ const msg = messages[index];
41
+ if (!msg || typeof msg !== 'object') {
42
+ break;
43
+ }
44
+ const role = String(msg.role || '').toLowerCase();
45
+ if (role !== 'tool') {
46
+ break;
47
+ }
48
+ index += 1;
49
+ }
50
+ return index;
51
+ }
52
+ function findToolMessageIndex(messages, startIndex, callId) {
53
+ for (let i = startIndex; i < messages.length; i += 1) {
54
+ const msg = messages[i];
55
+ if (!msg || typeof msg !== 'object') {
56
+ continue;
57
+ }
58
+ const role = String(msg.role || '').toLowerCase();
59
+ if (role !== 'tool') {
60
+ continue;
61
+ }
62
+ const id = normalizeToolMessageId(msg);
63
+ if (id === callId) {
64
+ return i;
65
+ }
66
+ }
67
+ return -1;
68
+ }
69
+ function createUnknownToolMessage(callId, call) {
70
+ let name;
71
+ const fn = call && typeof call === 'object' ? call.function : undefined;
72
+ if (fn && typeof fn.name === 'string' && fn.name.trim().length) {
73
+ name = fn.name.trim();
74
+ }
75
+ const description = name ? `tool "${name}"` : 'tool call';
76
+ const content = `${TOOL_UNKNOWN_PREFIX}: ${description} (${callId}) did not produce a result in this session. Treat this tool as failed with unknown status.`;
77
+ const msg = {
78
+ role: 'tool',
79
+ tool_call_id: callId,
80
+ content
81
+ };
82
+ if (name) {
83
+ msg.name = name;
84
+ }
85
+ return msg;
86
+ }
87
+ function normalizeToolCallOrdering(messages) {
88
+ let index = 0;
89
+ while (index < messages.length) {
90
+ const message = messages[index];
91
+ if (!message || typeof message !== 'object') {
92
+ index += 1;
93
+ continue;
94
+ }
95
+ const role = String(message.role || '').toLowerCase();
96
+ if (role !== 'assistant') {
97
+ index += 1;
98
+ continue;
99
+ }
100
+ const toolCalls = ensureArray(message.tool_calls);
101
+ if (!toolCalls.length) {
102
+ index += 1;
103
+ continue;
104
+ }
105
+ let insertionIndex = index + 1;
106
+ for (const call of toolCalls) {
107
+ if (!call || typeof call !== 'object') {
108
+ continue;
109
+ }
110
+ const callId = normalizeToolCallId(call);
111
+ if (!callId) {
112
+ continue;
113
+ }
114
+ const existingIndex = findToolMessageIndex(messages, insertionIndex, callId);
115
+ if (existingIndex >= 0) {
116
+ if (existingIndex === insertionIndex) {
117
+ insertionIndex += 1;
118
+ continue;
119
+ }
120
+ const [relocated] = messages.splice(existingIndex, 1);
121
+ messages.splice(insertionIndex, 0, relocated);
122
+ insertionIndex += 1;
123
+ continue;
124
+ }
125
+ const placeholder = createUnknownToolMessage(callId, call);
126
+ messages.splice(insertionIndex, 0, placeholder);
127
+ insertionIndex += 1;
128
+ }
129
+ index = Math.max(index + 1, insertionIndex);
130
+ }
131
+ }
132
+ function sanitizeSessionId(raw) {
133
+ const trimmed = raw.trim();
134
+ if (!trimmed) {
135
+ return '';
136
+ }
137
+ return trimmed.replace(/[^A-Za-z0-9_.-]/g, '_');
138
+ }
139
+ async function loadSessionHistory(sessionId) {
140
+ try {
141
+ const file = path.join(TOOL_HISTORY_ROOT, `${sanitizeSessionId(sessionId)}.json`);
142
+ if (!fsSync.existsSync(file)) {
143
+ return null;
144
+ }
145
+ const text = await fs.readFile(file, 'utf-8');
146
+ const parsed = JSON.parse(text);
147
+ if (!parsed || typeof parsed !== 'object') {
148
+ return null;
149
+ }
150
+ parsed.lastMessages = Array.isArray(parsed.lastMessages) ? parsed.lastMessages : [];
151
+ parsed.pendingToolUses = parsed.pendingToolUses && typeof parsed.pendingToolUses === 'object'
152
+ ? parsed.pendingToolUses
153
+ : {};
154
+ parsed.updatedAt = typeof parsed.updatedAt === 'string' && parsed.updatedAt.trim().length
155
+ ? parsed.updatedAt
156
+ : new Date().toISOString();
157
+ return parsed;
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ async function persistSessionHistory(sessionId, history) {
164
+ try {
165
+ if (!fsSync.existsSync(TOOL_HISTORY_ROOT)) {
166
+ await fs.mkdir(TOOL_HISTORY_ROOT, { recursive: true });
167
+ }
168
+ const file = path.join(TOOL_HISTORY_ROOT, `${sanitizeSessionId(sessionId)}.json`);
169
+ const payload = JSON.stringify(history);
170
+ await fs.writeFile(file, payload, 'utf-8');
171
+ }
172
+ catch {
173
+ // history persistence must never block the main flow
174
+ }
175
+ }
176
+ function collectToolHistoryRecords(messages) {
177
+ const now = new Date().toISOString();
178
+ const records = [];
179
+ for (const msg of messages) {
180
+ if (!msg || typeof msg !== 'object') {
181
+ continue;
182
+ }
183
+ const role = String(msg.role || '').toLowerCase();
184
+ if (role === 'assistant') {
185
+ const toolCalls = ensureArray(msg.tool_calls);
186
+ for (const call of toolCalls) {
187
+ const id = normalizeToolCallId(call);
188
+ if (!id) {
189
+ continue;
190
+ }
191
+ const fn = call.function;
192
+ const name = fn && typeof fn.name === 'string' && fn.name.trim().length
193
+ ? fn.name.trim()
194
+ : undefined;
195
+ records.push({
196
+ role: 'assistant',
197
+ tool_use: { id, name },
198
+ ts: now
199
+ });
200
+ }
201
+ continue;
202
+ }
203
+ if (role === 'tool') {
204
+ const id = normalizeToolMessageId(msg);
205
+ if (!id) {
206
+ continue;
207
+ }
208
+ const rawName = msg.name;
209
+ const name = typeof rawName === 'string' && rawName.trim().length ? rawName.trim() : undefined;
210
+ const content = msg.content;
211
+ const status = typeof content === 'string' && content.startsWith(TOOL_UNKNOWN_PREFIX) ? 'unknown' : 'ok';
212
+ records.push({
213
+ role: 'tool',
214
+ tool_result: { id, name, status },
215
+ ts: now
216
+ });
217
+ }
218
+ }
219
+ return records;
220
+ }
221
+ function buildUpdatedHistory(existing, delta) {
222
+ const prevMessages = existing?.lastMessages ?? [];
223
+ const combined = [...prevMessages, ...delta];
224
+ const trimmed = combined.slice(-10);
225
+ const pending = {};
226
+ for (const entry of trimmed) {
227
+ if (entry.tool_use) {
228
+ pending[entry.tool_use.id] = {
229
+ name: entry.tool_use.name,
230
+ ts: entry.ts
231
+ };
232
+ }
233
+ if (entry.tool_result) {
234
+ delete pending[entry.tool_result.id];
235
+ }
236
+ }
237
+ return {
238
+ lastMessages: trimmed,
239
+ pendingToolUses: pending,
240
+ updatedAt: new Date().toISOString()
241
+ };
242
+ }
243
+ export async function applyToolSessionCompat(chat, ctx) {
244
+ if (!chat || !Array.isArray(chat.messages) || chat.messages.length === 0) {
245
+ return;
246
+ }
247
+ const entry = (ctx.entryEndpoint || '').toLowerCase();
248
+ if (!entry.includes('/v1/messages')) {
249
+ normalizeToolCallOrdering(chat.messages);
250
+ return;
251
+ }
252
+ normalizeToolCallOrdering(chat.messages);
253
+ const validCallIds = new Set();
254
+ for (const msg of chat.messages) {
255
+ if (!msg || typeof msg !== 'object') {
256
+ continue;
257
+ }
258
+ const role = String(msg.role || '').toLowerCase();
259
+ if (role !== 'assistant') {
260
+ continue;
261
+ }
262
+ const toolCalls = ensureArray(msg.tool_calls);
263
+ for (const call of toolCalls) {
264
+ const id = normalizeToolCallId(call);
265
+ if (id) {
266
+ validCallIds.add(id);
267
+ }
268
+ }
269
+ }
270
+ if (Array.isArray(chat.toolOutputs) && chat.toolOutputs.length) {
271
+ const filtered = chat.toolOutputs.filter((entry) => {
272
+ if (!entry || typeof entry !== 'object') {
273
+ return false;
274
+ }
275
+ const rawId = entry.tool_call_id ??
276
+ entry.call_id ??
277
+ entry.id;
278
+ if (typeof rawId !== 'string') {
279
+ return false;
280
+ }
281
+ const trimmed = rawId.trim();
282
+ return trimmed.length > 0 && validCallIds.has(trimmed);
283
+ });
284
+ chat.toolOutputs = filtered.length ? filtered : undefined;
285
+ }
286
+ const metadata = (chat.metadata || {});
287
+ const identifiers = extractSessionIdentifiersFromMetadata(metadata);
288
+ const sessionId = identifiers.sessionId;
289
+ if (!sessionId) {
290
+ return;
291
+ }
292
+ const history = await loadSessionHistory(sessionId);
293
+ const records = collectToolHistoryRecords(chat.messages);
294
+ if (!records.length && !history) {
295
+ return;
296
+ }
297
+ const updated = buildUpdatedHistory(history, records);
298
+ await persistSessionHistory(sessionId, updated);
299
+ }
@@ -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 {
@@ -48,7 +48,6 @@ export declare function buildResponsesRequestFromChat(payload: Record<string, un
48
48
  bridgeHistory?: BridgeInputBuildResult;
49
49
  systemInstruction?: string;
50
50
  }): BuildResponsesRequestResult;
51
- export declare function ensureResponsesApplyPatchArguments(input?: BridgeInputItem[]): void;
52
51
  export declare function buildResponsesPayloadFromChat(payload: unknown, context?: ResponsesRequestContext): Record<string, unknown> | unknown;
53
52
  export declare function extractRequestIdFromResponse(response: any): string | undefined;
54
53
  export { buildChatResponseFromResponses } from '../shared/responses-response-utils.js';
@@ -21,7 +21,6 @@ function isObject(v) {
21
21
  // --- Public bridge functions ---
22
22
  export function captureResponsesContext(payload, dto) {
23
23
  const preservedInput = cloneBridgeEntries(payload.input);
24
- ensureResponsesApplyPatchArguments(preservedInput);
25
24
  ensureBridgeInstructions(payload);
26
25
  const context = {
27
26
  requestId: dto?.route?.requestId,
@@ -398,76 +397,6 @@ export function buildResponsesRequestFromChat(payload, ctx, extras) {
398
397
  ensureBridgeInstructions(out);
399
398
  return { request: out, originalSystemMessages };
400
399
  }
401
- export function ensureResponsesApplyPatchArguments(input) {
402
- if (!Array.isArray(input) || !input.length) {
403
- return;
404
- }
405
- for (const entry of input) {
406
- if (!entry || typeof entry !== 'object')
407
- continue;
408
- const type = typeof entry.type === 'string' ? entry.type.toLowerCase() : '';
409
- if (type !== 'function_call')
410
- continue;
411
- const name = (typeof entry.name === 'string' && entry.name.trim()) ||
412
- (entry.function && typeof entry.function === 'object' && typeof entry.function.name === 'string' && entry.function.name.trim()) ||
413
- '';
414
- if (name !== 'apply_patch')
415
- continue;
416
- let normalized;
417
- try {
418
- normalized = normalizeApplyPatchArguments(entry.arguments ?? entry.function?.arguments);
419
- }
420
- catch {
421
- // best-effort: do not fail the whole request due to a malformed historical tool call
422
- normalized = undefined;
423
- }
424
- if (normalized === undefined) {
425
- continue;
426
- }
427
- entry.arguments = normalized;
428
- if (entry.function && typeof entry.function === 'object') {
429
- entry.function.arguments = normalized;
430
- }
431
- }
432
- }
433
- function normalizeApplyPatchArguments(source) {
434
- let parsed;
435
- if (typeof source === 'string' && source.trim()) {
436
- try {
437
- parsed = JSON.parse(source);
438
- }
439
- catch {
440
- parsed = { patch: source };
441
- }
442
- }
443
- else if (source && typeof source === 'object') {
444
- parsed = { ...source };
445
- }
446
- else if (source === undefined) {
447
- parsed = {};
448
- }
449
- else {
450
- return typeof source === 'string' ? source : undefined;
451
- }
452
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
453
- return typeof source === 'string' ? source : undefined;
454
- }
455
- const patchText = typeof parsed.patch === 'string' && parsed.patch.trim().length
456
- ? parsed.patch
457
- : typeof parsed.input === 'string' && parsed.input.trim().length
458
- ? parsed.input
459
- : undefined;
460
- if (patchText) {
461
- parsed.patch = patchText;
462
- parsed.input = patchText;
463
- }
464
- try {
465
- return JSON.stringify(parsed);
466
- }
467
- catch {
468
- return typeof source === 'string' ? source : undefined;
469
- }
470
- }
471
400
  function readToolCallIdStyleFromContext(ctx) {
472
401
  if (!ctx) {
473
402
  return undefined;
@@ -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
  }
@@ -12,6 +12,14 @@ function cloneParameters(value) {
12
12
  if (isPlainRecord(value)) {
13
13
  const cloned = {};
14
14
  for (const [key, entry] of Object.entries(value)) {
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.
17
+ if (typeof key === 'string') {
18
+ if (key.startsWith('$'))
19
+ continue;
20
+ if (key === 'exclusiveMinimum' || key === 'exclusiveMaximum')
21
+ continue;
22
+ }
15
23
  cloned[key] = cloneParameters(entry);
16
24
  }
17
25
  return cloned;
@@ -153,70 +153,50 @@ export function buildResponsesOutputFromChat(options) {
153
153
  };
154
154
  }
155
155
  function normalizeUsage(usageRaw) {
156
- if (usageRaw && typeof usageRaw === 'object') {
157
- const usage = { ...usageRaw };
158
- if (usage.input_tokens != null && usage.prompt_tokens == null) {
159
- usage.prompt_tokens = usage.input_tokens;
160
- }
161
- if (usage.output_tokens != null && usage.completion_tokens == null) {
162
- usage.completion_tokens = usage.output_tokens;
163
- }
164
- if (usage.prompt_tokens != null && usage.completion_tokens != null && usage.total_tokens == null) {
165
- const total = Number(usage.prompt_tokens) + Number(usage.completion_tokens);
166
- if (!Number.isNaN(total))
167
- usage.total_tokens = total;
168
- }
169
- try {
170
- delete usage.input_tokens;
171
- delete usage.output_tokens;
172
- }
173
- catch {
174
- /* ignore */
175
- }
176
- return usage;
156
+ if (!usageRaw || typeof usageRaw !== 'object') {
157
+ return usageRaw;
177
158
  }
178
- return usageRaw;
179
- }
180
- function extractApplyPatchArguments(rawArgs) {
181
- // Upstream Responses providers may wrap apply_patch arguments in a JSON object
182
- // (e.g. { patch: '*** Begin Patch...', input: '...' }). For Codex, the tool
183
- // expects a FREEFORM patch string obeying the unified diff grammar. Here we
184
- // best-effort extract such a patch string when available.
185
- const tryExtractFromObject = (obj) => {
186
- if (!obj || typeof obj !== 'object' || Array.isArray(obj))
187
- return null;
188
- const record = obj;
189
- const candidates = ['patch', 'input'];
190
- for (const key of candidates) {
191
- const value = record[key];
192
- if (typeof value !== 'string')
193
- continue;
194
- const trimmed = value.trimStart();
195
- if (trimmed.startsWith('*** Begin Patch')) {
196
- return trimmed;
197
- }
198
- }
199
- return null;
200
- };
201
- if (typeof rawArgs === 'string') {
202
- const trimmed = rawArgs.trimStart();
203
- if (trimmed.startsWith('*** Begin Patch')) {
204
- return trimmed;
205
- }
206
- try {
207
- const parsed = JSON.parse(rawArgs);
208
- const fromObject = tryExtractFromObject(parsed);
209
- if (fromObject) {
210
- return fromObject;
211
- }
212
- }
213
- catch {
214
- // non-JSON string that is not a patch header; leave to caller
159
+ const usage = { ...usageRaw };
160
+ // 统一 Responses 与 Chat 两种 usage 形态:
161
+ // - Responses: input_tokens / output_tokens / total_tokens
162
+ // - Chat: prompt_tokens / completion_tokens / total_tokens
163
+ const inputTokens = typeof usage.input_tokens === 'number'
164
+ ? usage.input_tokens
165
+ : typeof usage.prompt_tokens === 'number'
166
+ ? usage.prompt_tokens
167
+ : undefined;
168
+ const outputTokens = typeof usage.output_tokens === 'number'
169
+ ? usage.output_tokens
170
+ : typeof usage.completion_tokens === 'number'
171
+ ? usage.completion_tokens
172
+ : undefined;
173
+ let totalTokens = typeof usage.total_tokens === 'number'
174
+ ? usage.total_tokens
175
+ : undefined;
176
+ if (totalTokens === undefined && inputTokens !== undefined && outputTokens !== undefined) {
177
+ const total = Number(inputTokens) + Number(outputTokens);
178
+ if (!Number.isNaN(total)) {
179
+ totalTokens = total;
215
180
  }
216
- return null;
217
181
  }
218
- const fromObject = tryExtractFromObject(rawArgs);
219
- return fromObject;
182
+ // Responses 规范字段:input_tokens / output_tokens / total_tokens
183
+ if (inputTokens !== undefined) {
184
+ usage.input_tokens = inputTokens;
185
+ }
186
+ if (outputTokens !== undefined) {
187
+ usage.output_tokens = outputTokens;
188
+ }
189
+ if (totalTokens !== undefined) {
190
+ usage.total_tokens = totalTokens;
191
+ }
192
+ // 为了兼容内部统计逻辑,保留 prompt_tokens / completion_tokens 映射(如果原本没有)
193
+ if (usage.prompt_tokens == null && inputTokens !== undefined) {
194
+ usage.prompt_tokens = inputTokens;
195
+ }
196
+ if (usage.completion_tokens == null && outputTokens !== undefined) {
197
+ usage.completion_tokens = outputTokens;
198
+ }
199
+ return usage;
220
200
  }
221
201
  function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, baseCount, offset) {
222
202
  try {
@@ -228,37 +208,16 @@ function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, b
228
208
  if (!sanitized || sanitized.toLowerCase() === 'tool')
229
209
  return null;
230
210
  const rawArgs = fn?.arguments ?? call.arguments ?? {};
231
- let argsStr;
232
- if (sanitized === 'apply_patch') {
233
- const patch = extractApplyPatchArguments(rawArgs);
234
- if (patch != null) {
235
- argsStr = patch;
236
- }
237
- else if (typeof rawArgs === 'string') {
238
- argsStr = rawArgs;
239
- }
240
- else {
211
+ const argsStr = typeof rawArgs === 'string'
212
+ ? rawArgs
213
+ : (() => {
241
214
  try {
242
- argsStr = JSON.stringify(rawArgs ?? {});
215
+ return JSON.stringify(rawArgs ?? {});
243
216
  }
244
217
  catch {
245
- argsStr = '{}';
218
+ return '{}';
246
219
  }
247
- }
248
- }
249
- else {
250
- argsStr =
251
- typeof rawArgs === 'string'
252
- ? rawArgs
253
- : (() => {
254
- try {
255
- return JSON.stringify(rawArgs ?? {});
256
- }
257
- catch {
258
- return '{}';
259
- }
260
- })();
261
- }
220
+ })();
262
221
  const originalCallId = typeof call.id === 'string' && call.id.trim().length
263
222
  ? String(call.id)
264
223
  : (typeof call.call_id === 'string' && call.call_id.trim().length ? String(call.call_id) : undefined);