Skip to content

Commit 1e92cfb

Browse files
authored
Change function calling methods to tool-use, modify input format (#133)
This also adds a lot of testing, and also adds Gemini image understanding. Various bugs have been caught by test and fixed, including incorrect temperatures in Gemini and Vertex, and incorrect use of maximum tokens in Gemini. Also, change version to 0.21.0, and update copyright years.
1 parent 8351ee9 commit 1e92cfb

24 files changed

+1360
-1096
lines changed

NEWS.org

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
* Version 0.21.0
2+
- Incompatible change to function calling, which is now tool use, affecting arguments and methods.
3+
- Support image understanding in Claude
4+
- Support streaming tool use in Claude
25
- Add ~llm-models-add~ as a convenience method to add a model to the known list.
36
* Version 0.20.0
47
- Add ability to output according to a JSON spec.

README.org

+53-62
Large diffs are not rendered by default.

llm-azure.el

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
;;; llm-azure.el --- llm module for integrating with Azure's Open AI service -*- lexical-binding: t; package-lint-main-file: "llm.el"; byte-compile-docstring-max-column: 200-*-
22

3-
;; Copyright (c) 2024 Free Software Foundation, Inc.
3+
;; Copyright (c) 2024-2025 Free Software Foundation, Inc.
44

55
;; Author: Andrew Hyatt <[email protected]>
66
;; Homepage: https://github.com/ahyatt/llm

llm-claude.el

+73-48
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
;;; llm-claude.el --- llm module for integrating with Claude -*- lexical-binding: t; package-lint-main-file: "llm.el"; -*-
22

3-
;; Copyright (c) 2024 Free Software Foundation, Inc.
3+
;; Copyright (c) 2024-2025 Free Software Foundation, Inc.
44

55
;; Author: Andrew Hyatt <[email protected]>
66
;; Homepage: https://github.com/ahyatt/llm
@@ -43,64 +43,86 @@
4343
(unless (llm-claude-key provider)
4444
(error "No API key provided for Claude")))
4545

46-
(defun llm-claude--tool-call (call)
47-
"A Claude version of a function spec for CALL."
48-
`(("name" . ,(llm-function-call-name call))
49-
("description" . ,(llm-function-call-description call))
50-
("input_schema" . ,(llm-provider-utils-openai-arguments
51-
(llm-function-call-args call)))))
46+
(defun llm-claude--tool-call (tool)
47+
"A Claude version of a function spec for TOOL."
48+
`(:name ,(llm-tool-function-name tool)
49+
:description ,(llm-tool-function-description tool)
50+
:input_schema ,(llm-provider-utils-openai-arguments
51+
(llm-tool-function-args tool))))
5252

