From 4afd8e81be59046d26780f6c1dbd0782db61719b Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:24:46 -0500 Subject: [PATCH] fix(ai): deduplicate request tools (#46306) --- packages/ai/src/route/client.ts | 4 +++- packages/ai/test/compile.test.ts | 31 ++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/route/client.ts b/packages/ai/src/route/client.ts index aebb10c78e8..f2fb5b4fa9c 100644 --- a/packages/ai/src/route/client.ts +++ b/packages/ai/src/route/client.ts @@ -446,7 +446,9 @@ export function make( const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest, options?: StreamOptions) { const original = applyCachePolicy(resolveRequestOptions(request)) - const resolved = LLMRequest.update(original, sanitizeSurrogates({ ...LLMRequest.input(original), model: undefined })) + const sanitized = LLMRequest.update(original, sanitizeSurrogates({ ...LLMRequest.input(original), model: undefined })) + const tools = [...new Map(sanitized.tools.map((tool) => [tool.name, tool])).values()] + const resolved = tools.length === sanitized.tools.length ? sanitized : LLMRequest.update(sanitized, { tools }) const route = resolved.model.route const body = yield* route.body diff --git a/packages/ai/test/compile.test.ts b/packages/ai/test/compile.test.ts index 70645c05161..d0d374c689e 100644 --- a/packages/ai/test/compile.test.ts +++ b/packages/ai/test/compile.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { Effect, Ref, Schema } from "effect" import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import { LLM, Message, ToolCallPart, mergeProviderOptions } from "../src/index.js" +import { LLM, LLMRequest, Message, ToolCallPart, ToolDefinition, mergeProviderOptions } from "../src/index.js" import { AnthropicMessages, OpenAIChat } from "../src/protocols.js" import { Auth, LLMClient } from "../src/route.js" import { compileRequest } from "../src/route/client.js" @@ -77,6 +77,35 @@ describe("request option precedence", () => { }), ) + it.effect("keeps the last tool definition for duplicate names", () => + Effect.gen(function* () { + const request = LLM.request({ + model: OpenAIChat.route.model({ id: "gpt-4o-mini" }), + prompt: "Use a tool.", + }) + const prepared = yield* compileRequest( + LLMRequest.update(request, { + tools: [ + ToolDefinition.make({ name: "lookup", description: "old", inputSchema: { type: "object" } }), + ToolDefinition.make({ name: "search", description: "search", inputSchema: { type: "object" } }), + ToolDefinition.make({ name: "lookup", description: "new", inputSchema: { type: "object" } }), + ], + }), + ) + + expect(prepared.body.tools).toEqual([ + { + type: "function", + function: { name: "lookup", description: "new", parameters: { type: "object" }, strict: false }, + }, + { + type: "function", + function: { name: "search", description: "search", parameters: { type: "object" }, strict: false }, + }, + ]) + }), + ) + it.effect("applies model HTTP defaults before request HTTP overlays", () => LLMClient.generate( LLM.request({