@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.
- package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
- package/dist/conversion/codecs/openai-openai-codec.js +2 -1
- package/dist/conversion/codecs/responses-openai-codec.js +3 -2
- package/dist/conversion/compat/actions/claude-thinking-tools.d.ts +15 -0
- package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
- package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
- package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +15 -14
- package/dist/conversion/compat/profiles/chat-glm.json +194 -194
- package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +40 -13
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
- package/dist/conversion/hub/process/chat-process.js +107 -26
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +28 -10
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
- package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
- package/dist/conversion/hub/tool-session-compat.js +299 -0
- package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
- package/dist/conversion/responses/responses-openai-bridge.js +0 -71
- package/dist/conversion/shared/anthropic-message-utils.js +54 -0
- package/dist/conversion/shared/args-mapping.js +11 -3
- package/dist/conversion/shared/gemini-tool-utils.js +8 -0
- package/dist/conversion/shared/responses-output-builder.js +47 -88
- package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
- package/dist/conversion/shared/streaming-text-extractor.js +31 -38
- package/dist/conversion/shared/text-markup-normalizer.js +42 -27
- package/dist/conversion/shared/tool-filter-pipeline.js +2 -1
- package/dist/conversion/shared/tool-governor.js +75 -4
- package/dist/conversion/shared/tool-harvester.js +43 -12
- package/dist/conversion/shared/tool-mapping.d.ts +1 -0
- package/dist/conversion/shared/tool-mapping.js +33 -13
- package/dist/filters/index.d.ts +1 -0
- package/dist/filters/index.js +1 -0
- package/dist/filters/special/request-toolcalls-stringify.js +5 -55
- package/dist/filters/special/request-tools-normalize.js +14 -23
- package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
- package/dist/filters/special/response-apply-patch-toon-decode.js +109 -0
- package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
- package/dist/filters/special/response-tool-arguments-toon-decode.js +55 -13
- package/dist/guidance/index.js +70 -27
- package/dist/router/virtual-router/bootstrap.js +10 -5
- package/dist/router/virtual-router/classifier.js +9 -4
- package/dist/router/virtual-router/engine-health.d.ts +22 -0
- package/dist/router/virtual-router/engine-health.js +423 -0
- package/dist/router/virtual-router/engine-logging.d.ts +20 -0
- package/dist/router/virtual-router/engine-logging.js +197 -0
- package/dist/router/virtual-router/engine-selection.d.ts +32 -0
- package/dist/router/virtual-router/engine-selection.js +649 -0
- package/dist/router/virtual-router/engine.d.ts +21 -14
- package/dist/router/virtual-router/engine.js +200 -523
- package/dist/router/virtual-router/message-utils.js +22 -0
- package/dist/router/virtual-router/routing-instructions.d.ts +8 -1
- package/dist/router/virtual-router/routing-instructions.js +137 -3
- package/dist/router/virtual-router/tool-signals.js +57 -11
- package/dist/router/virtual-router/types.d.ts +30 -0
- package/dist/router/virtual-router/types.js +1 -1
- package/dist/servertool/engine.js +3 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -0
- package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
- package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
- package/dist/servertool/handlers/stop-message-auto.d.ts +1 -0
- package/dist/servertool/handlers/stop-message-auto.js +204 -0
- package/dist/servertool/handlers/vision.js +105 -7
- package/dist/servertool/server-side-tools.d.ts +3 -0
- package/dist/servertool/server-side-tools.js +29 -0
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +16 -0
- package/dist/tools/apply-patch-structured.d.ts +20 -0
- package/dist/tools/apply-patch-structured.js +239 -0
- package/dist/tools/tool-description-utils.d.ts +5 -0
- package/dist/tools/tool-description-utils.js +50 -0
- package/dist/tools/tool-registry.js +14 -5
- 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
|
+
}
|
|
@@ -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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
157
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
215
|
+
return JSON.stringify(rawArgs ?? {});
|
|
243
216
|
}
|
|
244
217
|
catch {
|
|
245
|
-
|
|
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);
|