branch: elpa/gptel
commit 6de3e00bf68e495164e7eeb1c006c7941b1b9a08
Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>

    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))))
 

Reply via email to