|
|
@@ -384,4 +384,347 @@ describe("AnthropicHandler", () => {
|
|
|
expect(calledMessages.every((m: any) => m.role === "user")).toBe(true)
|
|
|
})
|
|
|
})
|
|
|
+
|
|
|
+ describe("native tool calling", () => {
|
|
|
+ const systemPrompt = "You are a helpful assistant."
|
|
|
+ const messages: Anthropic.Messages.MessageParam[] = [
|
|
|
+ {
|
|
|
+ role: "user",
|
|
|
+ content: [{ type: "text" as const, text: "What's the weather in London?" }],
|
|
|
+ },
|
|
|
+ ]
|
|
|
+
|
|
|
+ const mockTools = [
|
|
|
+ {
|
|
|
+ type: "function" as const,
|
|
|
+ function: {
|
|
|
+ name: "get_weather",
|
|
|
+ description: "Get the current weather",
|
|
|
+ parameters: {
|
|
|
+ type: "object",
|
|
|
+ properties: {
|
|
|
+ location: { type: "string" },
|
|
|
+ },
|
|
|
+ required: ["location"],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ]
|
|
|
+
|
|
|
+ it("should include tools in request when toolProtocol is native", async () => {
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages, {
|
|
|
+ taskId: "test-task",
|
|
|
+ tools: mockTools,
|
|
|
+ toolProtocol: "native",
|
|
|
+ })
|
|
|
+
|
|
|
+ // Consume the stream to trigger the API call
|
|
|
+ for await (const _chunk of stream) {
|
|
|
+ // Just consume
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(mockCreate).toHaveBeenCalledWith(
|
|
|
+ expect.objectContaining({
|
|
|
+ tools: expect.arrayContaining([
|
|
|
+ expect.objectContaining({
|
|
|
+ name: "get_weather",
|
|
|
+ description: "Get the current weather",
|
|
|
+ input_schema: expect.objectContaining({
|
|
|
+ type: "object",
|
|
|
+ properties: expect.objectContaining({
|
|
|
+ location: { type: "string" },
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ ]),
|
|
|
+ }),
|
|
|
+ expect.anything(),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should not include tools when toolProtocol is xml", async () => {
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages, {
|
|
|
+ taskId: "test-task",
|
|
|
+ tools: mockTools,
|
|
|
+ toolProtocol: "xml",
|
|
|
+ })
|
|
|
+
|
|
|
+ // Consume the stream to trigger the API call
|
|
|
+ for await (const _chunk of stream) {
|
|
|
+ // Just consume
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(mockCreate).toHaveBeenCalledWith(
|
|
|
+ expect.not.objectContaining({
|
|
|
+ tools: expect.anything(),
|
|
|
+ }),
|
|
|
+ expect.anything(),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should not include tools when no tools are provided", async () => {
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages, {
|
|
|
+ taskId: "test-task",
|
|
|
+ toolProtocol: "native",
|
|
|
+ })
|
|
|
+
|
|
|
+ // Consume the stream to trigger the API call
|
|
|
+ for await (const _chunk of stream) {
|
|
|
+ // Just consume
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(mockCreate).toHaveBeenCalledWith(
|
|
|
+ expect.not.objectContaining({
|
|
|
+ tools: expect.anything(),
|
|
|
+ }),
|
|
|
+ expect.anything(),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should convert tool_choice 'auto' to Anthropic format", async () => {
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages, {
|
|
|
+ taskId: "test-task",
|
|
|
+ tools: mockTools,
|
|
|
+ toolProtocol: "native",
|
|
|
+ tool_choice: "auto",
|
|
|
+ })
|
|
|
+
|
|
|
+ // Consume the stream to trigger the API call
|
|
|
+ for await (const _chunk of stream) {
|
|
|
+ // Just consume
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(mockCreate).toHaveBeenCalledWith(
|
|
|
+ expect.objectContaining({
|
|
|
+ tool_choice: { type: "auto", disable_parallel_tool_use: true },
|
|
|
+ }),
|
|
|
+ expect.anything(),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should convert tool_choice 'required' to Anthropic 'any' format", async () => {
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages, {
|
|
|
+ taskId: "test-task",
|
|
|
+ tools: mockTools,
|
|
|
+ toolProtocol: "native",
|
|
|
+ tool_choice: "required",
|
|
|
+ })
|
|
|
+
|
|
|
+ // Consume the stream to trigger the API call
|
|
|
+ for await (const _chunk of stream) {
|
|
|
+ // Just consume
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(mockCreate).toHaveBeenCalledWith(
|
|
|
+ expect.objectContaining({
|
|
|
+ tool_choice: { type: "any", disable_parallel_tool_use: true },
|
|
|
+ }),
|
|
|
+ expect.anything(),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should omit both tools and tool_choice when tool_choice is 'none'", async () => {
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages, {
|
|
|
+ taskId: "test-task",
|
|
|
+ tools: mockTools,
|
|
|
+ toolProtocol: "native",
|
|
|
+ tool_choice: "none",
|
|
|
+ })
|
|
|
+
|
|
|
+ // Consume the stream to trigger the API call
|
|
|
+ for await (const _chunk of stream) {
|
|
|
+ // Just consume
|
|
|
+ }
|
|
|
+
|
|
|
+ // Verify that neither tools nor tool_choice are included in the request
|
|
|
+ expect(mockCreate).toHaveBeenCalledWith(
|
|
|
+ expect.not.objectContaining({
|
|
|
+ tools: expect.anything(),
|
|
|
+ }),
|
|
|
+ expect.anything(),
|
|
|
+ )
|
|
|
+ expect(mockCreate).toHaveBeenCalledWith(
|
|
|
+ expect.not.objectContaining({
|
|
|
+ tool_choice: expect.anything(),
|
|
|
+ }),
|
|
|
+ expect.anything(),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should convert specific tool_choice to Anthropic 'tool' format", async () => {
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages, {
|
|
|
+ taskId: "test-task",
|
|
|
+ tools: mockTools,
|
|
|
+ toolProtocol: "native",
|
|
|
+ tool_choice: { type: "function" as const, function: { name: "get_weather" } },
|
|
|
+ })
|
|
|
+
|
|
|
+ // Consume the stream to trigger the API call
|
|
|
+ for await (const _chunk of stream) {
|
|
|
+ // Just consume
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(mockCreate).toHaveBeenCalledWith(
|
|
|
+ expect.objectContaining({
|
|
|
+ tool_choice: { type: "tool", name: "get_weather", disable_parallel_tool_use: true },
|
|
|
+ }),
|
|
|
+ expect.anything(),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should enable parallel tool calls when parallelToolCalls is true", async () => {
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages, {
|
|
|
+ taskId: "test-task",
|
|
|
+ tools: mockTools,
|
|
|
+ toolProtocol: "native",
|
|
|
+ tool_choice: "auto",
|
|
|
+ parallelToolCalls: true,
|
|
|
+ })
|
|
|
+
|
|
|
+ // Consume the stream to trigger the API call
|
|
|
+ for await (const _chunk of stream) {
|
|
|
+ // Just consume
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(mockCreate).toHaveBeenCalledWith(
|
|
|
+ expect.objectContaining({
|
|
|
+ tool_choice: { type: "auto", disable_parallel_tool_use: false },
|
|
|
+ }),
|
|
|
+ expect.anything(),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should handle tool_use blocks in stream and emit tool_call_partial", async () => {
|
|
|
+ mockCreate.mockImplementationOnce(async () => ({
|
|
|
+ async *[Symbol.asyncIterator]() {
|
|
|
+ yield {
|
|
|
+ type: "message_start",
|
|
|
+ message: {
|
|
|
+ usage: {
|
|
|
+ input_tokens: 100,
|
|
|
+ output_tokens: 50,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+ yield {
|
|
|
+ type: "content_block_start",
|
|
|
+ index: 0,
|
|
|
+ content_block: {
|
|
|
+ type: "tool_use",
|
|
|
+ id: "toolu_123",
|
|
|
+ name: "get_weather",
|
|
|
+ },
|
|
|
+ }
|
|
|
+ },
|
|
|
+ }))
|
|
|
+
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages, {
|
|
|
+ taskId: "test-task",
|
|
|
+ tools: mockTools,
|
|
|
+ toolProtocol: "native",
|
|
|
+ })
|
|
|
+
|
|
|
+ const chunks: any[] = []
|
|
|
+ for await (const chunk of stream) {
|
|
|
+ chunks.push(chunk)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Find the tool_call_partial chunk
|
|
|
+ const toolCallChunk = chunks.find((chunk) => chunk.type === "tool_call_partial")
|
|
|
+ expect(toolCallChunk).toBeDefined()
|
|
|
+ expect(toolCallChunk).toEqual({
|
|
|
+ type: "tool_call_partial",
|
|
|
+ index: 0,
|
|
|
+ id: "toolu_123",
|
|
|
+ name: "get_weather",
|
|
|
+ arguments: undefined,
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should handle input_json_delta in stream and emit tool_call_partial arguments", async () => {
|
|
|
+ mockCreate.mockImplementationOnce(async () => ({
|
|
|
+ async *[Symbol.asyncIterator]() {
|
|
|
+ yield {
|
|
|
+ type: "message_start",
|
|
|
+ message: {
|
|
|
+ usage: {
|
|
|
+ input_tokens: 100,
|
|
|
+ output_tokens: 50,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+ yield {
|
|
|
+ type: "content_block_start",
|
|
|
+ index: 0,
|
|
|
+ content_block: {
|
|
|
+ type: "tool_use",
|
|
|
+ id: "toolu_123",
|
|
|
+ name: "get_weather",
|
|
|
+ },
|
|
|
+ }
|
|
|
+ yield {
|
|
|
+ type: "content_block_delta",
|
|
|
+ index: 0,
|
|
|
+ delta: {
|
|
|
+ type: "input_json_delta",
|
|
|
+ partial_json: '{"location":',
|
|
|
+ },
|
|
|
+ }
|
|
|
+ yield {
|
|
|
+ type: "content_block_delta",
|
|
|
+ index: 0,
|
|
|
+ delta: {
|
|
|
+ type: "input_json_delta",
|
|
|
+ partial_json: '"London"}',
|
|
|
+ },
|
|
|
+ }
|
|
|
+ yield {
|
|
|
+ type: "content_block_stop",
|
|
|
+ index: 0,
|
|
|
+ }
|
|
|
+ },
|
|
|
+ }))
|
|
|
+
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages, {
|
|
|
+ taskId: "test-task",
|
|
|
+ tools: mockTools,
|
|
|
+ toolProtocol: "native",
|
|
|
+ })
|
|
|
+
|
|
|
+ const chunks: any[] = []
|
|
|
+ for await (const chunk of stream) {
|
|
|
+ chunks.push(chunk)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Find the tool_call_partial chunks
|
|
|
+ const toolCallChunks = chunks.filter((chunk) => chunk.type === "tool_call_partial")
|
|
|
+ expect(toolCallChunks).toHaveLength(3)
|
|
|
+
|
|
|
+ // First chunk has id and name
|
|
|
+ expect(toolCallChunks[0]).toEqual({
|
|
|
+ type: "tool_call_partial",
|
|
|
+ index: 0,
|
|
|
+ id: "toolu_123",
|
|
|
+ name: "get_weather",
|
|
|
+ arguments: undefined,
|
|
|
+ })
|
|
|
+
|
|
|
+ // Subsequent chunks have arguments
|
|
|
+ expect(toolCallChunks[1]).toEqual({
|
|
|
+ type: "tool_call_partial",
|
|
|
+ index: 0,
|
|
|
+ id: undefined,
|
|
|
+ name: undefined,
|
|
|
+ arguments: '{"location":',
|
|
|
+ })
|
|
|
+
|
|
|
+ expect(toolCallChunks[2]).toEqual({
|
|
|
+ type: "tool_call_partial",
|
|
|
+ index: 0,
|
|
|
+ id: undefined,
|
|
|
+ name: undefined,
|
|
|
+ arguments: '"London"}',
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
})
|