From 219de0b667f22a166d3e3f51a53b671d2b21031c Mon Sep 17 00:00:00 2001 From: Golden Garlic <148346166+garlicKim21@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:18:59 +0000 Subject: [PATCH 1/2] fix(adk): keep Ollama tool property order stable across requests convertGenaiToolsToOllama ranges over the Go map of parameter properties and Sets each one into an api.ToolPropertiesMap. That type preserves insertion order, so a randomized range order is carried straight into the serialized request: the tool definitions come out in a different order on every call. Ollama servers cache on the prompt prefix. Tool definitions sit near the front of the rendered prompt, so a reordered tool block invalidates the prefix and the whole prompt is re-processed every request. Measured against a live agent with 9 tools bound (8 of them with more than one property): 5,400 distinct serializations of the tool block, so two consecutive requests agree 0.02% of the time. In a 19-request session the server logged a full re-process 19 times, and prefill accounted for 64% of the session's wall clock. The same agent on the Python runtime hit the cache on every turn, because dict iteration order is deterministic there. Sort the property names before inserting. Both branches of the conversion have the same problem, so both are sorted. Signed-off-by: Golden Garlic <148346166+garlicKim21@users.noreply.github.com> --- go/adk/pkg/models/ollama_adk.go | 13 +++++++-- go/adk/pkg/models/ollama_test.go | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/go/adk/pkg/models/ollama_adk.go b/go/adk/pkg/models/ollama_adk.go index 82f4435366..35e01c76e7 100644 --- a/go/adk/pkg/models/ollama_adk.go +++ b/go/adk/pkg/models/ollama_adk.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "iter" + "maps" + "slices" "strings" "github.com/google/uuid" @@ -364,8 +366,11 @@ func convertGenaiToolsToOllama(tools []*genai.Tool) []api.Tool { // Ollama requires typed properties, so we convert to map[string]any first then to api.ToolProperty. if m := parametersJsonSchemaToMap(decl.ParametersJsonSchema); m != nil { if props, ok := m["properties"].(map[string]any); ok { - for name, propAny := range props { - if propMap, ok := propAny.(map[string]any); ok { + // Sort: api.ToolPropertiesMap preserves insertion order, so + // ranging the map directly would emit a different property + // order on every call and break prompt prefix caching. + for _, name := range slices.Sorted(maps.Keys(props)) { + if propMap, ok := props[name].(map[string]any); ok { prop := api.ToolProperty{} if t, ok := propMap["type"].(string); ok { prop.Type = api.PropertyType{t} @@ -389,7 +394,9 @@ func convertGenaiToolsToOllama(tools []*genai.Tool) []api.Tool { } } } else if decl.Parameters != nil { - for name, schema := range decl.Parameters.Properties { + // Sorted for the same reason as above. + for _, name := range slices.Sorted(maps.Keys(decl.Parameters.Properties)) { + schema := decl.Parameters.Properties[name] if schema == nil { continue } diff --git a/go/adk/pkg/models/ollama_test.go b/go/adk/pkg/models/ollama_test.go index 9df42d6c8d..00eb6fe128 100644 --- a/go/adk/pkg/models/ollama_test.go +++ b/go/adk/pkg/models/ollama_test.go @@ -2,6 +2,7 @@ package models import ( "reflect" + "sort" "testing" "google.golang.org/genai" @@ -214,3 +215,50 @@ func TestConvertGenaiContentsToOllamaMessages(t *testing.T) { }) } } + +// TestConvertGenaiToolsToOllamaPropertyOrderIsStable guards prompt prefix +// caching. api.ToolPropertiesMap preserves insertion order, so if the +// conversion ranges over the Go map of properties directly, every call emits a +// different property order. Servers that cache on the prompt prefix then +// re-process the whole prompt on every request. +func TestConvertGenaiToolsToOllamaPropertyOrderIsStable(t *testing.T) { + props := map[string]*genai.Schema{ + "project": {Type: genai.TypeString, Description: "project slug"}, + "family": {Type: genai.TypeString, Description: "change family"}, + "bucket": {Type: genai.TypeString, Description: "severity bucket"}, + "since": {Type: genai.TypeString, Description: "lower bound"}, + "limit": {Type: genai.TypeInteger, Description: "max rows"}, + } + tools := []*genai.Tool{{ + FunctionDeclarations: []*genai.FunctionDeclaration{{ + Name: "list_changes", + Parameters: &genai.Schema{Type: genai.TypeObject, Properties: props}, + }}, + }} + + want := names(t, tools) + for i := 0; i < 100; i++ { + if got := names(t, tools); !reflect.DeepEqual(got, want) { + t.Fatalf("property order changed between calls:\n first: %v\n call %d: %v", want, i, got) + } + } + + if !sort.StringsAreSorted(want) { + t.Errorf("property order is not deterministic across processes: %v", want) + } +} + +// names returns the parameter property names of the first converted tool, in +// the order they would be serialized. +func names(t *testing.T, tools []*genai.Tool) []string { + t.Helper() + converted := convertGenaiToolsToOllama(tools) + if len(converted) != 1 { + t.Fatalf("expected 1 tool, got %d", len(converted)) + } + var out []string + for name := range converted[0].Function.Parameters.Properties.All() { + out = append(out, name) + } + return out +} From 2a401035d080cfc6ebb83eb40a3ff7957dd4bc97 Mon Sep 17 00:00:00 2001 From: Golden Garlic <148346166+garlicKim21@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:06:51 +0000 Subject: [PATCH 2/2] test(adk): satisfy depguard and modernize in ollama property-order test Use slices.IsSorted instead of sort.StringsAreSorted and range over int, per the .golangci.yaml rules merged from main. Signed-off-by: Golden Garlic <148346166+garlicKim21@users.noreply.github.com> --- go/adk/pkg/models/ollama_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go/adk/pkg/models/ollama_test.go b/go/adk/pkg/models/ollama_test.go index 00eb6fe128..c1174b3c32 100644 --- a/go/adk/pkg/models/ollama_test.go +++ b/go/adk/pkg/models/ollama_test.go @@ -2,7 +2,7 @@ package models import ( "reflect" - "sort" + "slices" "testing" "google.golang.org/genai" @@ -237,13 +237,13 @@ func TestConvertGenaiToolsToOllamaPropertyOrderIsStable(t *testing.T) { }} want := names(t, tools) - for i := 0; i < 100; i++ { + for i := range 100 { if got := names(t, tools); !reflect.DeepEqual(got, want) { t.Fatalf("property order changed between calls:\n first: %v\n call %d: %v", want, i, got) } } - if !sort.StringsAreSorted(want) { + if !slices.IsSorted(want) { t.Errorf("property order is not deterministic across processes: %v", want) } }