5353
(cl-defmethod llm-provider-chat-request ((provider llm-claude) prompt stream)
54-
(let ((request `(("model" . ,(llm-claude-chat-model provider))
55-
("stream" . ,(if stream t :json-false))
54+
(let ((request
55+
`(:model ,(llm-claude-chat-model provider)
56+
:stream ,(if stream t :false)
5657
;; Claude requires max_tokens
57-
("max_tokens" . ,(or (llm-chat-prompt-max-tokens prompt) 4096))
58-
("messages" .
59-
,(mapcar (lambda (interaction)
60-
`(("role" . ,(pcase (llm-chat-prompt-interaction-role interaction)
61-
('function 'user)
62-
('assistant 'assistant)
63-
('user 'user)))
64-
("content" .
65-
,(if (llm-chat-prompt-interaction-function-call-results interaction)
66-
(mapcar (lambda (result)
67-
`(("type" . "tool_result")
68-
("tool_use_id" .
69-
,(llm-chat-prompt-function-call-result-call-id
70-
result))
71-
("content" .
72-
,(llm-chat-prompt-function-call-result-result result))))
73-
(llm-chat-prompt-interaction-function-call-results interaction))
74-
(llm-chat-prompt-interaction-content interaction)))))
58+
:max_tokens ,(or (llm-chat-prompt-max-tokens prompt) 4096)
59+
:messages
60+
,(vconcat
61+
(mapcar (lambda (interaction)
62+
`(:role ,(pcase (llm-chat-prompt-interaction-role interaction)
63+
('tool_results "user")
64+
('tool_use "assistant")
65+
('assistant "assistant")
66+
('user "user"))
67+
:content
68+
,(cond ((llm-chat-prompt-interaction-tool-results interaction)
69+
(vconcat (mapcar (lambda (result)
70+
`(:type "tool_result"
71+
:tool_use_id
72+
,(llm-chat-prompt-tool-result-call-id result)
73+
:content
74+
,(llm-chat-prompt-tool-result-result result)))
75+
(llm-chat-prompt-interaction-tool-results interaction))))
76+
((llm-multipart-p (llm-chat-prompt-interaction-content interaction))
77+
(llm-claude--multipart-content
78+
(llm-chat-prompt-interaction-content interaction)))
79+
(t
80+
(llm-chat-prompt-interaction-content interaction)))))
7581
(llm-chat-prompt-interactions prompt)))))
7682
(system (llm-provider-utils-get-system-prompt prompt)))
77-
(when (llm-chat-prompt-functions prompt)
78-
(push `("tools" . ,(mapcar (lambda (f) (llm-claude--tool-call f))
79-
(llm-chat-prompt-functions prompt))) request))
83+
(when (llm-chat-prompt-tools prompt)
84+
(plist-put request :tools
85+
(vconcat (mapcar (lambda (f) (llm-claude--tool-call f))
86+
(llm-chat-prompt-tools prompt)))))
8087
(when (> (length system) 0)
81-
(push `("system" . ,system) request))
88+
(plist-put request :system system))
8289
(when (llm-chat-prompt-temperature prompt)
83-
(push `("temperature" . ,(llm-chat-prompt-temperature prompt)) request))
84-
(append request (llm-chat-prompt-non-standard-params prompt))))
85-
86-
(cl-defmethod llm-provider-extract-function-calls ((_ llm-claude) response)
90+
(plist-put request :temperature (llm-chat-prompt-temperature prompt)))
91+
(append request (llm-provider-utils-non-standard-params-plist prompt))))
92+
93+
(defun llm-claude--multipart-content (content)
94+
"Return CONTENT as a list of Claude multipart content."
95+
(vconcat (mapcar (lambda (part)
96+
(cond ((stringp part)
97+
`(:type "text"
98+
:text ,part))
99+
((llm-media-p part)
100+
`(:type "image"
101+
:source (:type "base64"
102+
:media_type ,(llm-media-mime-type part)
103+
:data ,(base64-encode-string (llm-media-data part) t))))
104+
(t
105+
(error "Unsupported multipart content: %s" part))))
106+
(llm-multipart-parts content))))
107+
108+
(cl-defmethod llm-provider-extract-tool-uses ((_ llm-claude) response)
87109
(let ((content (append (assoc-default 'content response) nil)))
88110
(cl-loop for item in content
89111
when (equal "tool_use" (assoc-default 'type item))
90-
collect (make-llm-provider-utils-function-call
112+
collect (make-llm-provider-utils-tool-use
91113
:id (assoc-default 'id item)
92114
:name (assoc-default 'name item)
93115
:args (assoc-default 'input item)))))
94116

95-
(cl-defmethod llm-provider-populate-function-calls ((_ llm-claude) prompt calls)
117+
(cl-defmethod llm-provider-populate-tool-uses ((_ llm-claude) prompt tool-uses)
96118
(llm-provider-utils-append-to-prompt
97119
prompt
98-
(mapcar (lambda (call)
99-
`((type . "tool_use")
100-
(id . ,(llm-provider-utils-function-call-id call))
101-
(name . ,(llm-provider-utils-function-call-name call))
102-
(input . ,(llm-provider-utils-function-call-args call))))
103-
calls)))
120+
(vconcat (mapcar (lambda (call)
121+
`(:type "tool_use"
122+
:id ,(llm-provider-utils-tool-use-id call)
123+
:name ,(llm-provider-utils-tool-use-name call)
124+
:input ,(llm-provider-utils-tool-use-args call)))
125+
tool-uses))))
104126

105127
(cl-defmethod llm-provider-chat-extract-result ((_ llm-claude) response)
106128
(let ((content (aref (assoc-default 'content response) 0)))
@@ -129,6 +151,9 @@
129151
(when (equal type "text_delta")
130152
(funcall msg-receiver (assoc-default 'text delta))))))))))
131153

154+
(cl-defmethod llm-provider-collect-streaming-tool-uses ((_ llm-claude) data)
155+
(llm-provider-utils-openai-collect-streaming-tool-uses data))
156+
132157
(cl-defmethod llm-provider-headers ((provider llm-claude))
133158
`(("x-api-key" . ,(if (functionp (llm-claude-key provider))
134159
(funcall (llm-claude-key provider))
@@ -153,16 +178,16 @@
153178
"Claude")
154179

155180
(cl-defmethod llm-capabilities ((_ llm-claude))
156-
(list 'streaming 'function-calls))
181+
(list 'streaming 'function-calls 'image-input))
157182

158183
(cl-defmethod llm-provider-append-to-prompt ((_ llm-claude) prompt result
159-
&optional func-results)
184+
&optional tool-use-results)
160185
;; Claude doesn't have a 'function role, so we just always use assistant here.
161186
;; But if it's a function result, it considers that a 'user response, which
162187
;; needs to be sent back.
163-
(llm-provider-utils-append-to-prompt prompt result func-results (if func-results
164-
'user
165-
'assistant)))
188+
(llm-provider-utils-append-to-prompt prompt result tool-use-results (if tool-use-results
189+
'user
190+
'assistant)))
166191

167192

168193
(provide 'llm-claude)

llm-fake.el

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
;;; llm-fake.el --- Use for developers looking at llm calls. -*- lexical-binding: t; package-lint-main-file: "llm.el"; -*-
22

3-
;; Copyright (c) 2023, 2024 Free Software Foundation, Inc.
3+
;; Copyright (c) 2023-2025 Free Software Foundation, Inc.
44

55
;; Author: Andrew Hyatt <[email protected]>
66
;; Homepage: https://github.com/ahyatt/llm
@@ -31,7 +31,7 @@
3131
;;; Code:
3232

3333
(cl-defstruct llm-fake
34-
"A provider for the fake LLM provider.
34+
"A provider for the fake LLM provider.
3535
3636
OUTPUT-TO-BUFFER can be nil, in which case, nothing will be
3737
output. If a string or a buffer, it will append the request as
@@ -44,7 +44,7 @@ message cons. If nil, the response will be a short text string.
4444
EMBEDDING-ACTION-FUNC will be called with no arguments to produce
4545
either a vector response for the chat, or a signal symbol and
4646
message cons. If nil, the response will be a simple vector."
47-
output-to-buffer chat-action-func embedding-action-func)
47+
output-to-buffer chat-action-func embedding-action-func)
4848

4949
(cl-defmethod llm-chat-async ((provider llm-fake) prompt response-callback error-callback)
5050
(condition-case err
@@ -104,9 +104,9 @@ message cons. If nil, the response will be a simple vector."
104104
(let* ((f (llm-fake-embedding-action-func provider))
105105
(result (funcall f)))
106106
(pcase (type-of result)
107-
('vector result)
108-
('cons (signal (car result) (cdr result)))
109-
(_ (error "Incorrect type found in `chat-embedding-func': %s" (type-of result)))))
107+
('vector result)
108+
('cons (signal (car result) (cdr result)))
109+
(_ (error "Incorrect type found in `chat-embedding-func': %s" (type-of result)))))
110110
[0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]))
111111

112112
(cl-defmethod llm-embedding-async ((provider llm-fake) string vector-callback error-callback)

llm-gemini.el

+7-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
;;; llm-gemini.el --- LLM implementation of Google Cloud Gemini AI -*- lexical-binding: t; package-lint-main-file: "llm.el"; -*-
22

3-
;; Copyright (c) 2023, 2024 Free Software Foundation, Inc.
3+
;; Copyright (c) 2023-2025 Free Software Foundation, Inc.
44

55
;; Author: Andrew Hyatt <[email protected]>
66
;; Homepage: https://github.com/ahyatt/llm
@@ -48,12 +48,12 @@ You can get this at https://makersuite.google.com/app/apikey."
4848
(format "https://generativelanguage.googleapis.com/v1beta/models/%s:embedContent?key=%s"
4949
(llm-gemini-embedding-model provider)
5050
(if (functionp (llm-gemini-key provider))
51-
(funcall (llm-claude-key provider))
51+
(funcall (llm-gemini-key provider))
5252
(llm-gemini-key provider))))
5353

5454
(cl-defmethod llm-provider-embedding-request ((provider llm-gemini) string)
55-
`((model . ,(llm-gemini-embedding-model provider))
56-
(content . ((parts . (((text . ,string))))))))
55+
`(:model ,(llm-gemini-embedding-model provider)
56+
:content (:parts [(:text ,string)])))
5757

5858
(cl-defmethod llm-provider-embedding-extract-result ((_ llm-gemini) response)
5959
(assoc-default 'values (assoc-default 'embedding response)))
@@ -75,22 +75,10 @@ If STREAMING-P is non-nil, use the streaming endpoint."
7575
(cl-defmethod llm-provider-chat-streaming-url ((provider llm-gemini))
7676
(llm-gemini--chat-url provider t))
7777

78-
(cl-defmethod llm-provider-populate-function-calls ((_ llm-gemini) prompt calls)
79-
(llm-provider-utils-append-to-prompt
80-
prompt
81-
;; For Vertex there is just going to be one call
82-
(mapcar (lambda (fc)
83-
`((functionCall
84-
.
85-
((name . ,(llm-provider-utils-function-call-name fc))
86-
(args . ,(llm-provider-utils-function-call-args fc))))))
87-
calls)))
88-
8978
(cl-defmethod llm-provider-chat-request ((_ llm-gemini) _ _)
90-
(mapcar (lambda (c) (if (eq (car c) 'generation_config)
91-
(cons 'generationConfig (cdr c))
92-
c))
93-
(cl-call-next-method)))
79+
;; Temporary, can be removed in the next version. Without this the old
80+
;; definition will cause problems when users upgrade.
81+
(cl-call-next-method))
9482

9583
(cl-defmethod llm-name ((_ llm-gemini))
9684
"Return the name of PROVIDER."

llm-gpt4all.el

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
;;; llm-gpt4all.el --- llm module for integrating with GPT4All -*- lexical-binding: t; package-lint-main-file: "llm.el"; -*-
22

3-
;; Copyright (c) 2023, 2024 Free Software Foundation, Inc.
3+
;; Copyright (c) 2023-2025 Free Software Foundation, Inc.
44

55
;; Author: Andrew Hyatt <[email protected]>
66
;; Homepage: https://github.com/ahyatt/llm

0 commit comments

Comments
 (0)