From a6b5544674dffe64a3cc0add256d4bcd509a66ba Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Thu, 9 Apr 2026 19:26:55 +0000 Subject: [PATCH] refactor(v2/anthropic): use MultiSystem for system prompts Switches buildRequest to emit anthReq.MultiSystem instead of anthReq.System whenever a system message is present. Upstream's MarshalJSON prefers MultiSystem when non-empty, so the wire format is unchanged for requests without cache_control. This refactor is a prerequisite for attaching cache_control markers to system parts in the next commit. Co-Authored-By: Claude Opus 4.6 --- v2/anthropic/anthropic.go | 13 ++++-- v2/anthropic/cache_test.go | 84 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 v2/anthropic/cache_test.go diff --git a/v2/anthropic/anthropic.go b/v2/anthropic/anthropic.go index 0a477e3..9dc713f 100644 --- a/v2/anthropic/anthropic.go +++ b/v2/anthropic/anthropic.go @@ -82,13 +82,14 @@ func (p *Provider) buildRequest(req provider.Request) anth.MessagesRequest { } var msgs []anth.Message + var systemText string for _, msg := range req.Messages { if msg.Role == "system" { - if len(anthReq.System) > 0 { - anthReq.System += "\n" + if len(systemText) > 0 { + systemText += "\n" } - anthReq.System += msg.Content + systemText += msg.Content continue } @@ -224,6 +225,12 @@ func (p *Provider) buildRequest(req provider.Request) anth.MessagesRequest { anthReq.Messages = msgs + if systemText != "" { + anthReq.MultiSystem = []anth.MessageSystemPart{ + anth.NewSystemMessagePart(systemText), + } + } + if req.Temperature != nil { f := float32(*req.Temperature) anthReq.Temperature = &f diff --git a/v2/anthropic/cache_test.go b/v2/anthropic/cache_test.go new file mode 100644 index 0000000..16d6439 --- /dev/null +++ b/v2/anthropic/cache_test.go @@ -0,0 +1,84 @@ +package anthropic + +import ( + "testing" + + "gitea.stevedudenhoeffer.com/steve/go-llm/v2/provider" + anth "github.com/liushuangls/go-anthropic/v2" +) + +// TestBuildRequest_MultiSystemUsedWhenSystemPresent verifies that after the +// refactor, the Anthropic provider uses MultiSystem (multi-part) rather than +// the flat System string when a system message is present. This is +// behavior-preserving — the upstream client's MarshalJSON prefers +// MultiSystem when both are set. +func TestBuildRequest_MultiSystemUsedWhenSystemPresent(t *testing.T) { + p := New("test-key") + req := provider.Request{ + Model: "claude-sonnet-4-6", + Messages: []provider.Message{ + {Role: "system", Content: "you are helpful"}, + {Role: "user", Content: "hello"}, + }, + } + anthReq := p.buildRequest(req) + + if len(anthReq.MultiSystem) != 1 { + t.Fatalf("expected 1 MultiSystem part, got %d", len(anthReq.MultiSystem)) + } + if anthReq.MultiSystem[0].Text != "you are helpful" { + t.Errorf("expected MultiSystem text 'you are helpful', got %q", anthReq.MultiSystem[0].Text) + } + if anthReq.MultiSystem[0].Type != "text" { + t.Errorf("expected MultiSystem type 'text', got %q", anthReq.MultiSystem[0].Type) + } + if anthReq.System != "" { + t.Errorf("expected System string to be empty when MultiSystem is used, got %q", anthReq.System) + } +} + +// TestBuildRequest_MultipleSystemMessagesConcatenated verifies that multiple +// system messages are joined into a single MultiSystem part (preserving +// existing newline-joined behavior from the old code). +func TestBuildRequest_MultipleSystemMessagesConcatenated(t *testing.T) { + p := New("test-key") + req := provider.Request{ + Model: "claude-sonnet-4-6", + Messages: []provider.Message{ + {Role: "system", Content: "part A"}, + {Role: "system", Content: "part B"}, + {Role: "user", Content: "hello"}, + }, + } + anthReq := p.buildRequest(req) + + if len(anthReq.MultiSystem) != 1 { + t.Fatalf("expected 1 MultiSystem part after concat, got %d", len(anthReq.MultiSystem)) + } + expected := "part A\npart B" + if anthReq.MultiSystem[0].Text != expected { + t.Errorf("expected MultiSystem text %q, got %q", expected, anthReq.MultiSystem[0].Text) + } +} + +// TestBuildRequest_NoSystemMessage verifies that when there's no system +// message, both System and MultiSystem are empty. +func TestBuildRequest_NoSystemMessage(t *testing.T) { + p := New("test-key") + req := provider.Request{ + Model: "claude-sonnet-4-6", + Messages: []provider.Message{ + {Role: "user", Content: "hello"}, + }, + } + anthReq := p.buildRequest(req) + + if len(anthReq.MultiSystem) != 0 { + t.Errorf("expected empty MultiSystem when no system message, got %d parts", len(anthReq.MultiSystem)) + } + if anthReq.System != "" { + t.Errorf("expected empty System string, got %q", anthReq.System) + } + // Silences the unused import warning in this file until Task 5. + _ = anth.CacheControlTypeEphemeral +}