branch: externals/llm
commit 0bea0123af3964502219e7373a25f2409a5e8f01
Author: Andrew Hyatt <[email protected]>
Commit: GitHub <[email protected]>

    Add tool choice for forcing tools on or off (#220)
---
 NEWS.org      |  3 ++-
 README.org    | 10 +++++++---
 llm-claude.el | 12 ++++++++++++
 llm-openai.el | 15 +++++++++++++++
 llm-test.el   | 14 +++++++++++---
 llm-vertex.el | 31 ++++++++++++++++++++++---------
 llm.el        | 24 ++++++++++++++++++++++--
 7 files changed, 91 insertions(+), 18 deletions(-)

diff --git a/NEWS.org b/NEWS.org
index 4691fef088..a095905f9d 100644
--- a/NEWS.org
+++ b/NEWS.org
@@ -1,4 +1,5 @@
-* Version 0.27.4
+* Version 0.28.0
+- Add tool calling options, for forbidding or forcing tool choice. 
 - Fix bug (or perhaps breaking change) in Ollama tool use.
 - Add Gemini 3 model, update Gemini code to pass thought signatures
 - Add =json-response= capability to Claude 4.5 and 4.1 Opus models
diff --git a/README.org b/README.org
index 0c06ff1cdc..0a2fb4ee12 100644
--- a/README.org
+++ b/README.org
@@ -228,8 +228,6 @@ Conversations can take place by repeatedly calling 
~llm-chat~ and its variants.
 ** Caution about ~llm-chat-prompt-interactions~
 The interactions in a prompt may be modified by conversation or by the 
conversion of the context and examples to what the LLM understands.  Different 
providers require different things from the interactions.  Some can handle 
system prompts, some cannot.  Some require alternating user and assistant chat 
interactions, others can handle anything.  It's important that clients keep to 
behaviors that work on all providers.  Do not attempt to read or manipulate 
~llm-chat-prompt-interactions~ af [...]
 ** Tool use
-*Note: tool use is currently beta quality.  If you want to use tool use, 
please watch the =llm= 
[[https://github.com/ahyatt/llm/discussions][discussions]] for any 
announcements about changes.*
-
 Tool use is a way to give the LLM a list of functions it can call, and have it 
call the functions for you.  The standard interaction has the following steps:
 1. The client sends the LLM a prompt with tools it can use.
 2. The LLM may return which tools to use, and with what arguments, or text as 
normal.
@@ -239,7 +237,7 @@ Tool use is a way to give the LLM a list of functions it 
can call, and have it c
 
 This basic structure is useful because it can guarantee a well-structured 
output (if the LLM does decide to use the tool). *Not every LLM can handle tool 
use, and those that do not will ignore the tools entirely*. The function 
=llm-capabilities= will return a list with =tool-use= in it if the LLM supports 
tool use.  Because not all providers support tool use when streaming, 
=streaming-tool-use= indicates the ability to use tool uses in 
~llm-chat-streaming~. Right now only Gemini, Vertex, [...]
 
-The way to call functions is to attach a list of functions to the =tools= slot 
in the prompt. This is a list of =llm-tool= structs, which is a tool that is an 
elisp function, with a name, a description, and a list of arguments. The 
docstrings give an explanation of the format.  An example is:
+The way to call tools is to attach a list of tools to the =tools= slot in the 
prompt. This is a list of =llm-tool= structs, which is a tool that is an elisp 
function, with a name, a description, and a list of arguments. The docstrings 
give an explanation of the format.  An example is:
 
 #+begin_src emacs-lisp
 (llm-chat-async
@@ -276,6 +274,12 @@ Tools will be called with vectors for array results, =nil= 
for false boolean res
 Be aware that there is no gaurantee that the tool will be called correctly.  
While the LLMs mostly get this right, they are trained on Javascript functions, 
so imitating Javascript names is recommended. So, "write_email" is a better 
name for a function than "write-email".
 
 Examples can be found in =llm-tester=. There is also a function call to 
generate function calls from existing elisp functions in 
=utilities/elisp-to-tool.el=.
+
+Tool use can be controlled by the =:tool-options= param in 
=llm-make-chat-prompt=
+that takes a =llm-tool-options= struct.  This can be set to force or forbid 
tool
+calling, or to force a specific tool to be called.  This is useful when a
+converastion with tools happens and the tools remain constant but how they are
+used may need to change.  Ollama does not support currently support this.
 ** Media input
 *Note:  media input functionality is currently alpha quality.  If you want to 
use it, please watch the =llm= 
[[https://github.com/ahyatt/llm/discussions][discussions]] for any 
announcements about changes.*
 
diff --git a/llm-claude.el b/llm-claude.el
index 13771b3a8a..ccb7d6d50e 100644
--- a/llm-claude.el
+++ b/llm-claude.el
@@ -109,6 +109,18 @@
       (setq request (plist-put request :system system)))
     (when (llm-chat-prompt-temperature prompt)
       (setq request (plist-put request :temperature 
(llm-chat-prompt-temperature prompt))))
+    (when-let* ((options (llm-chat-prompt-tool-options prompt)))
+      (setq request (plist-put request :tool_choice
+                               (append
+                                (list :type (pcase 
(llm-tool-options-tool-choice options)
+                                              ('auto "auto")
+                                              ('any "any")
+                                              ('none "none")
+                                              ((pred stringp) "tool")
+                                              (_ (error "Unknown tool choice 
option: %s"
+                                                        
(llm-tool-options-tool-choice options)))))
+                                (when (stringp (llm-tool-options-tool-choice 
options))
+                                  (list :name (llm-tool-options-tool-choice 
options)))))))
     (when (llm-chat-prompt-reasoning prompt)
       (setq request (plist-put request :thinking
                                (let (thinking-plist)
diff --git a/llm-openai.el b/llm-openai.el
index 80853818af..2b67ae5a10 100644
--- a/llm-openai.el
+++ b/llm-openai.el
@@ -200,6 +200,20 @@ PROVIDER is the Open AI provider struct."
     (list :tools (vconcat (mapcar #'llm-provider-utils-openai-tool-spec
                                   (llm-chat-prompt-tools prompt))))))
 
+(defun llm-openai--build-tool-choice (prompt)
+  "Build the tool_choice field if present in PROMPT."
+  (when-let ((options (llm-chat-prompt-tool-options prompt)))
+    (list :tool_choice
+          (pcase (llm-tool-options-tool-choice options)
+            ('auto "auto")
+            ('none "none")
+            ('any "required")
+            ((pred stringp)
+             (list
+              :function (list :name (llm-tool-options-tool-choice options))
+              :type "function"))
+            (_ (error "Unknown tool choice option: %s" 
(llm-tool-options-tool-choice options)))))))
+
 (defun llm-openai--build-tool-interaction (interaction)
   "Build the tool interaction for INTERACTION."
   (mapcar
@@ -301,6 +315,7 @@ STREAMING if non-nil, turn on response streaming."
            (llm-openai--build-max-tokens prompt)
            (llm-openai--build-response-format prompt)
            (llm-openai--build-tools prompt)
+           (llm-openai--build-tool-choice prompt)
            (llm-openai--build-messages prompt)))
 
     ;; Merge non-standard params
diff --git a/llm-test.el b/llm-test.el
index 9ae1fc0dc5..2e163396c2 100644
--- a/llm-test.el
+++ b/llm-test.el
@@ -210,7 +210,9 @@
                                              :name "func"
                                              :description "desc"
                                              :args '((:name "arg1" 
:description "desc1" :type string)
-                                                     (:name "arg2" 
:description "desc2" :type integer :optional t))))))
+                                                     (:name "arg2" 
:description "desc2" :type integer :optional t))))
+                               :tool-options (make-llm-tool-options
+                                              :tool-choice "func")))
            :openai
            (:model "model"
                    :messages [(:role "user" :content "Hello world")]
@@ -223,7 +225,8 @@
                                                 :properties
                                                 (:arg1 (:description "desc1" 
:type "string")
                                                        :arg2 (:description 
"desc2" :type "integer"))
-                                                :required ["arg1"])))])
+                                                :required ["arg1"])))]
+                   :tool_choice (:function (:name "func") :type "function"))
            :gemini
            (:contents [(:role "user" :parts [(:text "Hello world")])]
                       :tools [(:function_declarations
@@ -234,7 +237,10 @@
                                               :properties
                                               (:arg1 (:description "desc1" 
:type "string")
                                                      :arg2 (:description 
"desc2" :type "integer"))
-                                              :required ["arg1"]))])])
+                                              :required ["arg1"]))])]
+                      :toolConfig (:functionCallingConfig
+                                   (:mode "ANY"
+                                          :allowedFunctionNames ["func"])))
            :ollama (:model "model"
                            :messages [(:role "user" :content "Hello world")]
                            :tools
@@ -261,6 +267,8 @@
                                           (:arg1 (:description "desc1" :type 
"string")
                                                  :arg2 (:description "desc2" 
:type "integer"))
                                           :required ["arg1"]))]
