Skip to content

Commit 8a6f6b5

Browse files
committed
fix(veo): harden against silent failures — timeouts + redact secrets
The async restructuring alone left two silent-failure paths where the user gets neither a video nor an error: - The Veo HTTP calls (start/poll/download, image-to-video fetch) set no timeouts, so a stalled Google API call hangs the run indefinitely. Cap connect + read time so a hang surfaces as an error instead. - Error messages posted to chat could echo a Veo request URL, which carries the Gemini API key as a query param. Redact key=... before it reaches chat. Also keeps the unit tests from firing a real Veo HTTP call in a background future, and adds coverage for the redaction.
1 parent 4382772 commit 8a6f6b5

3 files changed

Lines changed: 42 additions & 14 deletions

File tree

src/yetibot/core/commands/veo.clj

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
(ns yetibot.core.commands.veo
2-
(:require [taoensso.timbre :refer [info error]]
2+
(:require [clojure.string :as string]
3+
[taoensso.timbre :refer [info error]]
34
[yetibot.core.hooks :refer [cmd-hook]]
45
[yetibot.core.util.image-input :as image-input]
56
[yetibot.core.util.gemini :as gemini]
67
[yetibot.core.chat :as chat]
78
[yetibot.core.webapp.routes.images :refer [store-image!]]))
89

10+
(defn redact
11+
"Strip a leaked Gemini API key from an error message before it reaches chat."
12+
[msg]
13+
(some-> msg (string/replace #"key=[^\s&\"]+" "key=***")))
14+
915
(defn veo-cmd
1016
"veo <prompt> # generate a short AI video with Veo
1117
@@ -36,13 +42,13 @@
3642
(chat/send-msg msg))
3743
(catch Exception e
3844
(error "veo: generation error in future:" (.getMessage e))
39-
(let [err-msg (str "Video generation failed: " (.getMessage e))
45+
(let [err-msg (str "Video generation failed: " (redact (.getMessage e)))
4046
msg (if user-mention (str user-mention ": " err-msg) err-msg)]
4147
(chat/send-msg msg))))))
4248
{:result/value (str "🎥 Grug start generating video for \"" prompt "\". This take some time (30s to 3m)...")})
4349
(catch Exception e
4450
(error "veo: initialization error:" (.getMessage e))
45-
{:result/error (str "Video generation initialization failed: " (.getMessage e))}))
51+
{:result/error (str "Video generation initialization failed: " (redact (.getMessage e)))}))
4652
{:result/error
4753
"Gemini API is not configured. Set `gemini.key` in config."}))
4854

src/yetibot/core/util/gemini.clj

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,17 @@
298298
(def ^:private veo-poll-interval-ms 6000)
299299
(def ^:private veo-max-polls 50) ; ~5 min ceiling
300300

