Skip to content
Open
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
5 changes: 5 additions & 0 deletions provider/anthropicprovider/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ func (a *client) buildMessageParams(messages []*message.Message, opts []agent.Op
var params anthropic.MessageNewParams
if p, ok := agent.GetOption(opts, MessageNewParams); ok {
params = p
// Clone the mutable slice fields appended to below so we never mutate
// the caller's backing arrays (the option stores a shallow copy of the
// struct); the gemini provider clones for the same reason.
params.System = slices.Clone(params.System)
params.Messages = slices.Clone(params.Messages)
}
params.Model = cmp.Or(params.Model, a.config.Model)
params.MaxTokens = cmp.Or(params.MaxTokens, 4096)
Expand Down
47 changes: 47 additions & 0 deletions provider/anthropicprovider/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,53 @@ func TestStreamingToolCallsSupportInterleavedDeltas(t *testing.T) {
}
}

// Building the request must not mutate the caller's MessageNewParams slices.
// The provider appends system instructions to params.System; if it shares the
// caller's backing array (spare capacity), the append corrupts the caller's data.
func TestBuildMessageParams_DoesNotMutateCallerSystemSlice(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"id":"m","type":"message","role":"assistant","model":"claude","content":[{"type":"text","text":"ok"}],"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1}}`)
}))
defer server.Close()
a := newTestClient(t, server)

// Caller-supplied System slice with spare capacity.
system := make([]anthropic.TextBlockParam, 1, 4)
system[0] = anthropic.TextBlockParam{Text: "s0"}
opt := anthropicprovider.MessageNewParams(anthropic.MessageNewParams{System: system})

Comment on lines +472 to +476

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — added TestBuildMessageParams_DoesNotMutateCallerMessagesSlice, a red-green regression test covering a caller-supplied Messages slice with spare capacity.

if _, err := a.RunText(t.Context(), "hi", agent.WithInstructions("added"), opt).Collect(); err != nil {
t.Fatalf("run: %v", err)
}
if full := system[:cap(system)]; full[1].Text != "" {
t.Errorf("provider mutated the caller's System backing array: spare slot = %q", full[1].Text)
}
}

func TestBuildMessageParams_DoesNotMutateCallerMessagesSlice(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"id":"m","type":"message","role":"assistant","model":"claude","content":[{"type":"text","text":"ok"}],"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1}}`)
}))
defer server.Close()
a := newTestClient(t, server)

// Caller-supplied Messages slice with spare capacity. The provider appends
// the run's messages to params.Messages; with aliasing that append lands in
// the caller's spare slot instead of a cloned slice.
messages := make([]anthropic.MessageParam, 1, 4)
messages[0] = anthropic.NewUserMessage(anthropic.NewTextBlock("seeded"))
opt := anthropicprovider.MessageNewParams(anthropic.MessageNewParams{Messages: messages})

if _, err := a.RunText(t.Context(), "hi", opt).Collect(); err != nil {
t.Fatalf("run: %v", err)
}
if full := messages[:cap(messages)]; len(full[1].Content) != 0 {
t.Errorf("provider mutated the caller's Messages backing array: spare slot has %d content block(s)", len(full[1].Content))
}
}

// A tool call with empty Arguments must serialize to an object input ({}), not
// null: Anthropic rejects a tool_use block whose input is null.
func TestToolUseEmptyArgumentsSerializeAsObject(t *testing.T) {
Expand Down