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