301+
;; Cap connect + read time on every Veo HTTP call so a stalled Google API surfaces
302+
;; as an error instead of hanging the run forever — an otherwise silent failure
303+
;; where the user gets neither a video nor an error.
304+
(def ^:private veo-http-timeouts {:connection-timeout 10000 :socket-timeout 120000})
305+
301306
(defn- veo-image-part
302307
"Fetch an image URL and return a Veo instance image (base64), or nil. Used for
303308
image-to-video — e.g. a mentioned Discord user's avatar becomes the opening frame."
304309
[image-url]
305-
(let [resp (client/get image-url {:as :byte-array :throw-exceptions false})
310+
(let [resp (client/get image-url (merge veo-http-timeouts
311+
{:as :byte-array :throw-exceptions false}))
306312
mime (first (string/split (get-in resp [:headers "Content-Type"] "image/png") #";"))]
307313
(when (<= 200 (:status resp) 299)
308314
{:mimeType mime
@@ -315,10 +321,11 @@
315321
(let [url (format "%s/models/%s:predictLongRunning?key=%s" api-base (veo-model) (:key config))
316322
body {:instances [(cond-> {:prompt prompt} image (assoc :image image))]
317323
:parameters {:aspectRatio "16:9" :durationSeconds (veo-duration)}}
318-
resp (client/post url {:content-type :json
319-
:body (json/write-str body)
320-
:as :json
321-
:throw-exceptions false})]
324+
resp (client/post url (merge veo-http-timeouts
325+
{:content-type :json
326+
:body (json/write-str body)
327+
:as :json
328+
:throw-exceptions false}))]
322329
(if (<= 200 (:status resp) 299)
323330
(get-in resp [:body :name])
324331
(let [msg (extract-api-error (:body resp))]
@@ -331,7 +338,8 @@
331338
[op-name]
332339
(loop [n 0]
333340
(let [body (:body (client/get (format "%s/%s?key=%s" api-base op-name (:key config))
334-
{:as :json :throw-exceptions false}))]
341+
(merge veo-http-timeouts
342+
{:as :json :throw-exceptions false})))]
335343
(cond
336344
(:error body) (throw (ex-info (str "Veo generation failed: "
337345
(get-in body [:error :message]))
@@ -344,9 +352,10 @@
344352
(defn- veo-download
345353
"Download the generated mp4 and return it base64-encoded."
346354
[uri]
347-
(let [resp (client/get uri {:headers {"x-goog-api-key" (:key config)}
348-
:as :byte-array
349-
:throw-exceptions false})]
355+
(let [resp (client/get uri (merge veo-http-timeouts
356+
{:headers {"x-goog-api-key" (:key config)}
357+
:as :byte-array
358+
:throw-exceptions false}))]
350359
(if (<= 200 (:status resp) 299)
351360
(.encodeToString (Base64/getEncoder) ^bytes (:body resp))
352361
(throw (ex-info (str "Veo video download failed: " (:status resp))

test/yetibot/core/test/commands/veo.clj

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
(ns yetibot.core.test.commands.veo
2-
(:require [midje.sweet :refer [facts fact => provided anything throws]]
2+
(:require [midje.sweet :refer [facts fact => =not=> provided anything throws]]
3+
[midje.checkers :refer [contains]]
34
[yetibot.core.commands.veo :as veo]
45
[yetibot.core.util.gemini :as gemini]
56
[yetibot.core.util.image-input :as image-input]
@@ -16,7 +17,9 @@
1617
(binding [chat/*adapter* :mock-adapter
1718
chat/*target* :mock-target
1819
chat/*thread-ts* :mock-thread-ts]
19-
(veo/veo-cmd {:match "a cool robot dancing" :chat-source {} :user {:id "user123"}})
20+
;; no-op the future so the unit test never makes a real Veo HTTP call
21+
(with-redefs [clojure.core/future-call (fn [_] nil)]
22+
(veo/veo-cmd {:match "a cool robot dancing" :chat-source {} :user {:id "user123"}}))
2023
=> {:result/value "🎥 Grug start generating video for \"a cool robot dancing\". This take some time (30s to 3m)..."}
2124
(provided
2225
(gemini/configured?) => true
@@ -49,3 +52,13 @@
4952
(image-input/extract-images "a cool robot dancing" {}) => {:prompt "a cool robot dancing" :image-urls nil}
5053
(gemini/generate-video "a cool robot dancing" nil) => (throw (Exception. "API Error"))
5154
(chat/send-msg "<@user123>: Video generation failed: API Error") => anything))))
55+
56+
(facts "about redact"
57+
(fact "masks a leaked api key embedded in an error"
58+
(veo/redact "predictLongRunning?key=AIzaSyABC123 Read timed out") => (contains "key=***"))
59+
(fact "does not expose the key"
60+
(veo/redact "?key=AIzaSyABC123 boom") =not=> (contains "AIzaSyABC123"))
61+
(fact "leaves a clean message untouched"
62+
(veo/redact "Video generation failed: API Error") => "Video generation failed: API Error")
63+
(fact "tolerates nil"
64+
(veo/redact nil) => nil))

0 commit comments

Comments
 (0)