fix(functions): honor function_arguments_key when building the tool grammar (#11677)

* 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": "<function name>"}` 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 <antai12232931@outlook.com>

* 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

* chore: empty commit to trigger workflow approval

Signed-off-by: Tai An <antai12232931@outlook.com>

---------

Signed-off-by: Anai-Guo <antai12232931@outlook.com>
Signed-off-by: Tai An <antai12232931@outlook.com>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
This commit is contained in:
Tai Anandlocalai-org-maint-bot authored and GitHub committed 2026-09-28 00:52:51 +02:00
1 parent 950c271710
commit eec532704c
6 files changed
+36 -4

No files matched your search

+1 -1
View File
@@ -451,7 +451,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 := config.FunctionsConfig.ToJSONStructure(funcs)
g, err := jsStruct.Grammar(config.FunctionsConfig.GrammarOptions()...)
if err == nil {
config.Grammar = g
+1 -1
View File
@@ -293,7 +293,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 := turnCfg.FunctionsConfig.ToJSONStructure(functions.Functions(funcs))
g, err := jsStruct.Grammar(turnCfg.FunctionsConfig.GrammarOptions()...)
if err == nil {
turnCfg.Grammar = g
@@ -204,7 +204,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 := cfg.FunctionsConfig.ToJSONStructure(funcsWithNoAction)
g, err := jsStruct.Grammar(cfg.FunctionsConfig.GrammarOptions()...)
if err == nil {
cfg.Grammar = g
@@ -381,7 +381,7 @@ func handleWSResponseCreate(connCtx context.Context, conn *lockedConn, connectio
funcsWithNoAction = funcsWithNoAction.Select(cfg.FunctionToCall())
}
jsStruct := funcsWithNoAction.ToJSONStructure(cfg.FunctionsConfig.FunctionNameKey, cfg.FunctionsConfig.FunctionNameKey)
jsStruct := cfg.FunctionsConfig.ToJSONStructure(funcsWithNoAction)
g, err := jsStruct.Grammar(cfg.FunctionsConfig.GrammarOptions()...)
if err == nil {
cfg.Grammar = g
+5
View File
@@ -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
+27
View File
@@ -65,6 +65,33 @@ 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",
},
},
},
},
}
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)
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() {