+                           :tool_choice (:type "tool"
+                                               :name "func")
                            :stream :false)))
   "A list of tests for `llm-provider-chat-request'.")
 
diff --git a/llm-vertex.el b/llm-vertex.el
index ea92f10a43..1186c3e530 100644
--- a/llm-vertex.el
+++ b/llm-vertex.el
@@ -250,15 +250,28 @@ information than standard tool use."
    (when (llm-chat-prompt-tools prompt)
      ;; Although Gemini claims to be compatible with Open AI's function 
declaration,
      ;; it's only somewhat compatible.
-     `(:tools
-       [(:function_declarations
-         ,(vconcat (mapcar
-                    (lambda (tool)
-                      `(:name ,(llm-tool-name tool)
-                              :description ,(llm-tool-description tool)
-                              :parameters ,(llm-provider-utils-openai-arguments
-                                            (llm-tool-args tool))))
-                    (llm-chat-prompt-tools prompt))))]))
+     (append
+      `(:tools
+        [(:function_declarations
+          ,(vconcat (mapcar
+                     (lambda (tool)
+                       `(:name ,(llm-tool-name tool)
+                               :description ,(llm-tool-description tool)
+                               :parameters 
,(llm-provider-utils-openai-arguments
+                                             (llm-tool-args tool))))
+                     (llm-chat-prompt-tools prompt))))])
+      (when-let* ((options (llm-chat-prompt-tool-options prompt)))
+        `(:toolConfig
+          (:functionCallingConfig
+           ,(append
+             (list :mode (pcase (llm-tool-options-tool-choice options)
+                           ('auto "AUTO")
+                           ('none "NONE")
+                           ('any "ANY")
+                           ((pred stringp) "ANY")
+                           (_ (error "Unknown tool choice option: %s" 
(llm-tool-options-tool-choice options)))))
+             (when (stringp (llm-tool-options-tool-choice options))
+               `(:allowedFunctionNames [,(llm-tool-options-tool-choice 
options)]))))))))
    (llm-vertex--chat-parameters prompt model)))
 
 ;; TODO: remove after September 2025, this is only here so people can upgrade 
