@@ -32,7 +32,9 @@ import (
3232type anthropicRequest struct {
3333 Model string `json:"model"`
3434 MaxTokens int32 `json:"max_tokens"`
35- System string `json:"system,omitempty"`
35+ // System is `any`: a bare string normally, or []anthropicSystemBlock
36+ // when cache_prompt is on (the block form carries cache_control).
37+ System any `json:"system,omitempty"`
3638 Messages []anthropicMessage `json:"messages"`
3739 Stream bool `json:"stream,omitempty"`
3840 Temperature * float64 `json:"temperature,omitempty"`
@@ -52,9 +54,30 @@ type anthropicMessage struct {
5254}
5355
5456type anthropicTool struct {
55- Name string `json:"name"`
56- Description string `json:"description,omitempty"`
57- InputSchema json.RawMessage `json:"input_schema"`
57+ Name string `json:"name"`
58+ Description string `json:"description,omitempty"`
59+ InputSchema json.RawMessage `json:"input_schema"`
60+ CacheControl * anthropicCacheControl `json:"cache_control,omitempty"`
61+ }
62+
63+ // anthropicCacheControl marks a prompt-cache breakpoint. Anthropic caches
64+ // everything up to and including a block tagged {"type":"ephemeral"} (5-min
65+ // TTL) and serves that prefix at the cache-read rate (0.1x input) on later
66+ // calls that share it — the win on agentic/multi-turn workloads.
67+ type anthropicCacheControl struct {
68+ Type string `json:"type"` // "ephemeral"
69+ }
70+
71+ // ephemeralCacheControl is the single reused breakpoint marker.
72+ var ephemeralCacheControl = & anthropicCacheControl {Type : "ephemeral" }
73+
74+ // anthropicSystemBlock is the block form of the top-level system field.
75+ // Anthropic accepts system as a bare string OR a list of text blocks; the
76+ // block form is required to attach cache_control to the system prompt.
77+ type anthropicSystemBlock struct {
78+ Type string `json:"type"` // "text"
79+ Text string `json:"text"`
80+ CacheControl * anthropicCacheControl `json:"cache_control,omitempty"`
5881}
5982
6083// anthropicToolChoice mirrors the four shapes Anthropic accepts:
@@ -81,8 +104,9 @@ type anthropicContentBlock struct {
81104 // Tool-result block fields. tool_result uses `content` (not
82105 // `text`) and pairs with `tool_use_id`; modelling them as
83106 // distinct fields avoids ambiguity at marshal time.
84- ToolUseID string `json:"tool_use_id,omitempty"`
85- ResultContent string `json:"content,omitempty"`
107+ ToolUseID string `json:"tool_use_id,omitempty"`
108+ ResultContent string `json:"content,omitempty"`
109+ CacheControl * anthropicCacheControl `json:"cache_control,omitempty"`
86110}
87111
88112type anthropicResponse struct {
@@ -156,6 +180,11 @@ func buildAnthropicRequest(opts *pb.PredictOptions, cfg *proxyConfig, stream boo
156180 if req .ToolChoice != nil && req .ToolChoice .Type == anthropicToolChoiceNone {
157181 req .Tools , req .ToolChoice = nil , nil
158182 }
183+ // Prompt-cache breakpoint on the last tool: Anthropic caches the entire
184+ // tool block up to the marked tool — usually a large, fully stable prefix.
185+ if cfg .cachePrompt && len (req .Tools ) > 0 {
186+ req .Tools [len (req .Tools )- 1 ].CacheControl = ephemeralCacheControl
187+ }
159188
160189 var systemParts []string
161190 for _ , m := range opts .GetMessages () {
@@ -189,15 +218,54 @@ func buildAnthropicRequest(opts *pb.PredictOptions, cfg *proxyConfig, stream boo
189218 })
190219 }
191220 }
192- req .System = strings .Join (systemParts , "\n \n " )
221+ // System: block form (with cache_control) when caching is on, else the
222+ // bare string. Only set when non-empty so `omitempty` still drops it.
223+ if len (systemParts ) > 0 {
224+ joined := strings .Join (systemParts , "\n \n " )
225+ if cfg .cachePrompt {
226+ req .System = []anthropicSystemBlock {{Type : "text" , Text : joined , CacheControl : ephemeralCacheControl }}
227+ } else {
228+ req .System = joined
229+ }
230+ }
193231
194232 if len (req .Messages ) == 0 && opts .GetPrompt () != "" {
195233 req .Messages = []anthropicMessage {{Role : "user" , Content : opts .GetPrompt ()}}
196234 }
197235
236+ // Prompt-cache breakpoint on the final message block caches the whole
237+ // conversation prefix up to the newest turn. With the system + tools
238+ // breakpoints above, Anthropic serves the entire stable head at the
239+ // cache-read rate on the next agentic iteration (max 4 breakpoints; we
240+ // use at most 3, so we never exceed the limit).
241+ if cfg .cachePrompt {
242+ markLastMessageCacheable (req .Messages )
243+ }
244+
198245 return json .Marshal (req )
199246}
200247
248+ // markLastMessageCacheable tags the final block of the last message with a
249+ // cache_control breakpoint. String content is promoted to a single text
250+ // block so the marker has somewhere to attach; block content gets the marker
251+ // on its last element.
252+ func markLastMessageCacheable (msgs []anthropicMessage ) {
253+ if len (msgs ) == 0 {
254+ return
255+ }
256+ last := & msgs [len (msgs )- 1 ]
257+ switch c := last .Content .(type ) {
258+ case string :
259+ if c != "" {
260+ last .Content = []anthropicContentBlock {{Type : "text" , Text : c , CacheControl : ephemeralCacheControl }}
261+ }
262+ case []anthropicContentBlock :
263+ if len (c ) > 0 {
264+ c [len (c )- 1 ].CacheControl = ephemeralCacheControl
265+ }
266+ }
267+ }
268+
201269// appendToolResult appends a tool_result block as a user message,
202270// merging into a preceding user message that already carries blocks.
203271// Anthropic concatenates consecutive same-role messages on its end,
0 commit comments