;;; llm-chat-gemini.el --- Implements gemini llm intergration -*- lexical-binding: t; -*- ;; This file is not part of GNU Emacs. ;;; License: ;; llm-chat-gemini.el is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; llm-chat-gemini.el is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Code: (require 'llm-chat-api) (defvar llm-chat-gemini-models '("gemini-2.5-flash-preview-04-17" "gemini-2.5-pro-preview-03-25" "gemini-2.0-flash-lite" "gemini-1.5-flash" "gemini-1.5-flash-8b" "gemini-1.5-pro")) (cl-defgeneric llm-chat-resolve-model-backend ((backend (eql 'gemini)) model) (if (member model llm-chat-gemini-models) t)) (defun llm-chat-gemini-get-model () (if (member (llm-chat-get-model) llm-chat-gemini-models) (llm-chat-get-model) "gemini-2.0-flash-lite")) (defun llm-chat-gemini-convert-history-contents () (let ((contents '())) (dolist (history (llm-chat-get-history)) (let ((role (cdr (car history))) (text (cdr (car (cdr history))))) (cond ((or (string= role "user") (string= role "system")) (setq contents (append contents `((role . "user") (parts . (((text . ,text)))))))) ((string= role "assistant") (setq contents (append contents `((role . "model") (parts . (((text . ,text))))))))))) contents)) (defun llm-chat-gemini-json () (let ((json-object `((contents . (,(llm-chat-gemini-convert-history-contents))) (generation_config . ((temperature . ,(llm-chat-get-temperature))))))) (json-encode json-object))) (defun llm-chat-gemini-headers (backend) ()) (defun llm-chat-make-gemini-backend () (make-llm-chat-backend :api-endpoint (concat "https://generativelanguage.googleapis.com/v1beta/models/" (llm-chat-gemini-get-model) ":streamGenerateContent?key=" (auth-source-pick-first-password :host "api.gemini.com")) :api-key (auth-source-pick-first-password :host "api.gemini.com") :headers-fn #'llm-chat-gemini-headers :json-fn #'llm-chat-gemini-json :filter-fn #'llm-chat-gemini-filter)) (defun llm-chat-gemini-filter (proc string) "Buffer incomplete JSON until a complete object is received, then parse." (setq llm-chat--incomplete (concat llm-chat--incomplete string)) ;; Try to find a complete JSON object (simple heuristic: first { to last }) (write-region string nil "/tmp/stream.txt" t) (when (and (string-match "{" llm-chat--incomplete) (string-match "}[[:space:]]*\\'" llm-chat--incomplete)) (let ((json-str (substring llm-chat--incomplete (string-match "{" llm-chat--incomplete)))) (condition-case err (let* ((json-object (json-read-from-string json-str)) (candidates (alist-get 'candidates json-object)) (first-candidate (if (vectorp candidates) (aref candidates 0) nil)) (content (when first-candidate (alist-get 'content first-candidate))) (parts (when content (alist-get 'parts content))) (text-part (when (and parts (vectorp parts) (> (length parts) 0)) (alist-get 'text (aref parts 0)))) (finish-reason (when first-candidate (alist-get 'finishReason first-candidate)))) (with-current-buffer (get-buffer-create "*LLM Chat*") (save-excursion (goto-char (point-max)) (when text-part (if (not (llm-chat-get-buffer)) (progn (if (not (bolp)) (insert "\n")) (insert "(AI): "))) (insert text-part) (llm-chat-set-buffer (concat (or (llm-chat-get-buffer) "") text-part))) (when (and finish-reason (string= finish-reason "STOP")) (if (not (bolp)) (insert "\n")) (insert "(USR):") (llm-chat-add-assistant-history (llm-chat-get-buffer)) (llm-chat-set-buffer nil))))) (error (message "Gemini filter: JSON parsing error: %s" (error-message-string err))))) (setq llm-chat--incomplete ""))) ;; (defun llm-chat-gemini-filter (proc string) ;; "Filter function to handle Gemini API JSON responses." ;; (with-current-buffer (get-buffer-create "*LLM Chat*") ;; (save-excursion ;; (goto-char (point-max)) ;; (write-region (concat "start-----" string "-----end\n") nil "/tmp/stream.txt" t 'append) ;; ;; Try to extract JSON from the received string ;; (when-let* ((start-pos (string-match "{" string)) ;; (json-string (substring string start-pos))) ;; (condition-case err ;; (let* ((json-object (json-read-from-string json-string)) ;; (candidates (alist-get 'candidates json-object)) ;; (first-candidate (if (vectorp candidates) (aref candidates 0) nil)) ;; (content (when first-candidate ;; (alist-get 'content first-candidate))) ;; (parts (when content (alist-get 'parts content))) ;; (text-part (when (and parts (vectorp parts)) ;; (alist-get 'text (aref parts 0)))) ;; (finish-reason (when first-candidate ;; (alist-get 'finishReason first-candidate)))) ;; (when text-part ;; (if (not (llm-chat-get-buffer)) ;; (progn ;; (if (not (bolp)) ;; (insert "\n")) ;; (insert "(AI): "))) ;; (insert text-part) ;; (llm-chat-set-buffer (concat (or (llm-chat-get-buffer) "") text-part))) ;; (when (and finish-reason (string= finish-reason "STOP")) ;; (if (not (bolp)) ;; (insert "\n")) ;; (insert "(USR):") ;; (llm-chat-add-assistant-history (llm-chat-get-buffer)) ;; (llm-chat-set-buffer nil)))))))) (defun llm-chat-gemini-print-content (string) (let* ((json-object (json-read-from-string string)) (candidates (alist-get 'candidates json-object)) (first-candidate (aref candidates 0))) (when-let* ((content (alist-get 'content first-candidate)) (parts (alist-get 'parts content)) (first-part (aref parts 0)) (text (alist-get 'text first-part))) text))) (provide 'llm-chat-gemini) ;;; llm-chat-gemini.el ends here