to
diff --git a/llm.el b/llm.el
index 1b38185ec1..df12629744 100644
--- a/llm.el
+++ b/llm.el
@@ -72,7 +72,18 @@ See %s for the details on the restrictions on use." name 
tos)))
 
 Use of this directly is deprecated, instead use `llm-make-chat-prompt'."
   context examples interactions tools temperature max-tokens response-format
-  reasoning non-standard-params)
+  reasoning non-standard-params tool-options)
+
+(cl-defstruct llm-tool-options
+  "Contains standarized options for tool use.
+
+TOOL-CHOICE is a symbol that indicates how tools are chosen, or a string
+tool name.  It can be `none' (no tools are used), `auto' (the LLM
+chooses whether to use a tool), or `any' (the LLM must call one of the
+tools), or the string name of a tool, to force the use of that tool.
+
+Not all providers can force a particular tool use."
+  (tool-choice 'auto))
 
 (cl-defstruct llm-chat-prompt-interaction
   "This defines a single interaction given as part of a chat prompt.
@@ -231,7 +242,7 @@ instead."
                  :args args
                  :async async))
 
-(cl-defun llm-make-chat-prompt (content &key context examples tools
+(cl-defun llm-make-chat-prompt (content &key context examples tools 
tool-options
                                         temperature max-tokens response-format
                                         reasoning non-standard-params)
   "Create a `llm-chat-prompt' with CONTENT sent to the LLM provider.
@@ -272,6 +283,14 @@ optional.  When this is given, the LLM will either call the
 function or return text as normal, depending on what the LLM
 decides.
 
+TOOL-OPTIONS is a `llm-tool-options' struct, which can be used for
+setting whether tool callins is allowed or not or forcing a tool call to
+be used.  This is useful because some models work best when the same
+tools are in all the requests in a conversation, but the program wants
+to have some control over how they are called.  LLMs typically have some
+variablility in what tool options can be used, so not all can support
+every option.
+
 TEMPERATURE is a floating point number with a minimum of 0, and
 maximum of 1, which controls how predictable the result is, with
 0 being the most predicatable, and 1 being the most creative.
@@ -331,6 +350,7 @@ vectors (if a list).  This is optional."
                                      :content s))
                                   (if (listp content) content (list content)))
    :tools tools
+   :tool-options tool-options
    :temperature temperature
    :max-tokens max-tokens
    :response-format response-format

Reply via email to