branch: elpa/gptel
commit 6de3e00bf68e495164e7eeb1c006c7941b1b9a08
Author: Karthik Chikmagalur <[email protected]>
Commit: Karthik Chikmagalur <[email protected]>
gptel: Change how reasoning content is tracked
Stream parsers: Instead of a single :reasoning key in the fsm-info
to hold both the reasoning chunk and the state of the reasoning
block parser (not started/started/ending/done), use two keys
:reasoning and :reasoning-block respectively. Separating these
functions simplifies the conditionals considerably, and avoids a
limitation where we can't capture both a reasoning chunk and the
end of the reasoning block when they exist in a single filter
run.
* gptel-curl.el (gptel-curl--stream-filter): Use :reasoning and
:reasoning-block instead of overloading :reasoning.
* gptel-openai.el (gptel-curl--parse-stream): Adjust for
new :reasoning-block parameter.
* gptel-openai-extras.el (gptel-curl--parse-stream): Adjust for
new :reasoning-block parameter.
* gptel-anthropic.el (gptel-curl--parse-stream): Adjust for
new :reasoning-block parameter.
---
gptel-anthropic.el | 7 ++--
gptel-curl.el | 86 ++++++++++++++++++++++++++------------------------
gptel-openai-extras.el | 15 ++++-----
gptel-openai.el | 11 +++----
4 files changed, 60 insertions(+), 59 deletions(-)
diff --git a/gptel-anthropic.el b/gptel-anthropic.el
index 7174208de6..bdaebf73a0 100644
--- a/gptel-anthropic.el
+++ b/gptel-anthropic.el
@@ -94,7 +94,7 @@ information if the stream contains it. Not my best work, I
know."
:name (plist-get cblock
:name))
(plist-get info :tool-use))))
("thinking" (plist-put info :reasoning (plist-get cblock
:thinking))
- (plist-put info :thinking-block t)))))
+ (plist-put info :reasoning-block 'in)))))
((looking-at "content_block_stop")
(cond
@@ -111,9 +111,8 @@ information if the stream contains it. Not my best work, I
know."
(error (pop (plist-get info :tool-use)))) ;TODO: nreverse
:tool-use list
(plist-put info :partial_json nil))
- ((plist-get info :thinking-block) ;End of reasoning block
- (plist-put info :thinking-block nil)
- (plist-put info :reasoning t)))) ;Signal end of reasoning stream
to filter
+ ((plist-get info :reasoning-block) ;End of reasoning block
+ (plist-put info :reasoning-block t)))) ;Signal end of reasoning
stream to filter
((looking-at "message_delta")
;; collect stop_reason, usage_tokens and prepare tools
diff --git a/gptel-curl.el b/gptel-curl.el
index 734aa770bb..ad9713b3af 100644
--- a/gptel-curl.el
+++ b/gptel-curl.el
@@ -285,7 +285,9 @@ Optional RAW disables text properties and transformation."
(defun gptel-curl--stream-filter (process output)
(let* ((fsm (alist-get process gptel--request-alist))
- (proc-info (gptel-fsm-info fsm)))
+ (proc-info (gptel-fsm-info fsm))
+ (callback (or (plist-get proc-info :callback)
+ #'gptel-curl--stream-insert-response)))
(with-current-buffer (process-buffer process)
;; Insert output
(save-excursion
@@ -315,53 +317,55 @@ Optional RAW disables text properties and transformation."
(when (member http-status '("200" "100"))
(let ((response (gptel-curl--parse-stream
(plist-get proc-info :backend) proc-info))
- (reasoning (plist-get proc-info :reasoning)))
- ;; Depending on the API, there are two ways that reasoning or
+ (reasoning-block (plist-get proc-info :reasoning-block)))
+ ;; Depending on the API, there are two modes that reasoning or
;; chain-of-thought content appears: as part of the main response
;; but surrounded by <think>...</think> tags, or as a separate
- ;; JSON field in the response stream. Both cases are handled here
- ;; via dispatch on the value of the :reasoning key. :reasoning has
- ;; five valid values:
+ ;; JSON field in the response stream.
;;
- ;; - nil before we've checked for <think> blocks or reasoning JSON
fields,
- ;; - 'in when inside a <think> block,
- ;; - a string containing the reasoning content (separate JSON
field), and
- ;; - t for the end of the reasoning part of the stream (separate
JSON field).
- ;; In all cases, :reasoning is
- ;; - 'done if the reasoning content is missing or done being
parsed.
+ ;; These cases are handled using two PROC-INFO keys:
+ ;;
+ ;; :reasoning-block is nil before checking for reasoning, 'in when
+ ;; in a reasoning block, t when we reach the end of the block, and
+ ;; 'done afterwards or if no reasoning block is found. This
+ ;; applies to both the modes above.
+ ;;
+ ;; :reasoning contains the reasoning text parsed from the separate
+ ;; JSON field.
;;
;; NOTE: We assume here that the reasoning block always
;; precedes the main response block.
- (unless (eq reasoning 'done)
- (cond
- ((or (stringp reasoning) (eq reasoning t))
- ;; Obtained from separate JSON field in response
- (funcall (or (plist-get proc-info :callback)
- #'gptel-curl--stream-insert-response)
- (cons 'reasoning reasoning) proc-info)
- (if (stringp reasoning)
- (plist-put proc-info :reasoning nil) ;Reset for next
parsing round
- (plist-put proc-info :reasoning 'done)))
- ((and (null reasoning) (length> response 0))
- (if (string-match-p "^ *<think>" response)
- (progn (setq response (cons 'reasoning response))
- (plist-put proc-info :reasoning 'in))
- (plist-put proc-info :reasoning 'done)))
- ((length> response 0)
- (if-let* ((idx (string-match-p "</think>" response)))
- (progn (funcall (or (plist-get proc-info :callback)
- #'gptel-curl--stream-insert-response)
- (cons 'reasoning
- (string-trim-left
- (substring response nil (+ idx 8))))
- proc-info)
- (setq response (substring response (+ idx 8)))
- (plist-put proc-info :reasoning 'done))
- (setq response (cons 'reasoning response))))))
+ (unless (eq reasoning-block 'done)
+ (let ((reasoning (plist-get proc-info :reasoning)))
+ (cond
+ ((stringp reasoning)
+ ;; Obtained from separate JSON field in response
+ (funcall callback (cons 'reasoning reasoning) proc-info)
+ (plist-put proc-info :reasoning nil)) ;Reset for next
parsing round
+ ((and (null reasoning-block) (length> response 0))
+ (if (string-match-p "^ *<think>" response)
+ ;; Obtained from main response stream
+ (progn (setq response (cons 'reasoning response))
+ (plist-put proc-info :reasoning-block 'in))
+ (plist-put proc-info :reasoning-block 'done)))
+ ((length> response 0)
+ (if-let* ((idx (string-match-p "</think>" response)))
+ (progn
+ (funcall callback
+ (cons 'reasoning ;last reasoning chunk
+ (string-trim-left
+ (substring response nil (+ idx 8))))
+ proc-info)
+ ;; Signal end of reasoning stream
+ (funcall callback '(reasoning . t) proc-info)
+ (setq response (substring response (+ idx 8)))
+ (plist-put proc-info :reasoning-block 'done))
+ (setq response (cons 'reasoning response)))))
+ (when (eq reasoning-block t) ;End of reasoning block
+ (funcall callback '(reasoning . t) proc-info)
+ (plist-put proc-info :reasoning-block 'done))))
(unless (equal response "") ;Response callback
- (funcall (or (plist-get proc-info :callback)
- #'gptel-curl--stream-insert-response)
- response proc-info))))))))
+ (funcall callback response proc-info))))))))
(cl-defgeneric gptel-curl--parse-stream (backend proc-info)
"Stream parser for gptel-curl.
diff --git a/gptel-openai-extras.el b/gptel-openai-extras.el
index 3d89da21c6..e5b7b04dc6 100644
--- a/gptel-openai-extras.el
+++ b/gptel-openai-extras.el
@@ -280,7 +280,7 @@ parameters."
(cl-defmethod gptel-curl--parse-stream :before ((_backend gptel-deepseek) info)
"Capture reasoning block stream into INFO."
- (unless (eq (plist-get info :reasoning) 'done)
+ (unless (eq (plist-get info :reasoning-block) 'done)
(save-excursion
(ignore-errors
(catch 'done
@@ -288,18 +288,17 @@ parameters."
(unless (looking-at-p " *\\[DONE\\]")
(when-let* ((response (gptel--json-read))
(delta (map-nested-elt response '(:choices 0
:delta))))
- (if-let* ((reasoning-content (plist-get delta
:reasoning_content))
- ((not (eq reasoning-content :null))))
+ (if-let* ((reasoning (plist-get delta :reasoning_content))
+ ((not (eq reasoning :null))))
;; :reasoning will be consumed by the gptel-request
callback
;; and reset by the stream filter.
(plist-put info :reasoning
- (concat (plist-get info :reasoning)
reasoning-content))
+ (concat (plist-get info :reasoning) reasoning))
(when-let* ((content (plist-get delta :content))
((not (eq content :null))))
- (unless (plist-get info :reasoning) ;Don't overwrite
existing value
- (if (plist-member delta :reasoning_content) ;Check for
reasoning model
- (plist-put info :reasoning t) ;End of streaming
reasoning block
- (plist-put info :reasoning 'done))) ;Not using a
reasoning model
+ (if (plist-member delta :reasoning_content) ;Check for
reasoning model
+ (plist-put info :reasoning-block t) ;End of streaming
reasoning block
+ (plist-put info :reasoning-block 'done)) ;Not using a
reasoning model
(throw 'done t)))))))))))
(cl-defmethod gptel--parse-response :before ((_backend gptel-deepseek)
response info)
diff --git a/gptel-openai.el b/gptel-openai.el
index ea32b254ea..221f86f511 100644
--- a/gptel-openai.el
+++ b/gptel-openai.el
@@ -230,8 +230,8 @@ information if the stream contains it."
;; old tool block continues, so continue collecting
arguments in :partial_json
(push (plist-get func :arguments) (plist-get info
:partial_json)))))
;; Check for reasoning blocks, currently only used by
Openrouter
- ;; FIXME: Should this be moved to a dedicated Openrouter
backend?
- (unless (or (eq (plist-get info :reasoning) 'done)
+ ;; MAYBE: Should this be moved to a dedicated Openrouter
backend?
+ (unless (or (eq (plist-get info :reasoning-block) 'done)
(not (plist-member delta :reasoning)))
(if-let* ((reasoning-chunk (plist-get delta :reasoning))
;for openrouter
((not (eq reasoning-chunk :null))))
@@ -240,10 +240,9 @@ information if the stream contains it."
;; Done with reasoning if we get non-empty content
(if-let* ((c (plist-get delta :content))
((not (or (eq c :null) (string-empty-p c)))))
- (unless (plist-get info :reasoning) ;Don't overwrite
existing value
- (if (plist-member info :reasoning) ;Is this a
reasoning model?
- (plist-put info :reasoning t) ;End of streaming
reasoning block
- (plist-put info :reasoning 'done)))))))))) ;Not
using a reasoning model
+ (if (plist-member info :reasoning) ;Is this a
reasoning model?
+ (plist-put info :reasoning-block t) ;End of
streaming reasoning block
+ (plist-put info :reasoning-block 'done))))))))) ;Not
using a reasoning model
(error (goto-char (match-beginning 0))))
(apply #'concat (nreverse content-strs))))