From de4dca62a53524b41422adfddca6b3148015b54d Mon Sep 17 00:00:00 2001 From: alexdarbyshire <157761717+alexdarbyshire@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:45:01 +1000 Subject: [PATCH 1/4] fix(adk): encode nil tool-call args as {} for OpenAI chat completions Signed-off-by: alexdarbyshire <157761717+alexdarbyshire@users.noreply.github.com> --- go/adk/pkg/models/openai_adk.go | 6 +++++- go/adk/pkg/models/openai_adk_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/go/adk/pkg/models/openai_adk.go b/go/adk/pkg/models/openai_adk.go index 89665545f1..e1429bd527 100644 --- a/go/adk/pkg/models/openai_adk.go +++ b/go/adk/pkg/models/openai_adk.go @@ -269,7 +269,11 @@ 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) + args := fc.Args + if args == nil { + args = map[string]any{} + } + argsJSON, _ := json.Marshal(args) toolCall := openai.ChatCompletionMessageFunctionToolCallParam{ ID: fc.ID, Type: constant.Function(openAIToolTypeFunction), diff --git a/go/adk/pkg/models/openai_adk_test.go b/go/adk/pkg/models/openai_adk_test.go index dde5959be8..7352c00ca5 100644 --- a/go/adk/pkg/models/openai_adk_test.go +++ b/go/adk/pkg/models/openai_adk_test.go @@ -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) { From 0313acd234a5f6ee512d102795b36376da777acf Mon Sep 17 00:00:00 2001 From: alexdarbyshire <157761717+alexdarbyshire@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:45:01 +1000 Subject: [PATCH 2/4] fix(adk): encode nil tool-call args as {} for OpenAI Responses Signed-off-by: alexdarbyshire <157761717+alexdarbyshire@users.noreply.github.com> --- go/adk/pkg/models/openai_responses.go | 6 +++++- go/adk/pkg/models/openai_responses_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/go/adk/pkg/models/openai_responses.go b/go/adk/pkg/models/openai_responses.go index a39810dd58..a870d05822 100644 --- a/go/adk/pkg/models/openai_responses.go +++ b/go/adk/pkg/models/openai_responses.go @@ -124,7 +124,11 @@ func genaiContentsToResponsesInput(contents []*genai.Content, config *genai.Gene )) } for _, fc := range functionCalls { - argsJSON, _ := json.Marshal(fc.Args) + args := fc.Args + if args == nil { + args = map[string]any{} + } + argsJSON, _ := json.Marshal(args) input = append(input, responses.ResponseInputItemParamOfFunctionCall( string(argsJSON), fc.ID, diff --git a/go/adk/pkg/models/openai_responses_test.go b/go/adk/pkg/models/openai_responses_test.go index bbc9e1b0b4..920155084b 100644 --- a/go/adk/pkg/models/openai_responses_test.go +++ b/go/adk/pkg/models/openai_responses_test.go @@ -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) { From 76c6ec6e8bcdbcdacebe2456bc10aea4998f8f43 Mon Sep 17 00:00:00 2001 From: alexdarbyshire <157761717+alexdarbyshire@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:05:05 +1000 Subject: [PATCH 3/4] fix(adk): encode nil tool-call args as {} for SAP AI note - defensive. SAP AI allows BYO model, I have not verified whether their LLM proxy will autofix or 422/500 "null" string Signed-off-by: alexdarbyshire <157761717+alexdarbyshire@users.noreply.github.com> --- go/adk/pkg/models/sapaicore_adk.go | 6 +++++- go/adk/pkg/models/sapaicore_adk_test.go | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/go/adk/pkg/models/sapaicore_adk.go b/go/adk/pkg/models/sapaicore_adk.go index 96d92c644b..d3b4e87078 100644 --- a/go/adk/pkg/models/sapaicore_adk.go +++ b/go/adk/pkg/models/sapaicore_adk.go @@ -211,7 +211,11 @@ 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) + args := fc.Args + if args == nil { + args = map[string]any{} + } + argsJSON, _ := json.Marshal(args) tc := map[string]any{ "type": "function", "function": map[string]any{ diff --git a/go/adk/pkg/models/sapaicore_adk_test.go b/go/adk/pkg/models/sapaicore_adk_test.go index 39a199ceb5..2f85887982 100644 --- a/go/adk/pkg/models/sapaicore_adk_test.go +++ b/go/adk/pkg/models/sapaicore_adk_test.go @@ -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) { From 0efab2f6bdd71bf39c53a02310a1f5314974fd05 Mon Sep 17 00:00:00 2001 From: alexdarbyshire <157761717+alexdarbyshire@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:46:36 +1000 Subject: [PATCH 4/4] refactor(adk): extract non-nil function call argument helper Signed-off-by: alexdarbyshire <157761717+alexdarbyshire@users.noreply.github.com> --- go/adk/pkg/models/base.go | 13 +++++++++++++ go/adk/pkg/models/bedrock.go | 14 +------------- go/adk/pkg/models/openai_adk.go | 6 +----- go/adk/pkg/models/openai_responses.go | 6 +----- go/adk/pkg/models/sapaicore_adk.go | 6 +----- 5 files changed, 17 insertions(+), 28 deletions(-) diff --git a/go/adk/pkg/models/base.go b/go/adk/pkg/models/base.go index ea22a3d639..b4bddc45e7 100644 --- a/go/adk/pkg/models/base.go +++ b/go/adk/pkg/models/base.go @@ -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) diff --git a/go/adk/pkg/models/bedrock.go b/go/adk/pkg/models/bedrock.go index e7d8701753..049ff64a52 100644 --- a/go/adk/pkg/models/bedrock.go +++ b/go/adk/pkg/models/bedrock.go @@ -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, diff --git a/go/adk/pkg/models/openai_adk.go b/go/adk/pkg/models/openai_adk.go index e1429bd527..59040741e1 100644 --- a/go/adk/pkg/models/openai_adk.go +++ b/go/adk/pkg/models/openai_adk.go @@ -269,11 +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 { - args := fc.Args - if args == nil { - args = map[string]any{} - } - argsJSON, _ := json.Marshal(args) + argsJSON, _ := json.Marshal(nonNilFunctionCallArgs(fc.Args)) toolCall := openai.ChatCompletionMessageFunctionToolCallParam{ ID: fc.ID, Type: constant.Function(openAIToolTypeFunction), diff --git a/go/adk/pkg/models/openai_responses.go b/go/adk/pkg/models/openai_responses.go index a870d05822..9ef4eb45d0 100644 --- a/go/adk/pkg/models/openai_responses.go +++ b/go/adk/pkg/models/openai_responses.go @@ -124,11 +124,7 @@ func genaiContentsToResponsesInput(contents []*genai.Content, config *genai.Gene )) } for _, fc := range functionCalls { - args := fc.Args - if args == nil { - args = map[string]any{} - } - argsJSON, _ := json.Marshal(args) + argsJSON, _ := json.Marshal(nonNilFunctionCallArgs(fc.Args)) input = append(input, responses.ResponseInputItemParamOfFunctionCall( string(argsJSON), fc.ID, diff --git a/go/adk/pkg/models/sapaicore_adk.go b/go/adk/pkg/models/sapaicore_adk.go index d3b4e87078..6ed53d9965 100644 --- a/go/adk/pkg/models/sapaicore_adk.go +++ b/go/adk/pkg/models/sapaicore_adk.go @@ -211,11 +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 { - args := fc.Args - if args == nil { - args = map[string]any{} - } - argsJSON, _ := json.Marshal(args) + argsJSON, _ := json.Marshal(nonNilFunctionCallArgs(fc.Args)) tc := map[string]any{ "type": "function", "function": map[string]any{