Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/yetibot/core/commands/banana.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@
[yetibot.core.util.gemini :as gemini]
[yetibot.core.webapp.routes.images :refer [store-image!]]))

(defn banana-budget-cmd
"banana budget # show monthly budget status"
{:yb/cat #{:info}}
[_]
(if (gemini/configured?)
(try
(let [{:keys [images-generated max-images spent budget remaining images-left veo-clips-left veo-cost-units month]}
(gemini/budget-status)]
{:result/value (format "Monthly Gemini budget status for %s:\n- Total Budget: $%.2f\n- Spent: $%.2f (%.1f%%)\n- Remaining: $%.2f\n- Image Units Generated: %d/%d\n- Remaining capacity: ~%d images OR ~%d Veo video clips (each clip costs %d image-units)"
month budget spent (* 100 (/ spent budget)) remaining images-generated max-images images-left veo-clips-left veo-cost-units)
:result/data (gemini/budget-status)})
(catch Exception e
(error "banana budget error:" (.getMessage e))
{:result/error (str "Could not load budget status: " (.getMessage e))}))
{:result/error
"Gemini API is not configured. Set `gemini.key` in config."}))

(defn banana-cmd
"banana <prompt> # generate an image using Gemini nano banana image generation"
{:yb/cat #{:img}}
Expand All @@ -29,4 +46,5 @@
"Gemini API is not configured. Set `gemini.key` in config."}))

(cmd-hook #"banana"
#"budget" banana-budget-cmd
#".+" banana-cmd)
18 changes: 18 additions & 0 deletions src/yetibot/core/commands/veo.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@
[yetibot.core.util.gemini :as gemini]
[yetibot.core.webapp.routes.images :refer [store-image!]]))

(defn veo-budget-cmd
"veo budget # show monthly budget status"
{:yb/cat #{:info}}
[_]
(if (gemini/configured?)
(try
(let [{:keys [images-generated max-images spent budget remaining images-left veo-clips-left veo-cost-units month]}
(gemini/budget-status)]
{:result/value (format "Monthly Gemini budget status for %s:\n- Total Budget: $%.2f\n- Spent: $%.2f (%.1f%%)\n- Remaining: $%.2f\n- Image Units Generated: %d/%d\n- Remaining capacity: ~%d images OR ~%d Veo video clips (each clip costs %d image-units)"
month budget spent (* 100 (/ spent budget)) remaining images-generated max-images images-left veo-clips-left veo-cost-units)
:result/data (gemini/budget-status)})
(catch Exception e
(error "veo budget error:" (.getMessage e))
{:result/error (str "Could not load budget status: " (.getMessage e))}))
{:result/error
"Gemini API is not configured. Set `gemini.key` in config."}))

(defn veo-cmd
"veo <prompt> # generate a short AI video with Veo

Expand All @@ -31,4 +48,5 @@
"Gemini API is not configured. Set `gemini.key` in config."}))

(cmd-hook #"veo"
#"budget" veo-budget-cmd
#".+" veo-cmd)
10 changes: 9 additions & 1 deletion src/yetibot/core/util/gemini.clj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
(s/def ::monthly (s/keys :opt-un [::budget]))
(s/def ::budget (s/or :string string? :number number?))

(declare veo-cost-units)

(s/def ::config (s/keys :req-un [::key] :opt-un [::cost ::monthly]))

;; All Gemini settings live under [:gemini] and share a single API key
Expand Down Expand Up @@ -120,12 +122,18 @@
(let [{:keys [count]} @usage-tracker
max-imgs (max-images-per-month)
spent (* count (cost-per-image))
remaining (- (monthly-budget) spent)]
remaining (max 0.0 (- (monthly-budget) spent))
v-units (veo-cost-units)
images-left (long (Math/floor (/ remaining (cost-per-image))))
veo-clips-left (long (Math/floor (/ images-left v-units)))]
{:images-generated count
:max-images max-imgs
:spent (double spent)
:budget (monthly-budget)
:remaining (double remaining)
:images-left images-left
:veo-clips-left veo-clips-left
:veo-cost-units v-units
:month (:month @usage-tracker)}))

(defn- check-budget!
Expand Down
23 changes: 23 additions & 0 deletions test/yetibot/core/test/commands/banana_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
(ns yetibot.core.test.commands.banana-test
(:require [midje.sweet :refer [facts fact => contains provided]]
[yetibot.core.commands.banana :as b]
[yetibot.core.util.gemini :as gemini]))

(facts "about banana-budget-cmd"
(fact "it returns an error if Gemini is not configured"
(b/banana-budget-cmd {}) => (contains {:result/error string?})
(provided (gemini/configured?) => false))

(fact "it returns budget status if configured"
(b/banana-budget-cmd {}) => (contains {:result/value string? :result/data map?})
(provided
(gemini/configured?) => true
(gemini/budget-status) => {:images-generated 0
:max-images 100
:spent 0.0
:budget 10.0
:remaining 10.0
:images-left 100
:veo-clips-left 20
:veo-cost-units 5
:month "2026-05"})))
23 changes: 23 additions & 0 deletions test/yetibot/core/test/commands/veo_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
(ns yetibot.core.test.commands.veo-test
(:require [midje.sweet :refer [facts fact => contains provided]]
[yetibot.core.commands.veo :as v]
[yetibot.core.util.gemini :as gemini]))

(facts "about veo-budget-cmd"
(fact "it returns an error if Gemini is not configured"
(v/veo-budget-cmd {}) => (contains {:result/error string?})
(provided (gemini/configured?) => false))

(fact "it returns budget status if configured"
(v/veo-budget-cmd {}) => (contains {:result/value string? :result/data map?})
(provided
(gemini/configured?) => true
(gemini/budget-status) => {:images-generated 0
:max-images 100
:spent 0.0
:budget 10.0
:remaining 10.0
:images-left 100
:veo-clips-left 20
:veo-cost-units 5
:month "2026-05"})))
20 changes: 20 additions & 0 deletions test/yetibot/core/test/util/gemini_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
(ns yetibot.core.test.util.gemini-test
(:require
[midje.sweet :refer [=> fact facts contains provided anything]]
[yetibot.core.db.image-budget :as image-budget]
[yetibot.core.util.gemini :as gemini]))

(facts "about budget-status"
(fact "it calculates budget status including veo details"
(let [status (gemini/budget-status)]
status => (contains {:images-generated integer?
:max-images integer?
:spent number?
:budget number?
:remaining number?
:images-left integer?
:veo-clips-left integer?
:veo-cost-units integer?
:month string?}))
(provided
(image-budget/query (contains {:where/map {:month anything}})) => [])))
Loading