Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions bridge_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1384,39 +1384,61 @@ func TestAnthropicToolChoiceParallelDisabled(t *testing.T) {
cases := []struct {
name string
toolChoice any // nil, or map with "type" key.
withInjectedTools bool
expectDisableParallel bool
expectToolChoiceTypeInRequest string
}{
// With injected tools - disable_parallel_tool_use should be set.
{
name: "no tool_choice defined defaults to auto",
name: "with injected tools: no tool_choice defined defaults to auto",
toolChoice: nil,
withInjectedTools: true,
expectDisableParallel: true,
expectToolChoiceTypeInRequest: toolChoiceAuto,
},
{
name: "tool_choice auto",
name: "with injected tools: tool_choice auto",
toolChoice: map[string]any{"type": toolChoiceAuto},
withInjectedTools: true,
expectDisableParallel: true,
expectToolChoiceTypeInRequest: toolChoiceAuto,
},
{
name: "tool_choice any",
name: "with injected tools: tool_choice any",
toolChoice: map[string]any{"type": toolChoiceAny},
withInjectedTools: true,
expectDisableParallel: true,
expectToolChoiceTypeInRequest: toolChoiceAny,
},
{
name: "tool_choice tool",
name: "with injected tools: tool_choice tool",
toolChoice: map[string]any{"type": toolChoiceTool, "name": "some_tool"},
withInjectedTools: true,
expectDisableParallel: true,
expectToolChoiceTypeInRequest: toolChoiceTool,
},
{
name: "tool_choice none",
name: "with injected tools: tool_choice none",
toolChoice: map[string]any{"type": toolChoiceNone},
withInjectedTools: true,
expectDisableParallel: false,
expectToolChoiceTypeInRequest: toolChoiceNone,
},
// Without injected tools - disable_parallel_tool_use should NOT be set.
{
name: "without injected tools: tool_choice auto",
toolChoice: map[string]any{"type": toolChoiceAuto},
withInjectedTools: false,
expectDisableParallel: false,
expectToolChoiceTypeInRequest: toolChoiceAuto,
},
{
name: "without injected tools: tool_choice any",
toolChoice: map[string]any{"type": toolChoiceAny},
withInjectedTools: false,
expectDisableParallel: false,
expectToolChoiceTypeInRequest: toolChoiceAny,
},
}

for _, tc := range cases {
Expand All @@ -1426,8 +1448,14 @@ func TestAnthropicToolChoiceParallelDisabled(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), time.Second*30)
t.Cleanup(cancel)

// Configure the bridge.
mcpMgr := mcp.NewServerProxyManager(nil, testTracer)
// Setup MCP tools conditionally.
var mcpMgr *mcp.ServerProxyManager
if tc.withInjectedTools {
mcpProxiers, _ := setupMCPServerProxiesForTest(t, testTracer)
mcpMgr = mcp.NewServerProxyManager(mcpProxiers, testTracer)
} else {
mcpMgr = mcp.NewServerProxyManager(nil, testTracer)
}
require.NoError(t, mcpMgr.Init(ctx))

arc := txtar.Parse(antSimple)
Expand Down
195 changes: 195 additions & 0 deletions intercept_anthropic_message_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package aibridge

import (
"context"
"testing"

"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/shared/constant"
"github.com/coder/aibridge/mcp"
mcpgo "github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/require"
)

func TestInjectTools_ParallelToolCalls(t *testing.T) {
t.Parallel()

t.Run("does not modify tool choice when no tools to inject", func(t *testing.T) {
t.Parallel()

i := &AnthropicMessagesInterceptionBase{
req: &MessageNewParamsWrapper{
MessageNewParams: anthropic.MessageNewParams{
ToolChoice: anthropic.ToolChoiceUnionParam{
OfAuto: &anthropic.ToolChoiceAutoParam{
Type: constant.ValueOf[constant.Auto](),
},
},
},
},
mcpProxy: &mockServerProxier{tools: nil}, // No tools to inject.
}

i.injectTools()

// Tool choice should remain unchanged - DisableParallelToolUse should not be set.
require.NotNil(t, i.req.ToolChoice.OfAuto)
require.False(t, i.req.ToolChoice.OfAuto.DisableParallelToolUse.Valid())
})

t.Run("disables parallel tool use for auto tool choice (default)", func(t *testing.T) {
t.Parallel()

i := &AnthropicMessagesInterceptionBase{
req: &MessageNewParamsWrapper{
MessageNewParams: anthropic.MessageNewParams{
// No tool choice set (default).
},
},
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{{ID: "test_tool", Name: "test", Description: "Test"}},
},
}

i.injectTools()

require.NotNil(t, i.req.ToolChoice.OfAuto)
require.True(t, i.req.ToolChoice.OfAuto.DisableParallelToolUse.Valid())
require.True(t, i.req.ToolChoice.OfAuto.DisableParallelToolUse.Value)
})

t.Run("disables parallel tool use for explicit auto tool choice", func(t *testing.T) {
t.Parallel()

i := &AnthropicMessagesInterceptionBase{
req: &MessageNewParamsWrapper{
MessageNewParams: anthropic.MessageNewParams{
ToolChoice: anthropic.ToolChoiceUnionParam{
OfAuto: &anthropic.ToolChoiceAutoParam{
Type: constant.ValueOf[constant.Auto](),
},
},
},
},
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{{ID: "test_tool", Name: "test", Description: "Test"}},
},
}

i.injectTools()

require.NotNil(t, i.req.ToolChoice.OfAuto)
require.True(t, i.req.ToolChoice.OfAuto.DisableParallelToolUse.Valid())
require.True(t, i.req.ToolChoice.OfAuto.DisableParallelToolUse.Value)
})

