aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-10 18:31:51 +0200
committerDanilo M. <danix@danix.xyz>2026-08-10 18:31:51 +0200
commita7dd41385baa8f3ac34deda827f0344f82fb713e (patch)
tree0760cfd549779a7d118ecff3de2a934ca8a7f2bd
parentc8d3298e1bb6a9245449dac1c8727ce3d932cd1d (diff)
downloadllamachat-a7dd41385baa8f3ac34deda827f0344f82fb713e.tar.gz
llamachat-a7dd41385baa8f3ac34deda827f0344f82fb713e.zip
feat: cost label showing spend and the next send's projection
Three states stay distinct: blank for a free local model, ? for a cloud model whose prices were never entered, and a figure when they were. Collapsing the first two would make an unpriced cloud model read as free. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--llamachat/ui.py52
-rwxr-xr-xtest_llamachat.py31
2 files changed, 82 insertions, 1 deletions
diff --git a/llamachat/ui.py b/llamachat/ui.py
index 7d5c4a2..6c26ca2 100644
--- a/llamachat/ui.py
+++ b/llamachat/ui.py
@@ -32,7 +32,7 @@ from PySide6.QtWidgets import (
QPushButton, QRadioButton, QSplitter, QTextBrowser, QVBoxLayout, QWidget,
)
-from . import backend, prompts
+from . import backend, models as models_mod, prompts
from .backend import Attachment, BackendError, SearchConfig
from .config import GLOBAL_PROMPT
@@ -133,6 +133,56 @@ def _short(count: int) -> str:
return str(count)
+def cost_text(spent: float, projected: float, billable: bool, priced: bool) -> str:
+ """The cost label beside the context meter.
+
+ Three states, deliberately distinct: a local model shows nothing, a
+ cloud model with no price entered shows '?', and a priced one shows
+ what it has cost plus what the composed draft would add. Blank and '?'
+ must not collapse into each other, or an unpriced cloud model reads as
+ free.
+ """
+ if not billable:
+ return ""
+ if not priced:
+ return "?"
+ text = models_mod.format_cost(spent)
+ if projected > 0:
+ text += f" +{models_mod.format_cost(projected)}"
+ return text
+
+
+class CostLabel(QLabel):
+ """A one-line money readout that hides itself when there is nothing to say."""
+
+ def __init__(self):
+ super().__init__("")
+ self.setToolTip("")
+
+ def set_cost(
+ self, spent: float, projected: float, billable: bool, priced: bool
+ ) -> None:
+ text = cost_text(spent, projected, billable, priced)
+ self.setText(text)
+ self.setVisible(bool(text))
+ if not billable:
+ self.setToolTip("")
+ elif not priced:
+ self.setToolTip(
+ "No prices set for this model.\n"
+ "Use Model settings to enter them."
+ )
+ else:
+ tip = f"{models_mod.format_cost(spent)} spent in this conversation"
+ if projected > 0:
+ tip += (
+ f"\n+{models_mod.format_cost(projected)} to send what is "
+ "composed now"
+ )
+ tip += "\nApproximate: based on the prices you entered."
+ self.setToolTip(tip)
+
+
class StreamWorker(QObject):
"""Runs one streaming request off the GUI thread."""
diff --git a/test_llamachat.py b/test_llamachat.py
index b561b66..18b6f0a 100755
--- a/test_llamachat.py
+++ b/test_llamachat.py
@@ -2025,6 +2025,36 @@ def test_model_dialog_values():
print("ok model dialog value conversion")
+def test_cost_label_text():
+ """The label distinguishes free, unpriced, and a real figure."""
+ from llamachat import ui
+
+ # A local model costs nothing, so the label says nothing.
+ assert ui.cost_text(spent=0.0, projected=0.0, billable=False, priced=False) == ""
+
+ # A cloud model whose price was never entered: ? rather than blank, so
+ # it cannot be mistaken for free.
+ assert ui.cost_text(
+ spent=0.0, projected=0.0, billable=True, priced=False
+ ) == "?"
+
+ # Spent so far, with nothing composed yet.
+ assert ui.cost_text(
+ spent=0.043, projected=0.0, billable=True, priced=True
+ ) == "$0.043"
+
+ # Spent plus what sending the draft would add, kept visually separate.
+ assert ui.cost_text(
+ spent=0.043, projected=0.011, billable=True, priced=True
+ ) == "$0.043 +$0.011"
+
+ # A fresh conversation on a priced model still shows the projection.
+ assert ui.cost_text(
+ spent=0.0, projected=0.002, billable=True, priced=True
+ ) == "$0.000 +$0.002"
+ print("ok cost label text")
+
+
class _FakeResponse:
"""Enough of an http.client response for urlopen's context manager."""
@@ -2788,6 +2818,7 @@ if __name__ == "__main__":
test_client_auth_header()
test_multi_client()
test_model_dialog_values()
+ test_cost_label_text()
test_search_tool_schema()
test_search_results_sanitising()
test_tool_call_accumulation()