diff --git a/lib/deduplicator.ts b/lib/deduplicator.ts index 1d64940..35d7c09 100644 --- a/lib/deduplicator.ts +++ b/lib/deduplicator.ts @@ -18,9 +18,10 @@ export function detectDuplicates( ): DuplicateDetectionResult { const signatureMap = new Map() + const protectedToolsLower = protectedTools.map(t => t.toLowerCase()) const deduplicatableIds = unprunedToolCallIds.filter(id => { const metadata = toolMetadata.get(id) - return !metadata || !protectedTools.includes(metadata.tool) + return !metadata || !protectedToolsLower.includes(metadata.tool.toLowerCase()) }) for (const id of deduplicatableIds) { diff --git a/lib/fetch-wrapper/index.ts b/lib/fetch-wrapper/index.ts index 75fd7c8..bbfecff 100644 --- a/lib/fetch-wrapper/index.ts +++ b/lib/fetch-wrapper/index.ts @@ -53,6 +53,8 @@ export function installFetchWrapper( const body = JSON.parse(init.body) const inputUrl = typeof input === 'string' ? input : 'URL object' let modified = false + // Track tool IDs cached from this request for session-scoped deduplication + const cachedToolIds: string[] = [] // Try each format handler in order // OpenAI Chat Completions & Anthropic style (body.messages) @@ -61,6 +63,9 @@ export function installFetchWrapper( if (result.modified) { modified = true } + if (result.cachedToolIds) { + cachedToolIds.push(...result.cachedToolIds) + } } // Google/Gemini style (body.contents) @@ -77,15 +82,17 @@ export function installFetchWrapper( if (result.modified) { modified = true } + if (result.cachedToolIds) { + cachedToolIds.push(...result.cachedToolIds) + } } - // Run deduplication after handlers have populated toolParameters cache + // Run deduplication only on tool IDs from the current request (session-scoped) const sessionId = state.lastSeenSessionId - if (sessionId && state.toolParameters.size > 0) { - const toolIds = Array.from(state.toolParameters.keys()) + if (sessionId && cachedToolIds.length > 1) { const alreadyPruned = state.prunedIds.get(sessionId) ?? [] const alreadyPrunedLower = new Set(alreadyPruned.map(id => id.toLowerCase())) - const unpruned = toolIds.filter(id => !alreadyPrunedLower.has(id.toLowerCase())) + const unpruned = cachedToolIds.filter(id => !alreadyPrunedLower.has(id.toLowerCase())) if (unpruned.length > 1) { const { duplicateIds } = detectDuplicates(state.toolParameters, unpruned, config.protectedTools) if (duplicateIds.length > 0) { diff --git a/lib/fetch-wrapper/openai-chat.ts b/lib/fetch-wrapper/openai-chat.ts index 9aeb6d0..2b19e21 100644 --- a/lib/fetch-wrapper/openai-chat.ts +++ b/lib/fetch-wrapper/openai-chat.ts @@ -21,8 +21,8 @@ export async function handleOpenAIChatAndAnthropic( return { modified: false, body } } - // Cache tool parameters from messages - cacheToolParametersFromMessages(body.messages, ctx.state) + // Cache tool parameters from messages and track which IDs were cached + const cachedToolIds = cacheToolParametersFromMessages(body.messages, ctx.state) let modified = false @@ -64,7 +64,7 @@ export async function handleOpenAIChatAndAnthropic( const { allSessions, allPrunedIds } = await getAllPrunedIds(ctx.client, ctx.state, ctx.logger) if (toolMessages.length === 0 || allPrunedIds.size === 0) { - return { modified, body } + return { modified, body, cachedToolIds } } let replacedCount = 0 @@ -125,8 +125,8 @@ export async function handleOpenAIChatAndAnthropic( ) } - return { modified: true, body } + return { modified: true, body, cachedToolIds } } - return { modified, body } + return { modified, body, cachedToolIds } } diff --git a/lib/fetch-wrapper/openai-responses.ts b/lib/fetch-wrapper/openai-responses.ts index 7741617..7f286d2 100644 --- a/lib/fetch-wrapper/openai-responses.ts +++ b/lib/fetch-wrapper/openai-responses.ts @@ -21,8 +21,8 @@ export async function handleOpenAIResponses( return { modified: false, body } } - // Cache tool parameters from input - cacheToolParametersFromInput(body.input, ctx.state) + // Cache tool parameters from input and track which IDs were cached + const cachedToolIds = cacheToolParametersFromInput(body.input, ctx.state) let modified = false @@ -52,13 +52,13 @@ export async function handleOpenAIResponses( const functionOutputs = body.input.filter((item: any) => item.type === 'function_call_output') if (functionOutputs.length === 0) { - return { modified, body } + return { modified, body, cachedToolIds } } const { allSessions, allPrunedIds } = await getAllPrunedIds(ctx.client, ctx.state, ctx.logger) if (allPrunedIds.size === 0) { - return { modified, body } + return { modified, body, cachedToolIds } } let replacedCount = 0 @@ -99,8 +99,8 @@ export async function handleOpenAIResponses( ) } - return { modified: true, body } + return { modified: true, body, cachedToolIds } } - return { modified, body } + return { modified, body, cachedToolIds } } diff --git a/lib/fetch-wrapper/types.ts b/lib/fetch-wrapper/types.ts index f23baf9..65d91f3 100644 --- a/lib/fetch-wrapper/types.ts +++ b/lib/fetch-wrapper/types.ts @@ -28,6 +28,8 @@ export interface FetchHandlerResult { modified: boolean /** The potentially modified body object */ body: any + /** Tool call IDs that were cached from this request (for session-scoped deduplication) */ + cachedToolIds?: string[] } /** Session data returned from getAllPrunedIds */ diff --git a/lib/tool-cache.ts b/lib/tool-cache.ts index 669fa0f..1be9b6f 100644 --- a/lib/tool-cache.ts +++ b/lib/tool-cache.ts @@ -1,13 +1,32 @@ import type { PluginState } from "./state" +/** Maximum number of tool parameters to cache to prevent unbounded memory growth */ +const MAX_TOOL_PARAMETERS_CACHE_SIZE = 500 + +/** + * Ensures the toolParameters cache doesn't exceed the maximum size. + * Removes oldest entries (first inserted) when limit is exceeded. + */ +function trimToolParametersCache(state: PluginState): void { + if (state.toolParameters.size > MAX_TOOL_PARAMETERS_CACHE_SIZE) { + const excess = state.toolParameters.size - MAX_TOOL_PARAMETERS_CACHE_SIZE + const keys = Array.from(state.toolParameters.keys()) + for (let i = 0; i < excess; i++) { + state.toolParameters.delete(keys[i]) + } + } +} + /** * Cache tool parameters from OpenAI Chat Completions style messages. * Extracts tool call IDs and their parameters from assistant messages with tool_calls. + * Returns the list of tool call IDs that were cached from this request. */ export function cacheToolParametersFromMessages( messages: any[], state: PluginState -): void { +): string[] { + const cachedIds: string[] = [] for (const message of messages) { if (message.role !== 'assistant' || !Array.isArray(message.tool_calls)) { continue @@ -26,21 +45,26 @@ export function cacheToolParametersFromMessages( tool: toolCall.function.name, parameters: params }) + cachedIds.push(toolCall.id) } catch (error) { // Silently ignore parse errors } } } + trimToolParametersCache(state) + return cachedIds } /** * Cache tool parameters from OpenAI Responses API format. * Extracts from input array items with type='function_call'. + * Returns the list of tool call IDs that were cached from this request. */ export function cacheToolParametersFromInput( input: any[], state: PluginState -): void { +): string[] { + const cachedIds: string[] = [] for (const item of input) { if (item.type !== 'function_call' || !item.call_id || !item.name) { continue @@ -54,8 +78,11 @@ export function cacheToolParametersFromInput( tool: item.name, parameters: params }) + cachedIds.push(item.call_id) } catch (error) { // Silently ignore parse errors } } + trimToolParametersCache(state) + return cachedIds }