Skip to content

Commit 22f2df1

Browse files
Merge branch 'main' into fix/local
2 parents 6e616f5 + 7529d30 commit 22f2df1

File tree

9 files changed

+300
-80
lines changed

9 files changed

+300
-80
lines changed

components/model/claude/claude.go

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,17 @@ func (cm *ChatModel) BindForcedTools(tools []*schema.ToolInfo) error {
434434
return nil
435435
}
436436

437+
// newCacheControlParam creates a CacheControlEphemeralParam from the given CacheControl.
438+
// If ctrl is nil or ctrl.TTL is empty, the returned param will have no TTL set,
439+
// which means the API will use its default TTL (5 minutes).
440+
func newCacheControlParam(ctrl *CacheControl) anthropic.CacheControlEphemeralParam {
441+
p := anthropic.NewCacheControlEphemeralParam()
442+
if ctrl != nil && ctrl.TTL != "" {
443+
p.TTL = ctrl.TTL
444+
}
445+
return p
446+
}
447+
437448
func toAnthropicToolParam(tools []*schema.ToolInfo) ([]anthropic.ToolUnionParam, error) {
438449
if len(tools) == 0 {
439450
return nil, nil
@@ -461,7 +472,7 @@ func toAnthropicToolParam(tools []*schema.ToolInfo) ([]anthropic.ToolUnionParam,
461472
}
462473

463474
if isBreakpointTool(tool) {
464-
toolParam.CacheControl = anthropic.NewCacheControlEphemeralParam()
475+
toolParam.CacheControl = newCacheControlParam(getToolBreakpointCacheControl(tool))
465476
}
466477

467478
result = append(result, anthropic.ToolUnionParam{OfTool: toolParam})
@@ -565,14 +576,14 @@ func (cm *ChatModel) populateInput(params *anthropic.MessageNewParams, system []
565576
block := anthropic.TextBlockParam{Text: m.Content}
566577
if isBreakpointMessage(m) {
567578
hasSetSysBreakPoint = true
568-
block.CacheControl = anthropic.NewCacheControlEphemeralParam()
579+
block.CacheControl = newCacheControlParam(getMessageBreakpointCacheControl(m))
569580
}
570581
params.System = append(params.System, block)
571582
}
572583

573584
// if no breakpoint has been set, a breakpoint will be set for the last system message
574-
if len(params.System) > 0 && !hasSetSysBreakPoint && fromOrDefault(specOptions.EnableAutoCache, false) {
575-
params.System[len(params.System)-1].CacheControl = anthropic.NewCacheControlEphemeralParam()
585+
if len(params.System) > 0 && !hasSetSysBreakPoint && specOptions.AutoCacheControl != nil {
586+
params.System[len(params.System)-1].CacheControl = newCacheControlParam(specOptions.AutoCacheControl)
576587
}
577588

578589
msgParams := make([]anthropic.MessageParam, 0, len(msgs))
@@ -591,10 +602,10 @@ func (cm *ChatModel) populateInput(params *anthropic.MessageNewParams, system []
591602
msgParams = append(msgParams, msgParam)
592603
}
593604

594-
if !hasSetMsgBreakPoint && fromOrDefault(specOptions.EnableAutoCache, false) {
605+
if !hasSetMsgBreakPoint && specOptions.AutoCacheControl != nil {
595606
lastMsgParam := msgParams[len(msgParams)-1]
596607
lastBlock := lastMsgParam.Content[len(lastMsgParam.Content)-1]
597-
populateContentBlockBreakPoint(lastBlock)
608+
populateContentBlockBreakPoint(lastBlock, specOptions.AutoCacheControl)
598609
}
599610

600611
params.Messages = msgParams
@@ -612,7 +623,7 @@ func (cm *ChatModel) populateTools(params *anthropic.MessageNewParams, commonOpt
612623
}
613624
}
614625

615-
if len(tools) > 0 && fromOrDefault(specOptions.EnableAutoCache, false) {
626+
if len(tools) > 0 && specOptions.AutoCacheControl != nil {
616627
hasBreakpoint := false
617628
for _, tool := range tools {
618629
if ctrl := tool.GetCacheControl(); ctrl != nil && ctrl.Type != "" {
@@ -622,7 +633,7 @@ func (cm *ChatModel) populateTools(params *anthropic.MessageNewParams, commonOpt
622633
}
623634
// if no breakpoint has been set, a breakpoint will be set for the last tool
624635
if !hasBreakpoint {
625-
tools[len(tools)-1].OfTool.CacheControl = anthropic.NewCacheControlEphemeralParam()
636+
tools[len(tools)-1].OfTool.CacheControl = newCacheControlParam(specOptions.AutoCacheControl)
626637
}
627638
}
628639

@@ -903,7 +914,7 @@ func convSchemaMessage(message *schema.Message) (mp anthropic.MessageParam, err
903914
}
904915

905916
if len(messageParams) > 0 && isBreakpointMessage(message) {
906-
populateContentBlockBreakPoint(messageParams[len(messageParams)-1])
917+
populateContentBlockBreakPoint(messageParams[len(messageParams)-1], getMessageBreakpointCacheControl(message))
907918
}
908919

909920
switch message.Role {
@@ -974,21 +985,22 @@ func convToolMultiContent(callID string, parts []schema.MessageInputPart) (anthr
974985
return result, nil
975986
}
976987

977-
func populateContentBlockBreakPoint(block anthropic.ContentBlockParamUnion) {
988+
func populateContentBlockBreakPoint(block anthropic.ContentBlockParamUnion, cacheCtrl *CacheControl) {
989+
ctrl := newCacheControlParam(cacheCtrl)
978990
if block.OfText != nil {
979-
block.OfText.CacheControl = anthropic.NewCacheControlEphemeralParam()
991+
block.OfText.CacheControl = ctrl
980992
return
981993
}
982994
if block.OfImage != nil {
983-
block.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
995+
block.OfImage.CacheControl = ctrl
984996
return
985997
}
986998
if block.OfToolResult != nil {
987-
block.OfToolResult.CacheControl = anthropic.NewCacheControlEphemeralParam()
999+
block.OfToolResult.CacheControl = ctrl
9881000
return
9891001
}
9901002
if block.OfToolUse != nil {
991-
block.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
1003+
block.OfToolUse.CacheControl = ctrl
9921004
return
9931005
}
9941006
}

components/model/claude/claude_test.go

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,19 +345,19 @@ func TestWithTools(t *testing.T) {
345345

346346
func TestPopulateContentBlockBreakPoint(t *testing.T) {
347347
block := anthropic.NewTextBlock("input")
348-
populateContentBlockBreakPoint(block)
348+
populateContentBlockBreakPoint(block, nil)
349349
assert.NotEmpty(t, block.OfText.CacheControl.Type)
350350

351351
block = anthropic.NewImageBlock[anthropic.URLImageSourceParam](anthropic.URLImageSourceParam{})
352-
populateContentBlockBreakPoint(block)
352+
populateContentBlockBreakPoint(block, nil)
353353
assert.NotEmpty(t, block.OfImage.CacheControl.Type)
354354

355355
block = anthropic.NewToolResultBlock("userID", "input", false)
356-
populateContentBlockBreakPoint(block)
356+
populateContentBlockBreakPoint(block, nil)
357357
assert.NotEmpty(t, block.OfToolResult.CacheControl.Type)
358358

359359
block = anthropic.NewToolUseBlock("123", "input", "test_tool")
360-
populateContentBlockBreakPoint(block)
360+
populateContentBlockBreakPoint(block, nil)
361361
assert.NotEmpty(t, block.OfToolUse.CacheControl.Type)
362362
}
363363

@@ -762,3 +762,87 @@ func TestPopulateToolChoice(t *testing.T) {
762762
assert.Equal(t, "tool choice=unsupported not support", err.Error())
763763
})
764764
}
765+
766+
func TestCacheTTL(t *testing.T) {
767+
t.Run("SetMessageBreakpoint without TTL", func(t *testing.T) {
768+
msg := schema.UserMessage("hello")
769+
bp := SetMessageBreakpoint(msg)
770+
assert.True(t, isBreakpointMessage(bp))
771+
ctrl := getMessageBreakpointCacheControl(bp)
772+
assert.Nil(t, ctrl)
773+
})
774+
775+
t.Run("SetMessageCacheControl with TTL", func(t *testing.T) {
776+
msg := schema.UserMessage("hello")
777+
bp := SetMessageCacheControl(msg, &CacheControl{TTL: CacheTTL1h})
778+
assert.True(t, isBreakpointMessage(bp))
779+
ctrl := getMessageBreakpointCacheControl(bp)
780+
assert.Equal(t, CacheTTL1h, ctrl.TTL)
781+
})
782+
783+
t.Run("SetToolInfoBreakpoint without TTL", func(t *testing.T) {
784+
tool := &schema.ToolInfo{Name: "test"}
785+
bp := SetToolInfoBreakpoint(tool)
786+
assert.True(t, isBreakpointTool(bp))
787+
ctrl := getToolBreakpointCacheControl(bp)
788+
assert.Nil(t, ctrl)
789+
})
790+
791+
t.Run("SetToolInfoCacheControl with TTL", func(t *testing.T) {
792+
tool := &schema.ToolInfo{Name: "test"}
793+
bp := SetToolInfoCacheControl(tool, &CacheControl{TTL: CacheTTL5m})
794+
assert.True(t, isBreakpointTool(bp))
795+
ctrl := getToolBreakpointCacheControl(bp)
796+
assert.Equal(t, CacheTTL5m, ctrl.TTL)
797+
})
798+
799+
t.Run("newCacheControlParam without TTL", func(t *testing.T) {
800+
p := newCacheControlParam(nil)
801+
assert.Equal(t, CacheTTL(""), p.TTL)
802+
assert.NotEmpty(t, p.Type)
803+
})
804+
805+
t.Run("newCacheControlParam with TTL", func(t *testing.T) {
806+
p := newCacheControlParam(&CacheControl{TTL: CacheTTL1h})
807+
assert.Equal(t, CacheTTL1h, p.TTL)
808+
assert.NotEmpty(t, p.Type)
809+
})
810+
811+
t.Run("newCacheControlParam with invalid TTL passthrough", func(t *testing.T) {
812+
p := newCacheControlParam(&CacheControl{TTL: "invalid_ttl"})
813+
assert.Equal(t, CacheTTL("invalid_ttl"), p.TTL)
814+
assert.NotEmpty(t, p.Type)
815+
})
816+
817+
t.Run("populateContentBlockBreakPoint with TTL", func(t *testing.T) {
818+
block := anthropic.NewTextBlock("input")
819+
populateContentBlockBreakPoint(block, &CacheControl{TTL: CacheTTL1h})
820+
assert.NotEmpty(t, block.OfText.CacheControl.Type)
821+
assert.Equal(t, CacheTTL1h, block.OfText.CacheControl.TTL)
822+
})
823+
824+
t.Run("manual breakpoint TTL flows to params", func(t *testing.T) {
825+
cm := &ChatModel{model: "test", maxTokens: 100}
826+
msg := schema.UserMessage("hello")
827+
sysMsg := schema.SystemMessage("system")
828+
bpSys := SetMessageCacheControl(sysMsg, &CacheControl{TTL: CacheTTL1h})
829+
830+
params, err := cm.genMessageNewParams([]*schema.Message{bpSys, msg})
831+
assert.NoError(t, err)
832+
assert.Equal(t, CacheTTL1h, params.System[0].CacheControl.TTL)
833+
})
834+
835+
t.Run("WithAutoCacheControl with TTL", func(t *testing.T) {
836+
cm := &ChatModel{model: "test", maxTokens: 100}
837+
msg := schema.UserMessage("hello")
838+
sysMsg := schema.SystemMessage("system")
839+
840+
params, err := cm.genMessageNewParams(
841+
[]*schema.Message{sysMsg, msg},
842+
WithAutoCacheControl(&CacheControl{TTL: CacheTTL1h}),
843+
)
844+
assert.NoError(t, err)
845+
// auto cache should set TTL on last system message
846+
assert.Equal(t, CacheTTL1h, params.System[0].CacheControl.TTL)
847+
})
848+
}

components/model/claude/examples/claude_prompt_cache/claude_prompt_cache.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import (
2222
"os"
2323
"time"
2424

25-
"github.com/cloudwego/eino-ext/components/model/claude"
2625
"github.com/cloudwego/eino/components/model"
2726
"github.com/cloudwego/eino/schema"
27+
28+
"github.com/cloudwego/eino-ext/components/model/claude"
2829
)
2930

3031
func main() {
@@ -56,10 +57,11 @@ func systemCache() {
5657
log.Fatalf("NewChatModel of claude failed, err=%v", err)
5758
}
5859

59-
breakpoint := claude.SetMessageBreakpoint(&schema.Message{
60+
// SetMessageCacheControl sets a manual cache breakpoint with a 1-hour TTL.
61+
breakpoint := claude.SetMessageCacheControl(&schema.Message{
6062
Role: schema.System,
6163
Content: "The film Dongji Rescue, based on a true historical event, tells the story of Chinese fishermen braving turbulent seas to save strangers — a testament to the nation's capacity to create miracles in the face of adversity.\n\nEighty-three years ago, in 1942, the Japanese military seized the Lisbon Maru ship to transport 1,816 British prisoners of war from Hong Kong to Japan. Passing through the waters near Zhoushan, Zhejiang province, it was torpedoed by a US submarine. Fishermen from Zhoushan rescued 384 prisoners of war and hid them from Japanese search parties.\n\nActor Zhu Yilong, in an exclusive interview with China Daily, said he was deeply moved by the humanity shown in such extreme conditions when he accepted his role. \"This historical event proves that in dire circumstances, Chinese people can extend their goodness to protect both themselves and others,\" he said.\n\nLast Friday, a themed event for Dongji Rescue was held in Zhoushan, a city close to where the Lisbon Maru sank. Descendants of the British POWs and the rescuing fishermen gathered to watch the film and share their reflections.\n\nIn the film, the British POWs are aided by a group of fishermen from Dongji Island, whose courage and compassion cut a path through treacherous waves. After the screening, many descendants were visibly moved.\n\n\"I want to express my deepest gratitude to the Chinese people and the descendants of Chinese fishermen. When the film is released in the UK, I will bring my family and friends to watch it. Heroic acts like this deserve to be known worldwide,\" said Denise Wynne, a descendant of a British POW.\n\n\"I felt the profound friendship between the Chinese and British people through the film,\" said Li Hui, a descendant of a rescuer. Many audience members were brought to tears — some by the fishermen's bravery, others by the enduring spirit of \"never abandoning those in peril\".\n\n\"In times of sea peril, rescue is a must,\" said Wu Buwei, another rescuer's descendant, noting that this value has long been a part of Dongji fishermen's traditions.\n\nCoincidentally, on the morning of Aug 5, a real-life rescue unfolded on the shores of Dongfushan in Dongji town. Two tourists from Shanghai fell into the sea and were swept away by powerful waves. Without hesitation, two descendants of Dongji fishermen — Wang Yubin and Yang Xiaoping — leaped in to save them, braving a wind force of 9 to 10 before pulling them to safety.\n\n\"Our forebearers once rescued many British POWs here. As their descendants, we should carry forward their bravery,\" Wang said. Both rescuers later joined the film's cast and crew at the Zhoushan event, sharing the stage with actors who portrayed the fishermen in a symbolic \"reunion across 83 years\".\n\nChinese actor Wu Lei, who plays A Dang in the film, expressed his respect for the two rescuing fishermen on-site. In the film, A Dang saves a British POW named Thomas Newman. Though they share no common language, they manage to connect. In an interview with China Daily, Wu said, \"They connected through a small globe. A Dang understood when Newman pointed out his home, the UK, on the globe.\"\n\nThe film's director Fei Zhenxiang noted that the Eastern Front in World War II is often overlooked internationally, despite China's 14 years of resistance tying down large numbers of Japanese troops. \"Every Chinese person is a hero, and their righteousness should not be forgotten,\" he said.\n\nGuan Hu, codirector, said, \"We hope that through this film, silent kindness can be seen by the world\", marking the 80th anniversary of victory in the Chinese People's War of Resistance Against Japanese Aggression (1931-45) and the World Anti-Fascist War.",
62-
})
64+
}, &claude.CacheControl{TTL: claude.CacheTTL1h})
6365

6466
for i := 0; i < 2; i++ {
6567
now := time.Now()
@@ -107,7 +109,8 @@ func toolInfoCache() {
107109
log.Fatalf("NewChatModel of claude failed, err=%v", err)
108110
}
109111

110-
breakpoint := claude.SetToolInfoBreakpoint(&schema.ToolInfo{
112+
// SetToolInfoCacheControl sets a manual cache breakpoint on the tool with a 5-minute TTL.
113+
breakpoint := claude.SetToolInfoCacheControl(&schema.ToolInfo{
111114
Name: "get_time",
112115
Desc: "Get the current time in a given time zone",
113116
ParamsOneOf: schema.NewParamsOneOfByParams(
@@ -119,7 +122,7 @@ func toolInfoCache() {
119122
},
120123
},
121124
)},
122-
)
125+
&claude.CacheControl{TTL: claude.CacheTTL5m})
123126

124127
mockTools := []*schema.ToolInfo{
125128
{
@@ -230,8 +233,9 @@ func sessionCache() {
230233
log.Fatalf("NewChatModel of claude failed, err=%v", err)
231234
}
232235

236+
// WithAutoCacheControl enables automatic caching with a 1-hour TTL.
233237
opts := []model.Option{
234-
claude.WithEnableAutoCache(true),
238+
claude.WithAutoCacheControl(&claude.CacheControl{TTL: claude.CacheTTL1h}),
235239
}
236240

237241
mockTools := []*schema.ToolInfo{

components/model/claude/go.mod

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/cloudwego/eino-ext/components/model/claude
33
go 1.23.0
44

55
require (
6-
github.com/anthropics/anthropic-sdk-go v1.4.0
6+
github.com/anthropics/anthropic-sdk-go v1.26.0
77
github.com/aws/aws-sdk-go-v2/config v1.29.1
88
github.com/aws/aws-sdk-go-v2/credentials v1.17.54
99
github.com/bytedance/mockey v1.2.13
@@ -73,13 +73,13 @@ require (
7373
go.opentelemetry.io/otel/metric v1.24.0 // indirect
7474
go.opentelemetry.io/otel/trace v1.24.0 // indirect
7575
golang.org/x/arch v0.11.0 // indirect
76-
golang.org/x/crypto v0.39.0 // indirect
76+
golang.org/x/crypto v0.40.0 // indirect
7777
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
78-
golang.org/x/net v0.27.0 // indirect
79-
golang.org/x/oauth2 v0.21.0 // indirect
80-
golang.org/x/sync v0.15.0 // indirect
81-
golang.org/x/sys v0.33.0 // indirect
82-
golang.org/x/text v0.26.0 // indirect
78+
golang.org/x/net v0.41.0 // indirect
79+
golang.org/x/oauth2 v0.30.0 // indirect
80+
golang.org/x/sync v0.16.0 // indirect
81+
golang.org/x/sys v0.34.0 // indirect
82+
golang.org/x/text v0.27.0 // indirect
8383
golang.org/x/time v0.5.0 // indirect
8484
google.golang.org/api v0.189.0 // indirect
8585
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect

0 commit comments

Comments
 (0)