|
1 | 1 | ;;; llm-claude.el --- llm module for integrating with Claude -*- lexical-binding: t; package-lint-main-file: "llm.el"; -*-
|
2 | 2 |
|
3 |
| -;; Copyright (c) 2024 Free Software Foundation, Inc. |
| 3 | +;; Copyright (c) 2024-2025 Free Software Foundation, Inc. |
4 | 4 |
|
5 | 5 | ;; Author: Andrew Hyatt <[email protected]>
|
6 | 6 | ;; Homepage: https://github.com/ahyatt/llm
|
|
43 | 43 | (unless (llm-claude-key provider)
|
44 | 44 | (error "No API key provided for Claude")))
|
45 | 45 |
|
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)))) |
52 | 52 |
|
53 | 53 | (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) |
56 | 57 | ;; 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))))) |
75 | 81 | (llm-chat-prompt-interactions prompt)))))
|
76 | 82 | (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))))) |
80 | 87 | (when (> (length system) 0)
|
81 |
| - (push `("system" . ,system) request)) |
| 88 | + (plist-put request :system system)) |
82 | 89 | (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) |
87 | 109 | (let ((content (append (assoc-default 'content response) nil)))
|
88 | 110 | (cl-loop for item in content
|
89 | 111 | when (equal "tool_use" (assoc-default 'type item))
|
90 |
| - collect (make-llm-provider-utils-function-call |
| 112 | + collect (make-llm-provider-utils-tool-use |
91 | 113 | :id (assoc-default 'id item)
|
92 | 114 | :name (assoc-default 'name item)
|
93 | 115 | :args (assoc-default 'input item)))))
|
94 | 116 |
|
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) |
96 | 118 | (llm-provider-utils-append-to-prompt
|
97 | 119 | 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)))) |
104 | 126 |
|
105 | 127 | (cl-defmethod llm-provider-chat-extract-result ((_ llm-claude) response)
|
106 | 128 | (let ((content (aref (assoc-default 'content response) 0)))
|
|
129 | 151 | (when (equal type "text_delta")
|
130 | 152 | (funcall msg-receiver (assoc-default 'text delta))))))))))
|
131 | 153 |
|
| 154 | +(cl-defmethod llm-provider-collect-streaming-tool-uses ((_ llm-claude) data) |
| 155 | + (llm-provider-utils-openai-collect-streaming-tool-uses data)) |
| 156 | + |
132 | 157 | (cl-defmethod llm-provider-headers ((provider llm-claude))
|
133 | 158 | `(("x-api-key" . ,(if (functionp (llm-claude-key provider))
|
134 | 159 | (funcall (llm-claude-key provider))
|
|
153 | 178 | "Claude")
|
154 | 179 |
|
155 | 180 | (cl-defmethod llm-capabilities ((_ llm-claude))
|
156 |
| - (list 'streaming 'function-calls)) |
| 181 | + (list 'streaming 'function-calls 'image-input)) |
157 | 182 |
|
158 | 183 | (cl-defmethod llm-provider-append-to-prompt ((_ llm-claude) prompt result
|
159 |
| - &optional func-results) |
| 184 | + &optional tool-use-results) |
160 | 185 | ;; Claude doesn't have a 'function role, so we just always use assistant here.
|
161 | 186 | ;; But if it's a function result, it considers that a 'user response, which
|
162 | 187 | ;; 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))) |
166 | 191 |
|
167 | 192 |
|
168 | 193 | (provide 'llm-claude)
|
|
0 commit comments