From d93307924c89d740d148e817c2fd2176b9723dc9 Mon Sep 17 00:00:00 2001 From: Yetibot Date: Tue, 18 Aug 2026 03:18:21 +0000 Subject: [PATCH] add price tags to xai grok and image commands --- src/yetibot/core/commands/grok.clj | 2 +- src/yetibot/core/commands/image.clj | 2 +- src/yetibot/core/util/xai.clj | 25 ++++++++++++++++++- test/yetibot/core/test/commands/grok_test.clj | 3 ++- .../yetibot/core/test/commands/image_test.clj | 3 ++- test/yetibot/core/test/util/xai_test.clj | 22 ++++++++++++++++ 6 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/yetibot/core/commands/grok.clj b/src/yetibot/core/commands/grok.clj index 5bcdc47d..65ff595c 100644 --- a/src/yetibot/core/commands/grok.clj +++ b/src/yetibot/core/commands/grok.clj @@ -12,7 +12,7 @@ (let [prompt match _ (info "grok: generating text for prompt:" prompt) text (xai/generate-text prompt) - footer "\n\nSent via grok-4.6"] + footer (format "\n\nSent via grok-4.6 | Cost: $%.3f" (xai/cost-per-prompt))] {:result/value (str text footer) :result/data {:prompt prompt :response text}}) (catch Exception e diff --git a/src/yetibot/core/commands/image.clj b/src/yetibot/core/commands/image.clj index 70063e9b..23942594 100644 --- a/src/yetibot/core/commands/image.clj +++ b/src/yetibot/core/commands/image.clj @@ -18,7 +18,7 @@ id (store-image! image) base-url (gemini/yetibot-base-url) image-url (format "%s/generated-images/%s.png" base-url id) - footer "\n\nSent via grok-imagine-image-2.0"] + footer (format "\n\nSent via grok-imagine-image-2.0 | Cost: $%.3f" (xai/cost-per-image))] (info "image: image generated successfully, serving at" image-url) {:result/value (str image-url footer) :result/data {:id id :prompt match :url image-url}}) diff --git a/src/yetibot/core/util/xai.clj b/src/yetibot/core/util/xai.clj index e6dfe281..b0528d1a 100644 --- a/src/yetibot/core/util/xai.clj +++ b/src/yetibot/core/util/xai.clj @@ -7,13 +7,36 @@ [yetibot.core.config :refer [get-config]])) (s/def ::key string?) -(s/def ::config (s/keys :req-un [::key])) +(s/def ::cost-per-image (s/or :string string? :number number?)) +(s/def ::cost-per-prompt (s/or :string string? :number number?)) +(s/def ::config (s/keys :req-un [::key] :opt-un [::cost-per-image ::cost-per-prompt])) (def config (or (:value (get-config ::config [:xai])) {})) (defn configured? [] (some? (:key config))) +(defn- parse-number + "Parse a string to a number, returning the number unchanged if it's already a number. + Returns nil if parsing fails." + [v] + (if (string? v) + (try + (Double/parseDouble v) + (catch Exception _ nil)) + v)) + +(def ^:private default-cost-per-image 0.04) +(def ^:private default-cost-per-prompt 0.01) + +(defn cost-per-image [] + (or (parse-number (:cost-per-image config)) + default-cost-per-image)) + +(defn cost-per-prompt [] + (or (parse-number (:cost-per-prompt config)) + default-cost-per-prompt)) + (defn generate-image "Call the xAI API to generate an image from a text prompt." [prompt] diff --git a/test/yetibot/core/test/commands/grok_test.clj b/test/yetibot/core/test/commands/grok_test.clj index b76b2649..504ce899 100644 --- a/test/yetibot/core/test/commands/grok_test.clj +++ b/test/yetibot/core/test/commands/grok_test.clj @@ -10,8 +10,9 @@ (fact "it generates text and returns response with model footer when configured" (g/grok-cmd {:match "what is 2+2" :chat-source {}}) - => (contains {:result/value "4\n\nSent via grok-4.6" + => (contains {:result/value "4\n\nSent via grok-4.6 | Cost: $0.010" :result/data {:prompt "what is 2+2" :response "4"}}) (provided (xai/configured?) => true + (xai/cost-per-prompt) => 0.01 (xai/generate-text "what is 2+2") => "4"))) diff --git a/test/yetibot/core/test/commands/image_test.clj b/test/yetibot/core/test/commands/image_test.clj index 528b15dd..2f834723 100644 --- a/test/yetibot/core/test/commands/image_test.clj +++ b/test/yetibot/core/test/commands/image_test.clj @@ -11,10 +11,11 @@ (fact "it generates an image and returns URL with model footer when configured" (img/image-cmd {:match "image space kitty" :chat-source {}}) - => (contains {:result/value #"http://localhost:3003/generated-images/grok123.png\n\nSent via grok-imagine-image-2.0" + => (contains {:result/value #"http://localhost:3003/generated-images/grok123.png\n\nSent via grok-imagine-image-2.0 \| Cost: \$0.040" :result/data {:id "grok123" :prompt "image space kitty" :url "http://localhost:3003/generated-images/grok123.png"}}) (provided (xai/configured?) => true + (xai/cost-per-image) => 0.04 (yetibot.core.util.image-input/extract-images "image space kitty" {}) => {:prompt "space kitty" :image-urls []} (xai/generate-image "space kitty") => {:data "grokbytes" :mime-type "image/jpeg"} (yetibot.core.webapp.routes.images/store-image! {:data "grokbytes" :mime-type "image/jpeg"}) => "grok123" diff --git a/test/yetibot/core/test/util/xai_test.clj b/test/yetibot/core/test/util/xai_test.clj index 67f95ca0..a572db71 100644 --- a/test/yetibot/core/test/util/xai_test.clj +++ b/test/yetibot/core/test/util/xai_test.clj @@ -12,6 +12,28 @@ (with-redefs [xai/config {}] (xai/configured?)) => false)) +(facts "about xai cost-per-image" + (fact "it returns default if not overridden in config" + (with-redefs [xai/config {:key "secret-key"}] + (xai/cost-per-image)) => 0.04) + (fact "it returns the configured number value" + (with-redefs [xai/config {:key "secret-key" :cost-per-image 0.055}] + (xai/cost-per-image)) => 0.055) + (fact "it returns the parsed string value from config" + (with-redefs [xai/config {:key "secret-key" :cost-per-image "0.065"}] + (xai/cost-per-image)) => 0.065)) + +(facts "about xai cost-per-prompt" + (fact "it returns default if not overridden in config" + (with-redefs [xai/config {:key "secret-key"}] + (xai/cost-per-prompt)) => 0.01) + (fact "it returns the configured number value" + (with-redefs [xai/config {:key "secret-key" :cost-per-prompt 0.025}] + (xai/cost-per-prompt)) => 0.025) + (fact "it returns the parsed string value from config" + (with-redefs [xai/config {:key "secret-key" :cost-per-prompt "0.035"}] + (xai/cost-per-prompt)) => 0.035)) + (facts "about xai generate-image" (fact "it generates an image successfully when API returns valid data" (with-redefs [xai/config {:key "secret-key"}]