Skip to content
Merged
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: 10 additions & 3 deletions go/adk/pkg/models/ollama_adk.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"
"iter"
"maps"
"slices"
"strings"

"github.com/google/uuid"
Expand Down Expand Up @@ -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}
Expand All @@ -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
}
Expand Down
48 changes: 48 additions & 0 deletions go/adk/pkg/models/ollama_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package models

import (
"reflect"
"slices"
"testing"

"google.golang.org/genai"
Expand Down Expand Up @@ -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 := 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 !slices.IsSorted(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
}
Loading