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
13 changes: 13 additions & 0 deletions go/adk/pkg/models/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.base.RoundTrip(req)
}

// nonNilFunctionCallArgs returns args, substituting an empty map for nil.
// A no-argument FunctionCall reloads from the session store with nil Args
// (omitempty drops the empty map), and some providers reject null or "null".
//
// TODO: remove once the pinned go-genai fixes
// https://github.com/googleapis/go-genai/issues/920.
func nonNilFunctionCallArgs(args map[string]any) map[string]any {
if args == nil {
return map[string]any{}
}
return args
}

// parametersJsonSchemaToMap converts a genai.FunctionDeclaration.ParametersJsonSchema value
// to map[string]any. ParametersJsonSchema is typed as `any` and can hold:
// - map[string]any (rare — only if someone constructs it manually)
Expand Down
14 changes: 1 addition & 13 deletions go/adk/pkg/models/bedrock.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,22 +675,10 @@ func convertGenaiContentsToBedrockMessages(contents []*genai.Content, nameMap ma
if sanitized, ok := nameMap[callName]; ok {
callName = sanitized
}
// Bedrock requires toolUse.input to be a JSON object. A tool call
// with no arguments arrives here with a nil Args map (genai's
// FunctionCall.Args is `json:"args,omitempty"`, so an empty map is
// dropped when the event is persisted to the session store and
// reloaded as nil). NewLazyDocument(nil) serializes to `null`, which
// Bedrock rejects with "ValidationException: Malformed input request"
// ("The value at messages.N.content.M.toolUse.input is empty").
// Coerce nil to an empty object so no-argument tool calls round-trip.
args := part.FunctionCall.Args
if args == nil {
args = map[string]any{}
}
toolUse := types.ToolUseBlock{
ToolUseId: aws.String(sanitizeBedrockToolID(part.FunctionCall.ID, idMap, &idCounter)),
Name: aws.String(callName),
Input: document.NewLazyDocument(args),
Input: document.NewLazyDocument(nonNilFunctionCallArgs(part.FunctionCall.Args)),
}
contentBlocks = append(contentBlocks, &types.ContentBlockMemberToolUse{
Value: toolUse,
Expand Down
2 changes: 1 addition & 1 deletion go/adk/pkg/models/openai_adk.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ func genaiContentsToOpenAIMessages(contents []*genai.Content, config *genai.Gene
toolCalls := make([]openai.ChatCompletionMessageToolCallUnionParam, 0, len(functionCalls))
var toolResponseMessages []openai.ChatCompletionMessageParamUnion
for _, fc := range functionCalls {
argsJSON, _ := json.Marshal(fc.Args)
argsJSON, _ := json.Marshal(nonNilFunctionCallArgs(fc.Args))
toolCall := openai.ChatCompletionMessageFunctionToolCallParam{
ID: fc.ID,
Type: constant.Function(openAIToolTypeFunction),
Expand Down
13 changes: 13 additions & 0 deletions go/adk/pkg/models/openai_adk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,19 @@ func TestGenaiContentsToOpenAIMessages(t *testing.T) {
t.Errorf("len(messages) = %d, want 1", len(msgs))
}
})

t.Run("nil args (replay regression) encode as empty object", func(t *testing.T) {
contents := []*genai.Content{
{Role: "model", Parts: []*genai.Part{{FunctionCall: &genai.FunctionCall{ID: "call_1", Name: "ping", Args: nil}}}},
}
msgs, _ := genaiContentsToOpenAIMessages(contents, nil)
if len(msgs) == 0 || msgs[0].OfAssistant == nil || len(msgs[0].OfAssistant.ToolCalls) != 1 {
t.Fatalf("messages = %#v, want first message to be assistant with 1 tool call", msgs)
}
if got := msgs[0].OfAssistant.ToolCalls[0].GetFunction().Arguments; got != `{}` {
t.Errorf("arguments = %q, want {}", got)
}
})
}

func TestApplyOpenAIConfig(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion go/adk/pkg/models/openai_responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func genaiContentsToResponsesInput(contents []*genai.Content, config *genai.Gene
))
}
for _, fc := range functionCalls {
argsJSON, _ := json.Marshal(fc.Args)
argsJSON, _ := json.Marshal(nonNilFunctionCallArgs(fc.Args))
input = append(input, responses.ResponseInputItemParamOfFunctionCall(
string(argsJSON),
fc.ID,
Expand Down
13 changes: 13 additions & 0 deletions go/adk/pkg/models/openai_responses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ func TestGenaiContentsToResponsesInput(t *testing.T) {
t.Fatalf("output = %q, want 3", got)
}
})

t.Run("nil args (replay regression) encode as empty object", func(t *testing.T) {
contents := []*genai.Content{
{Role: "model", Parts: []*genai.Part{{FunctionCall: &genai.FunctionCall{ID: "call_1", Name: "ping", Args: nil}}}},
}
input, _ := genaiContentsToResponsesInput(contents, nil)
if len(input) == 0 || input[0].OfFunctionCall == nil {
t.Fatalf("input = %#v, want function_call item", input)
}
if got := input[0].OfFunctionCall.Arguments; got != `{}` {
t.Errorf("arguments = %q, want {}", got)
}
})
}

func TestGenaiToolsToResponsesTools(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion go/adk/pkg/models/sapaicore_adk.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func genaiContentsToOrchTemplate(contents []*genai.Content, config *genai.Genera
toolCalls := make([]map[string]any, 0, len(functionCalls))
var toolResponses []map[string]any
for _, fc := range functionCalls {
argsJSON, _ := json.Marshal(fc.Args)
argsJSON, _ := json.Marshal(nonNilFunctionCallArgs(fc.Args))
tc := map[string]any{
"type": "function",
"function": map[string]any{
Expand Down
21 changes: 21 additions & 0 deletions go/adk/pkg/models/sapaicore_adk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,27 @@ func TestGenaiContentsToOrchTemplate_ToolCall(t *testing.T) {
if toolCalls[0]["id"] != "call_1" {
t.Errorf("tool_calls[0].id = %v, want call_1", toolCalls[0]["id"])
}

t.Run("nil args (replay regression) encode as empty object", func(t *testing.T) {
contents := []*genai.Content{
{Role: "model", Parts: []*genai.Part{{FunctionCall: &genai.FunctionCall{ID: "call_1", Name: "ping", Args: nil}}}},
}
msgs, _ := genaiContentsToOrchTemplate(contents, nil)
if len(msgs) == 0 {
t.Fatal("expected at least 1 message")
}
toolCalls, ok := msgs[0]["tool_calls"].([]map[string]any)
if !ok || len(toolCalls) == 0 {
t.Fatalf("tool_calls = %v, want non-empty slice", msgs[0]["tool_calls"])
}
fn, ok := toolCalls[0]["function"].(map[string]any)
if !ok {
t.Fatalf("tool_calls[0].function = %v, want map", toolCalls[0]["function"])
}
if fn["arguments"] != `{}` {
t.Errorf("tool_calls[0].function.arguments = %v, want {}", fn["arguments"])
}
})
}

func TestGenaiContentsToOrchTemplate_FunctionResponse(t *testing.T) {
Expand Down
Loading