@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.
Files changed (45) hide show
  1. package/dist/conversion/compat/actions/claude-thinking-tools.d.ts +15 -0
  2. package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
  3. package/dist/conversion/compat/profiles/chat-gemini.json +1 -1
  4. package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
  5. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  6. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  7. package/dist/conversion/hub/pipeline/hub-pipeline.js +15 -0
  8. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
  9. package/dist/conversion/hub/process/chat-process.js +44 -17
  10. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
  11. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +13 -8
  12. package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
  13. package/dist/conversion/hub/tool-session-compat.js +299 -0
  14. package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
  15. package/dist/conversion/responses/responses-openai-bridge.js +0 -71
  16. package/dist/conversion/shared/gemini-tool-utils.js +8 -0
  17. package/dist/conversion/shared/responses-output-builder.js +6 -68
  18. package/dist/conversion/shared/tool-governor.js +75 -4
  19. package/dist/conversion/shared/tool-mapping.js +14 -8
  20. package/dist/filters/special/request-toolcalls-stringify.js +5 -55
  21. package/dist/filters/special/request-tools-normalize.js +0 -19
  22. package/dist/guidance/index.js +25 -9
  23. package/dist/router/virtual-router/engine-health.d.ts +11 -0
  24. package/dist/router/virtual-router/engine-health.js +210 -0
  25. package/dist/router/virtual-router/engine-logging.d.ts +19 -0
  26. package/dist/router/virtual-router/engine-logging.js +165 -0
  27. package/dist/router/virtual-router/engine-selection.d.ts +32 -0
  28. package/dist/router/virtual-router/engine-selection.js +649 -0
  29. package/dist/router/virtual-router/engine.d.ts +4 -13
  30. package/dist/router/virtual-router/engine.js +64 -517
  31. package/dist/router/virtual-router/health-manager.d.ts +23 -0
  32. package/dist/router/virtual-router/health-manager.js +14 -0
  33. package/dist/router/virtual-router/message-utils.js +22 -0
  34. package/dist/router/virtual-router/routing-instructions.d.ts +6 -1
  35. package/dist/router/virtual-router/routing-instructions.js +129 -3
  36. package/dist/router/virtual-router/types.d.ts +6 -0
  37. package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
  38. package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -0
  39. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -0
  40. package/dist/servertool/handlers/stop-message-auto.js +147 -0
  41. package/dist/servertool/handlers/vision.js +105 -7
  42. package/dist/servertool/server-side-tools.d.ts +2 -0
  43. package/dist/servertool/server-side-tools.js +2 -0
  44. package/dist/tools/tool-registry.js +195 -4
  45. 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
+ }
@@ -4,6 +4,7 @@
4
4
  "request": {
5
5
  "mappings": [
6
6
  { "action": "snapshot", "phase": "compat-pre" },
7
+ { "action": "claude_thinking_tool_schema" },
7
8
  {
8
9
  "action": "gemini_web_search_request"
9
10
  },
@@ -14,4 +15,3 @@
14
15
  "mappings": []
15
16
  }
16
17
  }
17
-
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "responses:output2choices-test",
3
+ "protocol": "openai-responses",
4
+ "response": {
5
+ "mappings": [
6
+ {
7
+ "action": "convert_responses_output_to_choices"
8
+ }
9
+ ]
10
+ }
11
+ }
12
+
@@ -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
  }
@@ -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
- for (const message of messages) {
213
- if (!message || typeof message !== 'object') {
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 content = message.content;
217
- if (!Array.isArray(content)) {
232
+ const typeValue = part.type;
233
+ if (typeof typeValue !== 'string') {
218
234
  continue;
219
235
  }
220
- for (const part of content) {
221
- if (!part || typeof part !== 'object') {
222
- continue;
223
- }
224
- const typeValue = part.type;
225
- if (typeof typeValue !== 'string') {
226
- continue;
227
- }
228
- const normalized = typeValue.toLowerCase();
229
- if (normalized.includes('image')) {
230
- return true;
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
- return request;
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';