@jsonstudio/llms 0.6.473 → 0.6.567
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/compat/actions/claude-thinking-tools.d.ts +15 -0
- package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -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.js +15 -0
- 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 +44 -17
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +13 -8
- package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
- package/dist/conversion/hub/tool-session-compat.js +299 -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/gemini-tool-utils.js +8 -0
- package/dist/conversion/shared/responses-output-builder.js +6 -68
- package/dist/conversion/shared/tool-governor.js +75 -4
- package/dist/conversion/shared/tool-mapping.js +14 -8
- package/dist/filters/special/request-toolcalls-stringify.js +5 -55
- package/dist/filters/special/request-tools-normalize.js +0 -19
- package/dist/guidance/index.js +25 -9
- package/dist/router/virtual-router/engine-health.d.ts +11 -0
- package/dist/router/virtual-router/engine-health.js +210 -0
- package/dist/router/virtual-router/engine-logging.d.ts +19 -0
- package/dist/router/virtual-router/engine-logging.js +165 -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 +4 -13
- package/dist/router/virtual-router/engine.js +64 -535
- package/dist/router/virtual-router/message-utils.js +22 -0
- package/dist/router/virtual-router/routing-instructions.d.ts +6 -1
- package/dist/router/virtual-router/routing-instructions.js +119 -3
- 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/stop-message-auto.d.ts +1 -0
- package/dist/servertool/handlers/stop-message-auto.js +147 -0
- package/dist/servertool/handlers/vision.js +105 -7
- package/dist/servertool/server-side-tools.d.ts +2 -0
- package/dist/servertool/server-side-tools.js +2 -0
- package/dist/tools/tool-registry.js +195 -4
- package/package.json +1 -1
|
@@ -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';
|
|
@@ -122,6 +122,8 @@ export class HubPipeline {
|
|
|
122
122
|
? normalizedMeta.responsesResume
|
|
123
123
|
: undefined;
|
|
124
124
|
const stdMetadata = workingRequest?.metadata;
|
|
125
|
+
const hasImageAttachment = (stdMetadata?.hasImageAttachment === true || stdMetadata?.hasImageAttachment === 'true') ||
|
|
126
|
+
(normalizedMeta?.hasImageAttachment === true || normalizedMeta?.hasImageAttachment === 'true');
|
|
125
127
|
const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
|
|
126
128
|
stdMetadata?.serverToolRequired === true;
|
|
127
129
|
const sessionIdentifiers = extractSessionIdentifiersFromMetadata(normalized.metadata);
|
|
@@ -256,6 +258,7 @@ export class HubPipeline {
|
|
|
256
258
|
}
|
|
257
259
|
const metadata = {
|
|
258
260
|
...normalized.metadata,
|
|
261
|
+
...(hasImageAttachment ? { hasImageAttachment: true } : {}),
|
|
259
262
|
...(capturedChatRequest ? { capturedChatRequest } : {}),
|
|
260
263
|
entryEndpoint: normalized.entryEndpoint,
|
|
261
264
|
providerProtocol: outboundProtocol,
|
|
@@ -382,6 +385,18 @@ export class HubPipeline {
|
|
|
382
385
|
adapterContext.serverToolFollowup = metadata
|
|
383
386
|
.serverToolFollowup;
|
|
384
387
|
}
|
|
388
|
+
const sessionId = typeof metadata.sessionId === 'string'
|
|
389
|
+
? metadata.sessionId.trim()
|
|
390
|
+
: '';
|
|
391
|
+
if (sessionId) {
|
|
392
|
+
adapterContext.sessionId = sessionId;
|
|
393
|
+
}
|
|
394
|
+
const conversationId = typeof metadata.conversationId === 'string'
|
|
395
|
+
? metadata.conversationId.trim()
|
|
396
|
+
: '';
|
|
397
|
+
if (conversationId) {
|
|
398
|
+
adapterContext.conversationId = conversationId;
|
|
399
|
+
}
|
|
385
400
|
if (target?.compatibilityProfile && typeof target.compatibilityProfile === 'string') {
|
|
386
401
|
adapterContext.compatibilityProfile = target.compatibilityProfile;
|
|
387
402
|
}
|
package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -206,29 +206,56 @@ function castSingleTool(tool) {
|
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
208
|
function containsImageAttachment(messages) {
|
|
209
|
-
if (!Array.isArray(messages)) {
|
|
209
|
+
if (!Array.isArray(messages) || !messages.length) {
|
|
210
210
|
return false;
|
|
211
211
|
}
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
// 仅检查当前请求中「最新一条 user 消息」是否携带图片,避免历史对话中的图片导致后续轮次反复触发。
|
|
213
|
+
let latestUser;
|
|
214
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
215
|
+
const candidate = messages[idx];
|
|
216
|
+
if (candidate && typeof candidate === 'object' && candidate.role === 'user') {
|
|
217
|
+
latestUser = candidate;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!latestUser) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const content = latestUser.content;
|
|
225
|
+
if (!Array.isArray(content)) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
for (const part of content) {
|
|
229
|
+
if (!part || typeof part !== 'object') {
|
|
214
230
|
continue;
|
|
215
231
|
}
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
232
|
+
const typeValue = part.type;
|
|
233
|
+
if (typeof typeValue !== 'string') {
|
|
218
234
|
continue;
|
|
219
235
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
236
|
+
const normalized = typeValue.toLowerCase();
|
|
237
|
+
if (!normalized.includes('image')) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const record = part;
|
|
241
|
+
let imageCandidate = '';
|
|
242
|
+
if (typeof record.image_url === 'string') {
|
|
243
|
+
imageCandidate = record.image_url;
|
|
244
|
+
}
|
|
245
|
+
else if (record.image_url && typeof record.image_url.url === 'string') {
|
|
246
|
+
imageCandidate = record.image_url.url ?? '';
|
|
247
|
+
}
|
|
248
|
+
else if (typeof record.url === 'string') {
|
|
249
|
+
imageCandidate = record.url;
|
|
250
|
+
}
|
|
251
|
+
else if (typeof record.uri === 'string') {
|
|
252
|
+
imageCandidate = record.uri;
|
|
253
|
+
}
|
|
254
|
+
else if (typeof record.data === 'string') {
|
|
255
|
+
imageCandidate = record.data;
|
|
256
|
+
}
|
|
257
|
+
if (imageCandidate.trim().length > 0) {
|
|
258
|
+
return true;
|
|
232
259
|
}
|
|
233
260
|
}
|
|
234
261
|
return false;
|
|
@@ -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' },
|
|
@@ -385,13 +386,6 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
385
386
|
if (Object.keys(generationConfig).length) {
|
|
386
387
|
request.generationConfig = generationConfig;
|
|
387
388
|
}
|
|
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
389
|
if (metadata?.providerMetadata && isJsonObject(metadata.providerMetadata)) {
|
|
396
390
|
request.metadata = jsonClone(metadata.providerMetadata);
|
|
397
391
|
}
|
|
@@ -413,7 +407,11 @@ function buildGeminiRequestFromChat(chat, metadata) {
|
|
|
413
407
|
request.metadata[key] = value;
|
|
414
408
|
}
|
|
415
409
|
}
|
|
416
|
-
|
|
410
|
+
// Apply claude-thinking compat directly at Gemini mapping time to ensure it is always active
|
|
411
|
+
// for antigravity.*.claude-sonnet-4-5-thinking, regardless of compatibilityProfile wiring.
|
|
412
|
+
const adapterContext = metadata?.context;
|
|
413
|
+
const compatRequest = applyClaudeThinkingToolSchemaCompat(request, adapterContext);
|
|
414
|
+
return compatRequest;
|
|
417
415
|
}
|
|
418
416
|
function buildGenerationConfigFromParameters(parameters) {
|
|
419
417
|
const config = {};
|
|
@@ -570,6 +568,13 @@ export class GeminiSemanticMapper {
|
|
|
570
568
|
};
|
|
571
569
|
}
|
|
572
570
|
async fromChat(chat, ctx) {
|
|
571
|
+
try {
|
|
572
|
+
const { applyToolSessionCompat } = await import('../tool-session-compat.js');
|
|
573
|
+
await applyToolSessionCompat(chat, ctx);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
// best-effort compat; do not block outbound mapping
|
|
577
|
+
}
|
|
573
578
|
const envelopePayload = buildGeminiRequestFromChat(chat, chat.metadata);
|
|
574
579
|
try {
|
|
575
580
|
const bridgePolicy = resolveBridgePolicy({ protocol: 'gemini-chat' });
|
|
@@ -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 {};
|
|
@@ -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;
|
|
@@ -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;
|