t.Run("disables parallel tool use for any tool choice", func(t *testing.T) {
t.Parallel()

i := &AnthropicMessagesInterceptionBase{
req: &MessageNewParamsWrapper{
MessageNewParams: anthropic.MessageNewParams{
ToolChoice: anthropic.ToolChoiceUnionParam{
OfAny: &anthropic.ToolChoiceAnyParam{
Type: constant.ValueOf[constant.Any](),
},
},
},
},
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{{ID: "test_tool", Name: "test", Description: "Test"}},
},
}

i.injectTools()

require.NotNil(t, i.req.ToolChoice.OfAny)
require.True(t, i.req.ToolChoice.OfAny.DisableParallelToolUse.Valid())
require.True(t, i.req.ToolChoice.OfAny.DisableParallelToolUse.Value)
})

t.Run("disables parallel tool use for tool choice type", func(t *testing.T) {
t.Parallel()

i := &AnthropicMessagesInterceptionBase{
req: &MessageNewParamsWrapper{
MessageNewParams: anthropic.MessageNewParams{
ToolChoice: anthropic.ToolChoiceUnionParam{
OfTool: &anthropic.ToolChoiceToolParam{
Type: constant.ValueOf[constant.Tool](),
Name: "specific_tool",
},
},
},
},
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{{ID: "test_tool", Name: "test", Description: "Test"}},
},
}

i.injectTools()

require.NotNil(t, i.req.ToolChoice.OfTool)
require.True(t, i.req.ToolChoice.OfTool.DisableParallelToolUse.Valid())
require.True(t, i.req.ToolChoice.OfTool.DisableParallelToolUse.Value)
})

t.Run("no-op for none tool choice type", func(t *testing.T) {
t.Parallel()

i := &AnthropicMessagesInterceptionBase{
req: &MessageNewParamsWrapper{
MessageNewParams: anthropic.MessageNewParams{
ToolChoice: anthropic.ToolChoiceUnionParam{
OfNone: &anthropic.ToolChoiceNoneParam{
Type: constant.ValueOf[constant.None](),
},
},
},
},
mcpProxy: &mockServerProxier{
tools: []*mcp.Tool{{ID: "test_tool", Name: "test", Description: "Test"}},
},
}

i.injectTools()

// Tools are still injected.
require.Len(t, i.req.Tools, 1)
Comment on lines +157 to +158

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I fully understand this case. The AI client sends a request with tool_choice: none, telling Anthropic to not execute any tools. But AIBridge still injects the tools even though Anthropic will never execute them, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! We don't conditionally inject tools based on tool choice right now. Maybe we should...

I'm in favour of not trying to be too clever / efficient here because this behaviour might confuse us / an operator looking at request logs. It might not be immediately apparently that tools didn't inject for this reason.

// But no parallel tool use modification for "none" type.
require.Nil(t, i.req.ToolChoice.OfAuto)
require.Nil(t, i.req.ToolChoice.OfAny)
require.Nil(t, i.req.ToolChoice.OfTool)
require.NotNil(t, i.req.ToolChoice.OfNone)
})
}

// mockServerProxier is a test implementation of mcp.ServerProxier.
type mockServerProxier struct {
tools []*mcp.Tool
}

func (m *mockServerProxier) Init(context.Context) error {
return nil
}

func (m *mockServerProxier) Shutdown(context.Context) error {
return nil
}

func (m *mockServerProxier) ListTools() []*mcp.Tool {
return m.tools
}

func (m *mockServerProxier) GetTool(id string) *mcp.Tool {
for _, t := range m.tools {
if t.ID == id {
return t
}
}
return nil
}

func (m *mockServerProxier) CallTool(context.Context, string, any) (*mcpgo.CallToolResult, error) {
return nil, nil
}
8 changes: 7 additions & 1 deletion intercept_anthropic_messages_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,14 @@ func (i *AnthropicMessagesInterceptionBase) injectTools() {
return
}

tools := i.mcpProxy.ListTools()
if len(tools) == 0 {
// No injected tools: no need to influence parallel tool calling.
return
}

// Inject tools.
for _, tool := range i.mcpProxy.ListTools() {
for _, tool := range tools {
i.req.Tools = append(i.req.Tools, anthropic.ToolUnionParam{
OfTool: &anthropic.ToolParam{
InputSchema: anthropic.ToolInputSchemaParam{
Expand Down