From bf0dcd4a9286bbf23dd355c32f117d041b847bbb Mon Sep 17 00:00:00 2001 From: Anai-Guo Date: Sun, 23 Aug 2026 03:25:59 -0700 Subject: [PATCH 1/2] fix(functions): honor function_arguments_key when building the tool grammar All four call sites of `Functions.ToJSONStructure(name, args string)` pass `FunctionsConfig.FunctionNameKey` as *both* arguments, so `FunctionArgumentsKey` never reaches the grammar generator. `ToJSONStructure` writes the two properties into the same map: property[nameKey] = FunctionName{Const: function.Name} property[argsKey] = Argument{...} When `nameKey == argsKey` the second assignment overwrites the first, so a model configured with `function_name_key` gets a grammar carrying only the arguments object -- the `{"const": ""}` constraint is gone and the grammar can no longer express which function was called. With `function_name_key: function`, the generated property set collapses from {"function": {"const": "get_weather"}, "arguments": {...}} to {"function": {"type": "object", "properties": {...}}} Setting only `function_arguments_key` is equally broken in the other direction: the grammar keeps emitting `arguments` while `ParseFunctionCall` (pkg/functions/parse.go) looks up the configured key, so the parsed call comes back with its arguments empty. The default configuration is unaffected -- with both keys empty `ToJSONStructure` falls back to `name`/`arguments` for both parameters, which is why this went unnoticed. The existing `ToJSONStructure()` unit test already calls the helper with two distinct keys, so only the call sites were wrong. Extend that test with a case that keeps both custom keys distinct and asserts the two properties survive. Signed-off-by: Anai-Guo --- core/http/endpoints/openai/chat.go | 2 +- core/http/endpoints/openai/realtime_model.go | 2 +- .../http/endpoints/openresponses/responses.go | 2 +- .../http/endpoints/openresponses/websocket.go | 2 +- pkg/functions/functions_test.go | 28 +++++++++++++++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/core/http/endpoints/openai/chat.go b/core/http/endpoints/openai/chat.go index f863631f6630..b921440000e7 100644 --- a/core/http/endpoints/openai/chat.go +++ b/core/http/endpoints/openai/chat.go @@ -342,7 +342,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator } // Update input grammar or json_schema based on use_llama_grammar option - jsStruct := funcs.ToJSONStructure(config.FunctionsConfig.FunctionNameKey, config.FunctionsConfig.FunctionNameKey) + jsStruct := funcs.ToJSONStructure(config.FunctionsConfig.FunctionNameKey, config.FunctionsConfig.FunctionArgumentsKey) g, err := jsStruct.Grammar(config.FunctionsConfig.GrammarOptions()...) if err == nil { config.Grammar = g diff --git a/core/http/endpoints/openai/realtime_model.go b/core/http/endpoints/openai/realtime_model.go index 0449daee3740..9d48e25c0f37 100644 --- a/core/http/endpoints/openai/realtime_model.go +++ b/core/http/endpoints/openai/realtime_model.go @@ -248,7 +248,7 @@ func (m *wrappedModel) Predict(ctx context.Context, messages schema.Messages, im } // Generate grammar from function definitions - jsStruct := functions.Functions(funcs).ToJSONStructure(turnCfg.FunctionsConfig.FunctionNameKey, turnCfg.FunctionsConfig.FunctionNameKey) + jsStruct := functions.Functions(funcs).ToJSONStructure(turnCfg.FunctionsConfig.FunctionNameKey, turnCfg.FunctionsConfig.FunctionArgumentsKey) g, err := jsStruct.Grammar(turnCfg.FunctionsConfig.GrammarOptions()...) if err == nil { turnCfg.Grammar = g diff --git a/core/http/endpoints/openresponses/responses.go b/core/http/endpoints/openresponses/responses.go index f8d741508b35..e05c08b78c93 100644 --- a/core/http/endpoints/openresponses/responses.go +++ b/core/http/endpoints/openresponses/responses.go @@ -220,7 +220,7 @@ func ResponsesEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, eval } // Generate grammar to constrain model output to valid function calls - jsStruct := funcsWithNoAction.ToJSONStructure(cfg.FunctionsConfig.FunctionNameKey, cfg.FunctionsConfig.FunctionNameKey) + jsStruct := funcsWithNoAction.ToJSONStructure(cfg.FunctionsConfig.FunctionNameKey, cfg.FunctionsConfig.FunctionArgumentsKey) g, err := jsStruct.Grammar(cfg.FunctionsConfig.GrammarOptions()...) if err == nil { cfg.Grammar = g diff --git a/core/http/endpoints/openresponses/websocket.go b/core/http/endpoints/openresponses/websocket.go index ffff7b0445c1..deba852ff276 100644 --- a/core/http/endpoints/openresponses/websocket.go +++ b/core/http/endpoints/openresponses/websocket.go @@ -283,7 +283,7 @@ func handleWSResponseCreate(connCtx context.Context, conn *lockedConn, input *sc funcsWithNoAction = funcsWithNoAction.Select(cfg.FunctionToCall()) } - jsStruct := funcsWithNoAction.ToJSONStructure(cfg.FunctionsConfig.FunctionNameKey, cfg.FunctionsConfig.FunctionNameKey) + jsStruct := funcsWithNoAction.ToJSONStructure(cfg.FunctionsConfig.FunctionNameKey, cfg.FunctionsConfig.FunctionArgumentsKey) g, err := jsStruct.Grammar(cfg.FunctionsConfig.GrammarOptions()...) if err == nil { cfg.Grammar = g diff --git a/pkg/functions/functions_test.go b/pkg/functions/functions_test.go index e0952c13fac6..2f5f71b38733 100644 --- a/pkg/functions/functions_test.go +++ b/pkg/functions/functions_test.go @@ -65,6 +65,34 @@ var _ = Describe("LocalAI grammar functions", func() { Expect(fnName.Const).To(Equal("search")) Expect(fnArgs.Properties["query"].(map[string]any)["type"]).To(Equal("string")) }) + + It("keeps the name and the arguments in separate properties when both keys are customized", func() { + var functions Functions = []Function{ + { + Name: "get_weather", + Parameters: map[string]any{ + "properties": map[string]any{ + "city": map[string]any{ + "type": "string", + }, + }, + }, + }, + } + + // function_name_key / function_arguments_key are two independent + // settings. Passing the same key for both collapses the structure + // onto a single property, and the arguments overwrite the function + // name constant - leaving a grammar that cannot express which + // function was called. + js := functions.ToJSONStructure("function", "parameters") + Expect(js.OneOf[0].Properties).To(HaveLen(2)) + + fnName := js.OneOf[0].Properties["function"].(FunctionName) + fnArgs := js.OneOf[0].Properties["parameters"].(Argument) + Expect(fnName.Const).To(Equal("get_weather")) + Expect(fnArgs.Properties["city"].(map[string]any)["type"]).To(Equal("string")) + }) }) Context("Select()", func() { It("selects one of the functions and returns a list containing only the selected one", func() { From afeafd1fa854b9a523c09f79953729f59bcb4aea Mon Sep 17 00:00:00 2001 From: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:02:10 +0000 Subject: [PATCH 2/2] test(functions): cover configured grammar keys Route grammar construction through FunctionsConfig so the regression test covers the key wiring used by every endpoint. Assisted-by: Codex:gpt-5 --- core/http/endpoints/openai/chat.go | 2 +- core/http/endpoints/openai/realtime_model.go | 2 +- core/http/endpoints/openresponses/responses.go | 2 +- core/http/endpoints/openresponses/websocket.go | 2 +- pkg/functions/functions.go | 5 +++++ pkg/functions/functions_test.go | 11 +++++------ 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/core/http/endpoints/openai/chat.go b/core/http/endpoints/openai/chat.go index b921440000e7..4272ed772b08 100644 --- a/core/http/endpoints/openai/chat.go +++ b/core/http/endpoints/openai/chat.go @@ -342,7 +342,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator } // Update input grammar or json_schema based on use_llama_grammar option - jsStruct := funcs.ToJSONStructure(config.FunctionsConfig.FunctionNameKey, config.FunctionsConfig.FunctionArgumentsKey) + jsStruct := config.FunctionsConfig.ToJSONStructure(funcs) g, err := jsStruct.Grammar(config.FunctionsConfig.GrammarOptions()...) if err == nil { config.Grammar = g diff --git a/core/http/endpoints/openai/realtime_model.go b/core/http/endpoints/openai/realtime_model.go index 9d48e25c0f37..0c736aa80bc0 100644 --- a/core/http/endpoints/openai/realtime_model.go +++ b/core/http/endpoints/openai/realtime_model.go @@ -248,7 +248,7 @@ func (m *wrappedModel) Predict(ctx context.Context, messages schema.Messages, im } // Generate grammar from function definitions - jsStruct := functions.Functions(funcs).ToJSONStructure(turnCfg.FunctionsConfig.FunctionNameKey, turnCfg.FunctionsConfig.FunctionArgumentsKey) + jsStruct := turnCfg.FunctionsConfig.ToJSONStructure(functions.Functions(funcs)) g, err := jsStruct.Grammar(turnCfg.FunctionsConfig.GrammarOptions()...) if err == nil { turnCfg.Grammar = g diff --git a/core/http/endpoints/openresponses/responses.go b/core/http/endpoints/openresponses/responses.go index e05c08b78c93..20dc303df393 100644 --- a/core/http/endpoints/openresponses/responses.go +++ b/core/http/endpoints/openresponses/responses.go @@ -220,7 +220,7 @@ func ResponsesEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, eval } // Generate grammar to constrain model output to valid function calls - jsStruct := funcsWithNoAction.ToJSONStructure(cfg.FunctionsConfig.FunctionNameKey, cfg.FunctionsConfig.FunctionArgumentsKey) + jsStruct := cfg.FunctionsConfig.ToJSONStructure(funcsWithNoAction) g, err := jsStruct.Grammar(cfg.FunctionsConfig.GrammarOptions()...) if err == nil { cfg.Grammar = g diff --git a/core/http/endpoints/openresponses/websocket.go b/core/http/endpoints/openresponses/websocket.go index deba852ff276..91cfde4488cc 100644 --- a/core/http/endpoints/openresponses/websocket.go +++ b/core/http/endpoints/openresponses/websocket.go @@ -283,7 +283,7 @@ func handleWSResponseCreate(connCtx context.Context, conn *lockedConn, input *sc funcsWithNoAction = funcsWithNoAction.Select(cfg.FunctionToCall()) } - jsStruct := funcsWithNoAction.ToJSONStructure(cfg.FunctionsConfig.FunctionNameKey, cfg.FunctionsConfig.FunctionArgumentsKey) + jsStruct := cfg.FunctionsConfig.ToJSONStructure(funcsWithNoAction) g, err := jsStruct.Grammar(cfg.FunctionsConfig.GrammarOptions()...) if err == nil { cfg.Grammar = g diff --git a/pkg/functions/functions.go b/pkg/functions/functions.go index 0686e4f9dd10..3e604d9dfd9d 100644 --- a/pkg/functions/functions.go +++ b/pkg/functions/functions.go @@ -89,6 +89,11 @@ func (f Functions) ToJSONStructure(name, args string) JSONFunctionStructure { return js } +// ToJSONStructure converts functions using the configured property keys. +func (c FunctionsConfig) ToJSONStructure(functions Functions) JSONFunctionStructure { + return functions.ToJSONStructure(c.FunctionNameKey, c.FunctionArgumentsKey) +} + // Select returns a list of functions containing the function with the given name func (f Functions) Select(name string) Functions { var funcs Functions diff --git a/pkg/functions/functions_test.go b/pkg/functions/functions_test.go index 2f5f71b38733..7c10da473931 100644 --- a/pkg/functions/functions_test.go +++ b/pkg/functions/functions_test.go @@ -80,12 +80,11 @@ var _ = Describe("LocalAI grammar functions", func() { }, } - // function_name_key / function_arguments_key are two independent - // settings. Passing the same key for both collapses the structure - // onto a single property, and the arguments overwrite the function - // name constant - leaving a grammar that cannot express which - // function was called. - js := functions.ToJSONStructure("function", "parameters") + config := FunctionsConfig{ + FunctionNameKey: "function", + FunctionArgumentsKey: "parameters", + } + js := config.ToJSONStructure(functions) Expect(js.OneOf[0].Properties).To(HaveLen(2)) fnName := js.OneOf[0].Properties["function"].(FunctionName)