;;; gptel-gemini.el ---  Gemini suppport for gptel  -*- lexical-binding: t; -*-

;; Copyright (C) 2023-2026  Karthik Chikmagalur

;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com>

;; This program 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.

;; This program 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 <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This file adds support for the Gemini API to gptel

;;; Code:
(require 'cl-generic)
(require 'map)
(require 'cl-lib)
(require 'gptel-request)

(declare-function prop-match-value "text-property-search" nil t)
(declare-function text-property-search-backward "text-property-search")
(declare-function json-read "json")
(declare-function gptel-context--wrap "gptel-context")
(declare-function gptel-context--collect-media "gptel-context")

(defvar json-object-type)

;;; Gemini
(cl-defstruct
    (gptel-gemini (:constructor gptel--make-gemini)
                  (:copier nil)
                  (:include gptel-backend)))

(defun gptel--gemini-update-tokens (usage info)
  "Update token usage information from USAGE.
USAGE is part of the response, INFO is the request plist."
  (when usage
    (let* ((input (or (plist-get usage :promptTokenCount) 0))
           (output (- (or (plist-get usage :totalTokenCount) 0) input))
           (cached (or (plist-get usage :cachedContentTokenCount) 0))
           (tokens (list :input (- input cached) :output output :cached cached)))
      ;; promptTokenCount includes the cached tokens, but we capture and display
      ;; the two exclusively in the UI.
      (plist-put info :tokens tokens)
      (plist-put info :tokens-full
                 (gptel--sum-plists (plist-get info :tokens-full)
                                    tokens)))))

;; TODO: Using alt=sse in the query url generates an OpenAI style streaming
;; response, with more immediate updates.  Maybe we should switch to that and
;; rewrite the stream parser?
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-gemini) info)
  "Parse a Gemini data stream.

Return the text response accumulated since the last call to this
function.  Additionally, mutate state INFO to add tool-use
information if the stream contains it."
  (let* ((content-strs))
    (condition-case nil
        (while (prog1 (search-forward "{" nil t) ; while-let is Emacs 29.1+ only
                 (backward-char 1))
          (save-match-data
            (when-let* ((response (gptel--json-read))
                        (text (gptel--parse-response
                               (plist-get info :backend)
                               response info 'include)))
              (push text content-strs))))
      (error
       (goto-char (match-beginning 0))))
    (apply #'concat (nreverse content-strs))))

(cl-defmethod gptel--parse-response ((_backend gptel-gemini) response info
                                     &optional include-text)
  "Parse an Gemini (non-streaming) RESPONSE and return response text.

Mutate state INFO with response metadata.

If INCLUDE-TEXT is non-nil, include response text in the prompts
list."
  (let* ((cand0 (map-nested-elt response '(:candidates 0)))
         (parts (map-nested-elt cand0 '(:content :parts)))
         (stop-reason (plist-get cand0 :finishReason)))
    (when stop-reason
      (plist-put info :stop-reason stop-reason)
      (gptel--gemini-update-tokens (plist-get response :usageMetadata) info))
    (cl-loop
     for part across parts
     for tx = (plist-get part :text)
     if (and tx (not (eq tx :null)))
     if (plist-get part :thought)
     do (unless (plist-get info :reasoning-block)
          (plist-put info :reasoning-block 'in))
     (plist-put info :reasoning (concat (plist-get info :reasoning) tx))
     else do
     (if (eq (plist-get info :reasoning-block) 'in)
       (plist-put info :reasoning-block t))
     and collect tx into content-strs end
     else if (plist-get part :functionCall)
     collect (copy-sequence it) into tool-use
     finally do                         ;Add text and tool-calls to prompts list
     (when (or tool-use include-text)
       (let* ((data (plist-get info :data))
              (prompts (plist-get data :contents))
              (last-prompt (aref prompts (1- (length prompts)))))
         (if (equal (plist-get last-prompt :role) "model")
             ;; When streaming, the last prompt may already have the role
             ;; "model" from prior calls to this function.  Append to its parts
             ;; instead of adding a new model role then.
             (plist-put last-prompt :parts
                        (vconcat (plist-get last-prompt :parts) parts))
           (plist-put                   ;otherwise create a new "model" role
            data :contents
            (vconcat prompts `((:role "model" :parts ,parts)))))))
     (when tool-use                    ;Capture tool call data for running tools
       (plist-put info :tool-use
                  (nconc (plist-get info :tool-use) tool-use)))
     finally return
     (and content-strs (apply #'concat content-strs)))))

(cl-defmethod gptel--request-data ((backend gptel-gemini) prompts)
  "JSON encode PROMPTS for sending to the Gemini BACKEND."
  (let ((prompts-plist
         (list :contents (vconcat prompts)
               :safetySettings [(:category "HARM_CATEGORY_HARASSMENT"
                                 :threshold "BLOCK_NONE")
                                (:category "HARM_CATEGORY_SEXUALLY_EXPLICIT"
                                 :threshold "BLOCK_NONE")
                                (:category "HARM_CATEGORY_DANGEROUS_CONTENT"
                                 :threshold "BLOCK_NONE")
                                (:category "HARM_CATEGORY_HATE_SPEECH"
                                 :threshold "BLOCK_NONE")]))
        params)
    (if gptel-system-prompt
        (plist-put prompts-plist :systemInstruction
                   `(:parts [(:text ,gptel-system-prompt)])))
    (when gptel-use-tools
      (when (eq gptel-use-tools 'force)
        (plist-put prompts-plist :toolConfig
                   '(:functionCallingConfig (:mode "ANY"))))
      (when gptel-tools
        (plist-put prompts-plist :tools
                   (gptel--parse-tools backend gptel-tools))))
    (when gptel-temperature
      (setq params
            (plist-put params
                       :temperature (max 0.0 gptel-temperature))))
    (when gptel-max-tokens
      (setq params
            (plist-put params
                       :maxOutputTokens gptel-max-tokens)))
    (when gptel-include-reasoning
      (setq params
            (plist-put params :thinkingConfig '(:includeThoughts t))))
    (when gptel--schema
      (setq params (nconc params (gptel--gemini-filter-schema
                                  (gptel--parse-schema backend gptel--schema)))))
    (when params
      (plist-put prompts-plist
                 :generationConfig params))
    ;; Merge request params with model and backend params.
    (gptel--merge-plists
     prompts-plist
     gptel--request-params
     (gptel-backend-request-params gptel-backend)
     (gptel--model-request-params  gptel-model))))

(cl-defmethod gptel--parse-schema ((_backend gptel-gemini) schema)
  "Convert SCHEMA to a Gemini API response schema spec."
  (list :responseMimeType "application/json"
        :responseSchema (gptel--preprocess-schema
                         (gptel--dispatch-schema-type schema))))

(defun gptel--gemini-filter-schema (schema)
  "Destructively filter unsupported attributes from SCHEMA.

Gemini's API does not support `additionalProperties'."
  (cl-remf schema :additionalProperties)
  (when (plistp schema)
    (cl-loop for (key val) on schema by #'cddr
             do (cond
                 ;; Recursively modify schemas within vectors (anyOf/allOf)
                 ((memq key '(:anyOf :allOf))
                  (dotimes (i (length val))
                    (aset val i (gptel--gemini-filter-schema (aref val i)))))
                 ;; Recursively modify plist values, which may contain sub-schemas
                 ((plistp val)
                  (when (cl-remf val :additionalProperties)
                    (cl-remf (plist-get schema key) :additionalProperties))
                  (gptel--gemini-filter-schema val))
                 ;; Default: do nothing to other key-value pairs yet.
                 (t nil))))
  schema)

(cl-defmethod gptel--parse-tools ((_backend gptel-gemini) tools)
  "Parse TOOLS to the Gemini API tool definition spec.

TOOLS is a list of `gptel-tool' structs, which see."
  (cl-loop
   for tool in (ensure-list tools)
   collect
   (list
    :name (gptel-tool-name tool)
    :description (gptel-tool-description tool)
    :parameters
    (if (not (gptel-tool-args tool))
         :null           ;NOTE: Gemini wants :null if the function takes no args
      (list :type "object"
            ;; See the generic implementation for an explanation of this
            ;; transformation.
            :properties
            (cl-loop
             for arg in (gptel-tool-args tool)
             for argspec = (copy-sequence arg)
             for name = (plist-get arg :name) ;handled differently
             for newname = (or (and (keywordp name) name)
                               (make-symbol (concat ":" name)))
             do                        ;ARGSPEC is ARG without unrecognized keys
             (cl-remf argspec :name)
             (cl-remf argspec :optional)
             if (equal (plist-get arg :type) "object")
             do (unless (plist-member argspec :required)
                  (plist-put argspec :required []))
             if (equal (plist-get arg :type) "string")
             do (cl-remf argspec :format)
             append (list newname (gptel--gemini-filter-schema argspec)))
            :required
            (vconcat
             (delq nil (mapcar
                        (lambda (arg) (and (not (plist-get arg :optional))
                                      (plist-get arg :name)))
                        (gptel-tool-args tool)))))))
   into tool-specs
   finally return `[(:functionDeclarations ,(vconcat tool-specs))]))

(cl-defmethod gptel--parse-tool-results ((_backend gptel-gemini) tool-use)
  "Return a prompt containing tool call results in TOOL-USE."
  (list
   :role "user"
   :parts
   (vconcat
    (mapcar
     (lambda (tool-call)
       (let ((result (plist-get tool-call :result))
             (name (plist-get tool-call :name)))
         `(:functionResponse
           (:name ,name :response
            (:name ,name :content ,result)))))
     tool-use))))

(cl-defmethod gptel--inject-prompt ((_backend gptel-gemini) data new-prompt &optional position)
  "Inject NEW-PROMPT into existing prompts in query DATA.

See generic implementation for full documentation."
  (when (keywordp (car-safe new-prompt)) ;Is new-prompt one or many?
    (setq new-prompt (list new-prompt)))
  (let ((prompts (plist-get data :contents)))
    (pcase position
      ('nil (plist-put data :contents (vconcat prompts new-prompt)))
      ((pred integerp)
       (when (< position 0) (setq position (+ (length prompts) position)))
       (plist-put data :contents (vconcat (substring prompts 0 position)
                                          new-prompt
                                          (substring prompts position)))))))

(cl-defmethod gptel--inject-tool-call ((_backend gptel-gemini) data tool-call new-call)
  "Replace TOOL-CALL in query DATA with NEW-CALL.

BACKEND is the `gptel-backend'.  See the generic function documentation
for details.  This implementation handles the Gemini API."
  ;; FIXME: We currently assume that the tool call being modified is in the last
  ;; position in the messages array.
  (if-let* ((messages (plist-get data :contents))
            (entry (aref messages (1- (length messages))))
            (parts (plist-get entry :parts))
            (indexed-call
             (cl-loop with name = (plist-get tool-call :name)
                      with old-args = (plist-get tool-call :args)
                      for chunk across parts
                      for i upfrom 0
                      if (plist-get chunk :functionCall)
                      if (and (equal (plist-get it :name) name)
                              (equal (plist-get it :args) old-args))
                      return (cons i (plist-get chunk :functionCall)) end
                      finally return nil))
            (index (car indexed-call))
            (call (cdr indexed-call)))
      (if (null new-call)
          (if (= (length parts) 1)
              (plist-put data :contents (substring messages nil -1))
            (plist-put entry :parts
                       (vconcat (substring parts 0 index)
                                (substring parts (1+ index)))))
        (when-let* ((args (plist-get new-call :args)))
          (plist-put call :args args))
        (when-let* ((name (plist-get new-call :name)))
          (plist-put call :name name)))
    (display-warning
     '(gptel tool-call)
     (format "Could not inject updated tool-call arguments for tool call %s, %s"
             (plist-get tool-call :name)
             (truncate-string-to-width (prin1-to-string new-call) 50 nil nil t)))))

(cl-defmethod gptel--parse-list ((backend gptel-gemini) prompt-list)
  "Parse PROMPT-LIST into a list of messages for BACKEND."
  (if (consp (car prompt-list))
      (let ((full-prompt))              ; Advanced format, list of lists
        (dolist (entry prompt-list)
          (pcase entry
            (`(prompt . ,msg)
             (push (list :role "user"
                         :parts `[(:text ,(or (car-safe msg) msg))])
                   full-prompt))
            (`(response . ,msg)
             (push (list :role "model"
                         :parts `[(:text ,(or (car-safe msg) msg))])
                   full-prompt))
            (`(tool . ,call)
             (push (list :role "model"
                         :parts (vector `(:functionCall ( :name ,(plist-get call :name)
                                                          :args ,(plist-get call :args)))))
                   full-prompt)
             (push (gptel--parse-tool-results backend (list (cdr entry))) full-prompt))))
        (nreverse full-prompt))
    (cl-loop for text in prompt-list    ; Simple format, list of strings
             for role = t then (not role)
             if text
             if role
             collect (list :role "user" :parts `[(:text ,text)]) into prompts
             else collect (list :role "model" :parts `[(:text ,text)]) into prompts
             finally return prompts)))

(cl-defmethod gptel--parse-buffer ((backend gptel-gemini) &optional max-entries)
  "Parse the current buffer into a list of messages for BACKEND.
Include up to MAX-ENTRIES queries/responses."
  (let ((prompts) (prev-pt (point)))
    (if (or gptel-mode gptel-track-response)
        (while (and (or (not max-entries) (>= max-entries 0))
                    (goto-char (previous-single-property-change
                                (point) 'gptel nil (point-min)))
                    (not (= (point) prev-pt)))
          (pcase (get-char-property (point) 'gptel)
            ('response
             (when-let* ((content (gptel--trim-prefixes
                                   (buffer-substring-no-properties (point) prev-pt))))
               (push (list :role "model" :parts `[(:text ,content)]) prompts)))
            (`(tool . ,_id)
             (save-excursion
               (condition-case nil
                   (let* ((tool-call (read (current-buffer)))
                          (name (plist-get tool-call :name))
                          (arguments  (plist-get tool-call :args)))
                     (plist-put tool-call :result
                                (string-trim (buffer-substring-no-properties
                                              (point) prev-pt)))
                     (push (gptel--parse-tool-results backend (list tool-call))
                           prompts)
                     (push (list :role "model"
                                 :parts
                                 (vector `(:functionCall ( :name ,name
                                                           :args ,arguments))))
                           prompts))
                 ((end-of-file invalid-read-syntax)
                  (message (format "Could not parse tool-call on line %s"
                                   (line-number-at-pos (point))))))))
            ('ignore)
            ('nil
             (if gptel-track-media
                 (when-let* ((content (gptel--gemini-parse-multipart
                                       (gptel--parse-media-links major-mode (point) prev-pt))))
                   (when (> (length content) 0)
                     (push (list :role "user" :parts content) prompts)))
               (when-let* ((content (gptel--trim-prefixes
                                     (buffer-substring-no-properties
                                      (point) prev-pt))))
                 (push (list :role "user" :parts `[(:text ,content)]) prompts)))))
          (setq prev-pt (point))
          (and max-entries (cl-decf max-entries)))
      (let ((content (string-trim (buffer-substring-no-properties
                                   (point-min) (point-max)))))
        (push (list :role "user" :parts `[(:text ,content)]) prompts)))
    prompts))

(defun gptel--gemini-parse-multipart (parts)
  "Convert a multipart prompt PARTS to the Gemini API format.

The input is an alist of the form
 ((:text \"some text\")
  (:media \"/path/to/media.png\" :mime \"image/png\")
  (:text \"More text\")).

The output is a vector of entries in a backend-appropriate
format."
  (cl-loop
   for part in parts
   for n upfrom 1
   with last = (length parts)
   for text = (plist-get part :text)
   for media = (plist-get part :media)
   if text do
   (and (or (= n 1) (= n last)) (setq text (gptel--trim-prefixes text))) and
   if text
   collect (list :text text) into parts-array end
   else if media
   collect
   `(:inline_data
     (:mime_type ,(plist-get part :mime)
      :data ,(gptel--base64-encode media)))
   into parts-array
   else if (plist-get part :textfile)
   collect
   (list :text (with-temp-buffer
                 (gptel--insert-file-string (plist-get part :textfile))
                 (buffer-string)))
   into parts-array
   finally return (vconcat parts-array)))

(cl-defmethod gptel--inject-media ((_backend gptel-gemini) prompts)
  "Wrap the first user prompt in PROMPTS with included media files.

Media files, if present, are placed in `gptel-context'."
  (when-let* ((media-list (gptel-context--collect-media)))
    (cl-callf (lambda (current)
                (vconcat (gptel--gemini-parse-multipart media-list)
                         current))
        (plist-get (car prompts) :parts))))

(defconst gptel--gemini-models
  (gptel--process-models
   '((gemini-pro-latest
      :description "Most powerful Gemini thinking model, always points to latest version"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048              ; 65536 output token limit
      :input-cost 1.25                  ; 2.50 for >200k tokens
      :output-cost 10.00                ; 15 for >200k tokens
      :cutoff-date "2025-01")
     (gemini-flash-latest
      :description "Best price / performance, always points to latest version"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048              ; 65536 output token limit
      :input-cost 0.3
      :output-cost 2.50
      :cutoff-date "2025-01")
     (gemini-flash-lite-latest
      :description "Fastest, cheapest Gemini model, always points to latest version"
      :capabilities (tool-use json media)
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "application/pdf" "text/plain"
                   "audio/x-aac" "audio/flac" "audio/mp3" "audio/m4a" "audio/mpeg"
                   "audio/mpga" "audio/mp4" "audio/opus" "audio/pcm" "audio/wav" "audio/webm"
                   "video/x-flv" "video/quicktime" "video/mpeg" "video/mp4"
                   "video/webm" "video/wmv" "video/3gpp")
      :context-window 1048              ; 64000 output token limit
      :input-cost 0.10
      :output-cost 0.40
      :cutoff-date "2025-01")
     (gemini-3.8-flash
      :description "Engineered for long-horizon software engineering, agents and critical reasoning"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048              ; 65536 output token limit
      :input-cost 0.75                  ; 1.50 from 2027-01-01
      :output-cost 3.75                 ; 7.50 from 2027-01-01
      :cutoff-date "2026-09")
     (gemini-3.7-flash
      :description "Next generation reasoning model with customizable thinking configurations"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048              ; 65536 output token limit
      :input-cost 0.75                  ; 1.50 from 2027-01-01
      :output-cost 3.75                 ; 7.50 from 2027-01-01
      :cutoff-date "2026-03")
     (gemini-3.6-flash
      :description "Most intelligent Gemini model built for speed, combining frontier intelligence with superior search and grounding"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048
      :input-cost 0.75                  ; 1.50 from 2027-01-01
      :output-cost 3.75                 ; 7.50 from 2027-01-01
      :cutoff-date "2026-03")
     (gemini-3.5-flash
      :description "Most intelligent Gemini model for sustained frontier performance in agentic and coding tasks"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048              ; 65536 output token limit
      :input-cost 1.50
      :output-cost 9.00
      :cutoff-date "2025-01")
     (gemini-3.5-flash-lite
      :description "Most cost-efficient multimodal Gemini model, optimized for high-volume agentic tasks and simple data processing"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048
      :input-cost 0.30
      :output-cost 2.50
      :cutoff-date "2026-07")
     (gemini-3.1-pro-preview
      :description "Most intelligent Gemini model with SOTA reasoning and multimodal understanding"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048              ; 65536 output token limit
      :input-cost 2.0                   ; 4.0 for >200k tokens
      :output-cost 12.00                ; 18.0 for >200k tokens
      :cutoff-date "2025-01")
     (gemini-3.1-flash-lite
      :description "DEPRECATED: Please use gemini-3.5-flash-lite instead"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048
      :input-cost 0.25
      :output-cost 1.50
      :cutoff-date "2025-01")
     (gemini-3-flash-preview
      :description "DEPRECATED: Please use gemini-3.6-flash or gemini-3.8-flash instead"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048              ; 65536 output token limit
      :input-cost 0.50
      :output-cost 3.00
      :cutoff-date "2025-01")
     (gemini-2.5-pro
      :description "Most powerful Gemini thinking model with state-of-the-art performance"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048              ; 65536 output token limit
      :input-cost 1.25                  ; 2.50 for >200k tokens
      :output-cost 10.00                ; 15 for >200k tokens
      :cutoff-date "2025-01")
     (gemini-2.5-flash
      :description "Best in terms of price-performance, with well-rounded capabilities"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "image/heic" "image/heif"
                   "application/pdf" "text/plain" "text/csv" "text/html"
                   "audio/mpeg" "audio/wav" "audio/ogg" "audio/flac" "audio/aac" "audio/mp3"
                   "video/mp4" "video/mpeg" "video/avi" "video/quicktime" "video/webm")
      :context-window 1048              ; 65536 output token limit
      :input-cost 0.3
      :output-cost 2.50
      :cutoff-date "2025-01")
     (gemini-2.5-flash-lite
      :description "Fastest & cheapest 2.5 model, for high-volume, latency-sensitive tasks"
      :capabilities (tool-use json media audio video)
      :mime-types ("image/png" "image/jpeg" "image/webp" "application/pdf" "text/plain"
                   "audio/x-aac" "audio/flac" "audio/mp3" "audio/m4a" "audio/mpeg"
                   "audio/mpga" "audio/mp4" "audio/opus" "audio/pcm" "audio/wav" "audio/webm"
                   "video/x-flv" "video/quicktime" "video/mpeg" "video/mp4"
                   "video/webm" "video/wmv" "video/3gpp")
      :context-window 1048              ; 64000 output token limit
      :input-cost 0.10
      :output-cost 0.40
      :cutoff-date "2025-01")))
  "List of available Gemini models and associated properties.
Keys:

- `:description': a brief description of the model.

- `:capabilities': a list of capabilities supported by the model.

- `:mime-types': a list of supported MIME types for media files.

- `:context-window': the context window size, in thousands of tokens.

- `:input-cost': the input cost, in US dollars per million tokens.

- `:output-cost': the output cost, in US dollars per million tokens.

- `:cutoff-date': the knowledge cutoff date.

- `:request-params': a plist of additional request parameters to
  include when using this model.

Information about the Gemini models was obtained from the following
source:

- <https://ai.google.dev/pricing>
- <https://ai.google.dev/gemini-api/docs/models>")

;;;###autoload
(cl-defun gptel-make-gemini
    (name &key curl-args key request-params
          (stream nil)
          (header
           (lambda (_info) (when-let* ((key (gptel--get-api-key)))
                        `(("X-goog-api-key" . ,key)))))
          (host "generativelanguage.googleapis.com")
          (protocol "https")
          (models gptel--gemini-models)
          (endpoint "/v1beta/models"))

  "Register a Gemini backend for gptel with NAME.

Keyword arguments:

CURL-ARGS (optional) is a list of additional Curl arguments.

HOST (optional) is the API host, defaults to
\"generativelanguage.googleapis.com\".

MODELS is a list of available model names, as symbols.
Additionally, you can specify supported LLM capabilities like
vision or tool-use by appending a plist to the model with more
information, in the form

 (model-name . plist)

For a list of currently recognized plist keys, see
`gptel--gemini-models'.  An example of a model specification
including both kinds of specs:

:models
\\='(gemini-2.0-flash-lite              ;Simple specs
  gemini-1.5-flash
  (gemini-1.5-pro-latest                ;Full spec
   :description
   \"Complex reasoning tasks, problem solving and data extraction\"
   :capabilities (tool json)
   :mime-types
   (\"image/jpeg\" \"image/png\" \"image/webp\" \"image/heic\")))


STREAM is a boolean to enable streaming responses, defaults to
false.

PROTOCOL (optional) specifies the protocol, \"https\" by default.

ENDPOINT (optional) is the API endpoint for completions, defaults to
\"/v1beta/models\".

HEADER (optional) is for additional headers to send with each
request.  It should be an alist or a function that retuns an
alist, like:
 ((\"Content-Type\" . \"application/json\"))

KEY (optional) is a variable whose value is the API key, or
function that returns the key.

REQUEST-PARAMS (optional) is a plist of additional HTTP request
parameters (as plist keys) and values supported by the API.  Use
these to set parameters that gptel does not provide user options
for."
  (declare (indent 1))
  (let ((backend (gptel--make-gemini
                  :curl-args curl-args
                  :name name
                  :host host
                  :header header
                  :models (gptel--process-models models)
                  :protocol protocol
                  :endpoint endpoint
                  :stream stream
                  :request-params request-params
                  :key key
                  :url (lambda (_info)
                         (let ((method
                                (if (and stream gptel-use-curl gptel-stream)
                                    "streamGenerateContent"
                                  "generateContent")))
                           (format "%s://%s%s/%s:%s"
                                   protocol
                                   host
                                   endpoint
                                   gptel-model
                                   method))))))
    (prog1 backend
      (setf (alist-get name gptel--known-backends
                       nil nil #'equal)
            backend))))

(provide 'gptel-gemini)
;;; gptel-gemini.el ends here

;; Local Variables:
;; byte-compile-warnings: (not docstrings)
;; End:
