@jsonstudio/llms 0.6.467 → 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 +1 -1
- 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 -517
- package/dist/router/virtual-router/health-manager.d.ts +23 -0
- package/dist/router/virtual-router/health-manager.js +14 -0
- 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 +129 -3
- package/dist/router/virtual-router/types.d.ts +6 -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/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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { JsonObject } from '../../hub/types/json.js';
|
|
2
|
+
import type { AdapterContext } from '../../hub/types/chat-envelope.js';
|
|
3
|
+
/**
|
|
4
|
+
* Compat for Claude models routed via antigravity on gemini-chat.
|
|
5
|
+
*
|
|
6
|
+
* Anthropic requires tools[*].custom.input_schema to be valid JSON Schema draft 2020-12.
|
|
7
|
+
* We currently send OpenAI-style parameters which may not fully conform, causing upstream
|
|
8
|
+
* invalid_request_error on tools.N.custom.input_schema.
|
|
9
|
+
*
|
|
10
|
+
* For safety, when we detect the antigravity.*.claude-* path over gemini-chat,
|
|
11
|
+
* we aggressively simplify Gemini functionDeclarations[*].parameters to a minimal
|
|
12
|
+
* but valid object schema, letting RouteCodex govern tool semantics while keeping
|
|
13
|
+
* Anthropic's schema validator happy.
|
|
14
|
+
*/
|
|
15
|
+
export declare function applyClaudeThinkingToolSchemaCompat(payload: JsonObject, adapterContext?: AdapterContext): JsonObject;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2
|
+
/**
|
|
3
|
+
* Compat for Claude models routed via antigravity on gemini-chat.
|
|
4
|
+
*
|
|
5
|
+
* Anthropic requires tools[*].custom.input_schema to be valid JSON Schema draft 2020-12.
|
|
6
|
+
* We currently send OpenAI-style parameters which may not fully conform, causing upstream
|
|
7
|
+
* invalid_request_error on tools.N.custom.input_schema.
|
|
8
|
+
*
|
|
9
|
+
* For safety, when we detect the antigravity.*.claude-* path over gemini-chat,
|
|
10
|
+
* we aggressively simplify Gemini functionDeclarations[*].parameters to a minimal
|
|
11
|
+
* but valid object schema, letting RouteCodex govern tool semantics while keeping
|
|
12
|
+
* Anthropic's schema validator happy.
|
|
13
|
+
*/
|
|
14
|
+
export function applyClaudeThinkingToolSchemaCompat(payload, adapterContext) {
|
|
15
|
+
const modelRaw = payload.model;
|
|
16
|
+
const modelId = typeof modelRaw === 'string' ? modelRaw.trim() : '';
|
|
17
|
+
// Only apply on Claude models.
|
|
18
|
+
// Upstream Anthropic enforces strict JSON Schema 2020-12 on custom.input_schema for these models.
|
|
19
|
+
if (!modelId.startsWith('claude-')) {
|
|
20
|
+
return payload;
|
|
21
|
+
}
|
|
22
|
+
const root = structuredClone(payload);
|
|
23
|
+
// Support both shapes:
|
|
24
|
+
// - Provider envelope: { model, request: { tools, ... } }
|
|
25
|
+
// - Gemini mapper request: { model, tools, ... }
|
|
26
|
+
const requestNode = isRecord(root.request)
|
|
27
|
+
? root.request
|
|
28
|
+
: root;
|
|
29
|
+
const toolsRaw = requestNode.tools;
|
|
30
|
+
if (!Array.isArray(toolsRaw)) {
|
|
31
|
+
return root;
|
|
32
|
+
}
|
|
33
|
+
const nextTools = [];
|
|
34
|
+
for (const entry of toolsRaw) {
|
|
35
|
+
if (!isRecord(entry)) {
|
|
36
|
+
nextTools.push(entry);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const decls = Array.isArray(entry.functionDeclarations)
|
|
40
|
+
? entry.functionDeclarations
|
|
41
|
+
: undefined;
|
|
42
|
+
if (!decls || !decls.length) {
|
|
43
|
+
// Non functionDeclarations-based tools (e.g. googleSearch) are left as-is.
|
|
44
|
+
nextTools.push(entry);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const nextDecls = [];
|
|
48
|
+
for (const fn of decls) {
|
|
49
|
+
if (!isRecord(fn)) {
|
|
50
|
+
nextDecls.push(fn);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const fnCopy = { ...fn };
|
|
54
|
+
// Replace parameters with a minimal, always-valid object schema.
|
|
55
|
+
fnCopy.parameters = {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {},
|
|
58
|
+
additionalProperties: true
|
|
59
|
+
};
|
|
60
|
+
// Drop strict flag to avoid upstream schema incompatibilities.
|
|
61
|
+
if (Object.prototype.hasOwnProperty.call(fnCopy, 'strict')) {
|
|
62
|
+
delete fnCopy.strict;
|
|
63
|
+
}
|
|
64
|
+
nextDecls.push(fnCopy);
|
|
65
|
+
}
|
|
66
|
+
nextTools.push({
|
|
67
|
+
functionDeclarations: nextDecls
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
requestNode.tools = nextTools;
|
|
71
|
+
return root;
|
|
72
|
+
}
|
|
@@ -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';
|