From c85f5726872b08d6cf0c0ab9020ff2d287f17a25 Mon Sep 17 00:00:00 2001 From: smallwhite Date: Mon, 30 Mar 2026 20:38:31 +0800 Subject: [PATCH] fix(provider): anthropic_messages sends system as content blocks with cache_control - Use SystemParts structured blocks when available, with per-block cache_control for Anthropic prompt caching support - Fall back to plain text block when SystemParts is absent - Output system as content blocks array instead of flat string - Update test expectation to match new array format --- pkg/providers/anthropic_messages/provider.go | 33 ++++++++++++++----- .../anthropic_messages/provider_test.go | 4 ++- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/pkg/providers/anthropic_messages/provider.go b/pkg/providers/anthropic_messages/provider.go index 1e865b709..26e7408e5 100644 --- a/pkg/providers/anthropic_messages/provider.go +++ b/pkg/providers/anthropic_messages/provider.go @@ -178,17 +178,34 @@ func buildRequestBody( } // Process messages - var systemPrompt string + var systemBlocks []map[string]any var apiMessages []any for _, msg := range messages { switch msg.Role { case "system": - // Accumulate system messages - if systemPrompt != "" { - systemPrompt += "\n\n" + msg.Content + // Prefer structured SystemParts for per-block cache_control. + // This enables Anthropic prompt caching: static blocks keep a + // stable prefix hash while dynamic parts (time, session) change. + if len(msg.SystemParts) > 0 { + for _, part := range msg.SystemParts { + block := map[string]any{ + "type": "text", + "text": part.Text, + } + if part.CacheControl != nil && part.CacheControl.Type != "" { + block["cache_control"] = map[string]string{ + "type": part.CacheControl.Type, + } + } + systemBlocks = append(systemBlocks, block) + } } else { - systemPrompt = msg.Content + // Fallback: no structured parts, use plain text block. + systemBlocks = append(systemBlocks, map[string]any{ + "type": "text", + "text": msg.Content, + }) } case "user": @@ -280,9 +297,9 @@ func buildRequestBody( result["messages"] = apiMessages - // Set system prompt if present - if systemPrompt != "" { - result["system"] = systemPrompt + // Set system prompt if present (always as content blocks array for cache_control support) + if len(systemBlocks) > 0 { + result["system"] = systemBlocks } // Add tools if present diff --git a/pkg/providers/anthropic_messages/provider_test.go b/pkg/providers/anthropic_messages/provider_test.go index ba9d24b66..b6fbb8c1f 100644 --- a/pkg/providers/anthropic_messages/provider_test.go +++ b/pkg/providers/anthropic_messages/provider_test.go @@ -86,7 +86,9 @@ func TestBuildRequestBody(t *testing.T) { want: map[string]any{ "model": "test-model", "max_tokens": int64(8192), - "system": "You are a helpful assistant.", + "system": []map[string]any{ + {"type": "text", "text": "You are a helpful assistant."}, + }, "messages": []any{ map[string]any{ "role": "user",