Skip to content

Instantly share code, notes, and snippets.

@QiangF
Created March 25, 2026 03:37
Show Gist options
  • Select an option

  • Save QiangF/5080eb98fa3c70379aa9517720b7f8f1 to your computer and use it in GitHub Desktop.

Select an option

Save QiangF/5080eb98fa3c70379aa9517720b7f8f1 to your computer and use it in GitHub Desktop.
http/2 429 error
HTTP/2 429
vary: Origin
vary: X-Origin
vary: Referer
content-type: application/json; charset=UTF-8
content-encoding: gzip
date: Wed, 25 Mar 2026 03:22:37 GMT
server: scaffolding on HTTPServer2
content-length: 581
x-xss-protection: 0
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
server-timing: gfet4t7; dur=467
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
[{
"error": {
"code": 429,
"message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 20, model: gemini-2.5-flash\nPlease retry in 22.906018063s.",
"status": "RESOURCE_EXHAUSTED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.Help",
"links": [
{
"description": "Learn more about Gemini API quotas",
"url": "https://ai.google.dev/gemini-api/docs/rate-limits"
}
]
},
{
"@type": "type.googleapis.com/google.rpc.QuotaFailure",
"violations": [
{
"quotaMetric": "generativelanguage.googleapis.com/generate_content_free_tier_requests",
"quotaId": "GenerateRequestsPerDayPerProjectPerModel-FreeTier",
"quotaDimensions": {
"location": "global",
"model": "gemini-2.5-flash"
},
"quotaValue": "20"
}
]
},
{
"@type": "type.googleapis.com/google.rpc.RetryInfo",
"retryDelay": "22s"
}
]
}
}
]
(use-package gptel-gemini-key-rotation
:ensure nil
:after (gptel)
:demand t
:config
;; (gptel-backends-llama-swap)
;; (gptel-backends-vibe-proxy)
(require 'gptel-gemini-key-rotation)
(require 'gptel-gemini)
(gptel-gemini-key-rotation-setup)
(setq gptel-model 'gemini-2.5-flash
gptel-backend
(gptel-make-gemini-rotating "Gemini" :stream t
:models '(gemini-2.5-flash gemini-2.5-pro gemini-2.5-flash-lite))))
;;; gptel-gemini-key-rotation.el --- Automatic Gemini API key rotation for gptel on errors -*- lexical-binding: t; -*-
;; This package provides automatic API key rotation for Gemini backends in gptel.
;; When a request fails (e.g., due to rate limiting or invalid key), it automatically
;; tries the next available key from authinfo.
(require 'gptel)
(require 'auth-source)
;;; Configuration
(defgroup gptel-gemini-key-rotation nil
"Automatic API key rotation for Gemini backends in gptel."
:group 'gptel)
(defcustom gptel-gemini-key-rotation-enabled t
"Enable automatic key rotation for Gemini backends."
:type 'boolean
:group 'gptel-gemini-key-rotation)
(defcustom gptel-gemini-authinfo-host "generativelanguage.googleapis.com"
"Host identifier for Gemini API keys in authinfo.
This is the 'machine' field in ~/.authinfo entries."
:type 'string
:group 'gptel-gemini-key-rotation)
(defcustom gptel-gemini-authinfo-user "apikey"
"User identifier for Gemini API keys in authinfo.
This is the 'login' field in ~/.authinfo entries."
:type 'string
:group 'gptel-gemini-key-rotation)
(defcustom gptel-gemini-key-rotation-errors
'("401" "403" "429" "key" "unauthorized" "forbidden" "rate limit" "quota")
"List of error strings that trigger key rotation.
If any of these strings appear in the error message/status, rotation is attempted."
:type '(repeat string)
:group 'gptel-gemini-key-rotation)
;;; Internal Variables
(defvar gptel-gemini--key-ring nil
"Cache of available Gemini API keys from authinfo.")
(defvar gptel-gemini--current-key-index 0
"Index of the currently active key in the key ring.")
(defvar gptel-gemini--rotation-in-progress nil
"Flag to prevent recursive rotation attempts.")
;;; Key Management
(defun gptel-gemini--fetch-all-keys ()
"Fetch all Gemini API keys from authinfo.
Returns a list of (SECRET . METADATA) pairs."
(let ((results (auth-source-search
:host gptel-gemini-authinfo-host
:user gptel-gemini-authinfo-user
:require '(:secret)
:max 100)))
(mapcar (lambda (entry)
(let ((secret (plist-get entry :secret)))
;; Handle both string secrets and function secrets
(when (functionp secret)
(setq secret (funcall secret)))
(cons secret entry)))
results)))
(defun gptel-gemini--refresh-key-ring ()
"Refresh the key ring from authinfo."
(setq gptel-gemini--key-ring (gptel-gemini--fetch-all-keys))
(setq gptel-gemini--current-key-index 0)
(message "Gemini key ring refreshed: %d keys available"
(length gptel-gemini--key-ring)))
(defun gptel-gemini--get-current-key ()
"Get the currently active API key."
(unless gptel-gemini--key-ring
(gptel-gemini--refresh-key-ring))
(when gptel-gemini--key-ring
(car (nth gptel-gemini--current-key-index gptel-gemini--key-ring))))
(defun gptel-gemini--rotate-key ()
"Rotate to the next available API key.
Returns the new key if rotation successful, nil if no more keys available."
(setq gptel-gemini--current-key-index (1+ gptel-gemini--current-key-index))
(if (>= gptel-gemini--current-key-index (length gptel-gemini--key-ring))
(progn
(message "Gemini key rotation: exhausted all available keys")
nil)
(let ((new-key (gptel-gemini--get-current-key)))
(message "Gemini key rotation: switched to key %d/%d"
(1+ gptel-gemini--current-key-index)
(length gptel-gemini--key-ring))
new-key)))
(defun gptel-gemini--should-rotate-p (info)
"Determine if we should rotate keys based on error INFO."
(and gptel-gemini-key-rotation-enabled
(not gptel-gemini--rotation-in-progress)
gptel-gemini--key-ring
(> (length gptel-gemini--key-ring) 1)
(let ((status (plist-get info :http-status))
(error-msg (format "%s" (plist-get info :error))))
(cl-some (lambda (pattern)
(or (and status (string-match-p (regexp-quote pattern) status))
(string-match-p (regexp-quote pattern) error-msg)))
gptel-gemini-key-rotation-errors))))
;;; Backend Integration
(defun gptel-gemini--make-rotating-key-function ()
"Create a function that returns the current key from the rotating key ring.
This function is suitable for use as the :key parameter in `gptel-make-gemini'."
(lambda ()
(or (gptel-gemini--get-current-key)
(progn
(gptel-gemini--refresh-key-ring)
(gptel-gemini--get-current-key)))))
(defun gptel-make-gemini-rotating (name &rest args)
"Create a Gemini backend with automatic key rotation.
NAME is the backend name. ARGS are passed to `gptel-make-gemini',
but :key is automatically set to use the rotating key ring.
Example:
(gptel-make-gemini-rotating \"Gemini-Rotating\"
:stream t
:models \\='(gemini-1.5-flash gemini-1.5-pro))"
(unless gptel-gemini--key-ring
(gptel-gemini--refresh-key-ring))
;; Remove :key from args if provided, we use our own
(setq args (plist-put args :key (gptel-gemini--make-rotating-key-function)))
(apply #'gptel-make-gemini name args))
;;; Error Handling Integration
(defun gptel-gemini--handle-error (info)
"Handle gptel errors by rotating keys if appropriate.
INFO is the plist containing error information."
(when (gptel-gemini--should-rotate-p info)
(let ((gptel-gemini--rotation-in-progress t))
(if-let ((new-key (gptel-gemini--rotate-key)))
(progn
;; Force backend to pick up new key by clearing any cached value
(when-let ((backend (plist-get info :backend)))
;; Update the backend's key slot if it exists
(when (slot-exists-p backend 'key)
(setf (slot-value backend 'key)
(gptel-gemini--make-rotating-key-function))))
;; Retry the request
(message "Retrying request with rotated key...")
(run-with-timer 0.5 nil #'gptel-send))
(message "No more Gemini keys available. Please check your authinfo.")))))
;;; Hook Installation
(defun gptel-gemini-key-rotation-setup ()
"Set up automatic key rotation hooks."
(interactive)
;; Initialize key ring
(gptel-gemini--refresh-key-ring)
;; not triggered on response with error "code": 429
(advice-add 'gptel--handle-error :around #'gptel-gemini--check-error-and-rotate)
(message "Gemini key rotation enabled with %d keys"
(length gptel-gemini--key-ring)))
;; Method 1: Advice on gptel--handle-error (receives FSM with full info)
(defun gptel-gemini--check-error-and-rotate (orig-fun fsm)
"Wrap gptel--handle-error to rotate keys on error."
(let* ((info (gptel-fsm-info fsm))
(error-data (plist-get info :error)))
(when (and error-data
(gptel-gemini--should-rotate-p info))
(gptel-gemini--handle-error info))
(funcall orig-fun fsm)))
;; Method 2: Wrap gptel-request to intercept errors
;; (advice-add 'gptel-request :around #'gptel-gemini--wrap-request)
(defun gptel-gemini--wrap-request (orig-fun &rest args)
"Wrap gptel-request to handle key rotation on errors."
;; args is (prompt :key val :key2 val2 ...)
;; Extract plist portion (everything after prompt)
(if (null args)
(apply orig-fun args)
(let* ((prompt (car args))
(plist (cdr args))
(callback (plist-get args :callback))
(wrapped-callback
(lambda (response info)
(when (and (not (stringp response))
(not (eq response t)) ; t means success with streaming
(gptel-gemini--should-rotate-p info))
(gptel-gemini--handle-error info))
(when callback
(funcall callback response info))))
(new-plist (plist-put plist :callback wrapped-callback)))
(apply orig-fun prompt new-plist))))
;;; Utility Commands
(defun gptel-gemini-force-rotate ()
"Manually force key rotation."
(interactive)
(if-let ((new-key (gptel-gemini--rotate-key)))
(message "Manually rotated to key %d"
(1+ gptel-gemini--current-key-index))
(message "No more keys available")))
(defun gptel-gemini-reset-key-ring ()
"Reset key ring to start from the first key again."
(interactive)
(setq gptel-gemini--current-key-index 0)
(message "Key ring reset to key 1"))
(provide 'gptel-gemini-key-rotation)
;;; gptel-gemini-key-rotation.el ends here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment