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
6 changes: 6 additions & 0 deletions components/model/agenticclaude/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,9 @@ const (
)

const implType = "AgenticClaude"

// KeyOfCacheCreationInputTokens is the Extra map key for Anthropic
// cache_creation_input_tokens. It uses the same key as the legacy claude
// adapter so downstream consumers (e.g. Langfuse cost tracking) do not need
// to distinguish between the two adapters.
const KeyOfCacheCreationInputTokens = "_eino_claude_cache_creation_input_tokens"
10 changes: 8 additions & 2 deletions components/model/agenticclaude/convertor.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,11 +574,17 @@ func toAgenticMessage(resp *anthropic.Message) (*schema.AgenticMessage, error) {
blocks = append(blocks, contentBlock)
}

return &schema.AgenticMessage{
msg := &schema.AgenticMessage{
Role: schema.AgenticRoleTypeAssistant,
ContentBlocks: blocks,
ResponseMeta: toAgenticResponseMeta(resp),
}, nil
}
if resp.Usage.CacheCreationInputTokens > 0 {
msg.Extra = map[string]any{
KeyOfCacheCreationInputTokens: int(resp.Usage.CacheCreationInputTokens),
}
}
return msg, nil
}

func toAgenticContentBlock(block any) (*schema.ContentBlock, error) {
Expand Down
10 changes: 8 additions & 2 deletions components/model/agenticclaude/event_convertor.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ func (c *streamConverter) toMessageStreamingChunk(event anthropic.MessageStreamE
case anthropic.MessageStartEvent:
return toAgenticMessage(&e.Message)
case anthropic.MessageDeltaEvent:
return &schema.AgenticMessage{
msg := &schema.AgenticMessage{
Role: schema.AgenticRoleTypeAssistant,
ResponseMeta: toDeltaResponseMeta(e),
}, nil
}
if e.Usage.CacheCreationInputTokens > 0 {
msg.Extra = map[string]any{
KeyOfCacheCreationInputTokens: int(e.Usage.CacheCreationInputTokens),
}
}
return msg, nil
case anthropic.ContentBlockStartEvent:
contentBlock, err := c.toStreamingStartBlock(e.Index, e.ContentBlock.AsAny())
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion components/model/agenticclaude/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/eino-contrib/jsonschema v1.0.3
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/wk8/go-ordered-map/v2 v2.1.8
golang.org/x/oauth2 v0.30.0
)

require (
Expand Down Expand Up @@ -73,7 +74,6 @@ require (
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
Expand Down
8 changes: 1 addition & 7 deletions components/model/agenticclaude/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJ
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
github.com/anthropics/anthropic-sdk-go v1.42.0 h1:Zv882/dnrE4OHnwhMAsi9lwVVXRF8GtR3ofiBResYUw=
github.com/anthropics/anthropic-sdk-go v1.42.0/go.mod h1:r4eaLX9tBolUrXLOrLj7eU8tmeBtoobCkM0kBsivBaY=
github.com/anthropics/anthropic-sdk-go v1.56.0 h1:idVU14wOZ06D0GBNEvuhn927xXmBVEquo0469iDwLsc=
github.com/anthropics/anthropic-sdk-go v1.56.0/go.mod h1:3EfIfmFqxH6rbiLcIP4tPFyXL/IHakx2wDG4OU+TIEI=
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
Expand Down Expand Up @@ -125,8 +123,6 @@ github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg=
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
Expand Down Expand Up @@ -181,8 +177,6 @@ github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGB
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20260508151727-1282bb917829 h1:zGlGD0Zfk2HaIo4EnUVBRhnXQ+cnGQz5X2PdBcplOyw=
github.com/standard-webhooks/standard-webhooks/libraries v0.0.0-20260508151727-1282bb917829/go.mod h1:L1MQhA6x4dn9r007T033lsaZMv9EmBAdXyU/+EF40fo=
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 h1:uOfcYT+3QungH6tIGSVCR/Y3KJmgJiHcojJbMTPDZAI=
github.com/standard-webhooks/standard-webhooks/libraries v0.0.1/go.mod h1:L1MQhA6x4dn9r007T033lsaZMv9EmBAdXyU/+EF40fo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand All @@ -197,9 +191,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
Expand Down
17 changes: 17 additions & 0 deletions components/model/agenticclaude/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/anthropics/anthropic-sdk-go/vertex"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"golang.org/x/oauth2/google"

"github.com/cloudwego/eino/callbacks"
"github.com/cloudwego/eino/components"
Expand Down Expand Up @@ -142,6 +143,19 @@ type GoogleVertexAIConfig struct {
// from CLOUD_ML_REGION environment variable.
// See: https://claude.ai/docs/en/google-vertex-ai
Region string

// Credentials provides explicit Google credentials for Vertex AI authentication.
// When set, vertex.WithCredentials is used instead of the ambient ADC
// (vertex.WithGoogleAuth). This avoids relying on GOOGLE_APPLICATION_CREDENTIALS
// environment variable, which is process-global and unsafe under concurrent
// or hot-reload scenarios.
//
// Example:
// creds, _ := google.CredentialsFromJSON(ctx, jsonKey, "https://www.googleapis.com/auth/cloud-platform")
// cfg.ByGoogleVertexAI = &GoogleVertexAIConfig{Credentials: creds, ...}
//
// Optional. When nil, falls back to ambient ADC via WithGoogleAuth.
Credentials *google.Credentials
}

type BedrockConfig struct {
Expand Down Expand Up @@ -256,6 +270,9 @@ func newClient(ctx context.Context, cfg *Config) (anthropic.Client, error) {
return anthropic.Client{}, errors.New("ByGoogleVertexAI is set but no region provided; set Region or CLOUD_ML_REGION")
}

if cfg.ByGoogleVertexAI.Credentials != nil {
return anthropic.NewClient(vertex.WithCredentials(ctx, region, projectID, cfg.ByGoogleVertexAI.Credentials)), nil
}
return anthropic.NewClient(vertex.WithGoogleAuth(ctx, region, projectID)), nil

case cfg.ByBedrock != nil:
Expand Down
36 changes: 31 additions & 5 deletions components/model/agenticgemini/conv.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,11 @@ func convUserInputFile(file *schema.UserInputFile) (*genai.Part, error) {

func convFunctionToolCall(call *schema.FunctionToolCall) (*genai.Part, error) {
args := make(map[string]any)
err := sonic.UnmarshalString(call.Arguments, &args)
if err != nil {
return nil, fmt.Errorf("unmarshal function tool call arguments to map[string]any fail: %w", err)
if call.Arguments != "" {
err := sonic.UnmarshalString(call.Arguments, &args)
if err != nil {
return nil, fmt.Errorf("unmarshal function tool call arguments to map[string]any fail: %w", err)
}
}

return genai.NewPartFromFunctionCall(call.Name, args), nil
Expand All @@ -353,7 +355,11 @@ func convFunctionToolResult(result *schema.FunctionToolResult) (*genai.Part, err
if err != nil {
response["output"] = text
}
return genai.NewPartFromFunctionResponse(result.Name, response), nil
name := result.Name
if name == "" {
name = result.CallID
}
return genai.NewPartFromFunctionResponse(name, response), nil
}

func functionToolResultContentToText(content []*schema.FunctionToolResultContentBlock) (string, error) {
Expand Down Expand Up @@ -650,7 +656,6 @@ func populateStreamingMeta(curBlocks []*schema.ContentBlock, curIndex int, lastT
return curIndex, lastType
}
if len(lastType) > 0 && curBlocks[0].Type != lastType {
// a new part, index++
curIndex++
}

Expand All @@ -664,9 +669,30 @@ func populateStreamingMeta(curBlocks []*schema.ContentBlock, curIndex int, lastT
}
curBlocks[i].StreamingMeta = &schema.StreamingMeta{Index: curIndex}

// Discrete block types (tool calls/results) are self-contained — each one is a
// complete unit, not a partial chunk to be concatenated. Advance curIndex so the
// next chunk starts at a new index, preventing ConcatAgenticMessages from trying
// to merge different tool calls that happen to share the same streaming index.
if isDiscreteBlockType(curBlocks[i].Type) {
curIndex++
}

return curIndex, curBlocks[len(curBlocks)-1].Type
}

func isDiscreteBlockType(t schema.ContentBlockType) bool {
switch t {
case schema.ContentBlockTypeFunctionToolCall,
schema.ContentBlockTypeFunctionToolResult,
schema.ContentBlockTypeServerToolCall,
schema.ContentBlockTypeServerToolResult,
schema.ContentBlockTypeMCPToolCall,
schema.ContentBlockTypeMCPToolResult:
return true
}
return false
}

func toGeminiTools(tools []*schema.ToolInfo) ([]*genai.FunctionDeclaration, error) {
gTools := make([]*genai.FunctionDeclaration, len(tools))
for i, tool := range tools {
Expand Down
91 changes: 91 additions & 0 deletions components/model/agenticgemini/conv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1259,3 +1259,94 @@ func TestConvAgenticMessages_MergeToolResults(t *testing.T) {
assert.Len(t, contents, 2)
})
}

func TestConvFunctionToolResult_EmptyName(t *testing.T) {
t.Run("empty name falls back to CallID", func(t *testing.T) {
part, err := convFunctionToolResult(&schema.FunctionToolResult{
CallID: "call-fallback-1",
Content: []*schema.FunctionToolResultContentBlock{
{Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: `{"status":"ok"}`}},
},
})
assert.NoError(t, err)
assert.NotNil(t, part)
assert.NotNil(t, part.FunctionResponse)
assert.Equal(t, "call-fallback-1", part.FunctionResponse.Name)
assert.Equal(t, "ok", part.FunctionResponse.Response["status"])
})

t.Run("explicit name is preserved", func(t *testing.T) {
part, err := convFunctionToolResult(&schema.FunctionToolResult{
CallID: "call-1",
Name: "read_file",
Content: []*schema.FunctionToolResultContentBlock{
{Type: schema.FunctionToolResultContentBlockTypeText, Text: &schema.UserInputText{Text: "file contents"}},
},
})
assert.NoError(t, err)
assert.NotNil(t, part)
assert.Equal(t, "read_file", part.FunctionResponse.Name)
assert.Equal(t, "file contents", part.FunctionResponse.Response["output"])
})
}

func TestConvFunctionToolCall_EmptyArguments(t *testing.T) {
t.Run("empty string arguments treated as empty object", func(t *testing.T) {
part, err := convFunctionToolCall(&schema.FunctionToolCall{
Name: "task_complete",
Arguments: "",
})
assert.NoError(t, err)
assert.NotNil(t, part)
assert.Equal(t, "task_complete", part.FunctionCall.Name)
assert.Empty(t, part.FunctionCall.Args)
})

t.Run("valid JSON arguments", func(t *testing.T) {
part, err := convFunctionToolCall(&schema.FunctionToolCall{
Name: "read_file",
Arguments: `{"path":"src/App.tsx"}`,
})
assert.NoError(t, err)
assert.NotNil(t, part)
assert.Equal(t, "read_file", part.FunctionCall.Name)
assert.Equal(t, "src/App.tsx", part.FunctionCall.Args["path"])
})

t.Run("empty object arguments", func(t *testing.T) {
part, err := convFunctionToolCall(&schema.FunctionToolCall{
Name: "task_complete",
Arguments: "{}",
})
assert.NoError(t, err)
assert.NotNil(t, part)
assert.Empty(t, part.FunctionCall.Args)
})

t.Run("invalid JSON arguments returns error", func(t *testing.T) {
_, err := convFunctionToolCall(&schema.FunctionToolCall{
Name: "write_file",
Arguments: `{"path":"broken`,
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "unmarshal function tool call arguments")
})

t.Run("empty args in message conversion roundtrip", func(t *testing.T) {
msg := &schema.AgenticMessage{
Role: schema.AgenticRoleTypeAssistant,
ContentBlocks: []*schema.ContentBlock{
schema.NewContentBlock(&schema.FunctionToolCall{
CallID: "call-1",
Name: "task_complete",
Arguments: "",
}),
},
}
content, err := convAgenticMessage(msg)
assert.NoError(t, err)
assert.NotNil(t, content)
assert.Len(t, content.Parts, 1)
assert.Equal(t, "task_complete", content.Parts[0].FunctionCall.Name)
})
}
Loading