Skip to content

Commit 75a2817

Browse files
committed
Add usage tokens + cost to chat messages
1 parent 1e5f2df commit 75a2817

File tree

7 files changed

+98
-30
lines changed

7 files changed

+98
-30
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Add usage tokens + cost to chat messages.
6+
57
## 0.5.1
68

79
- Fix openai key

docs/protocol.md

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ interface ChatPromptResponse {
346346
* The model used for this chat request.
347347
*/
348348
model: ChatModel;
349-
349+
350350
status: 'success';
351351
}
352352
```
@@ -376,16 +376,6 @@ interface ChatContentReceivedParams {
376376
* The owner of this content.
377377
*/
378378
role: 'user' | 'system' | 'assistant';
379-
380-
/**
381-
* Optional metadata about the generation
382-
*/
383-
metadata?: {
384-
/**
385-
* Number of tokens used in the generation
386-
*/
387-
tokensUsed?: number;
388-
};
389379
}
390380

391381
/**
@@ -395,6 +385,7 @@ type ChatContent =
395385
| TextContent
396386
| URLContent
397387
| ProgressContent
388+
| UsageContent
398389
| FileChangeContent
399390
| ToolCallPrepareContent
400391
| ToolCallRunContent
@@ -429,20 +420,35 @@ interface URLContent {
429420
}
430421

431422
/**
432-
* Details about the progress of the chat completion.
423+
* Details about the chat's usage, like used tokens and costs.
433424
*/
434-
interface ProgressContent {
435-
type: 'progress';
436-
425+
interface UsageContent {
426+
type: 'usage';
427+
428+
/*
429+
* Number of tokens sent on previous prompt including all context used by ECA.
430+
*/
431+
messageInputTokens: number;
432+
433+
/*
434+
* Number of tokens received from LLm in last prompt.
435+
*/
436+
messageOutputTokens: number;
437+
438+
/**
439+
* The total input + output tokens of the whole chat session so far.
440+
*/
441+
sessionTokens: number;
442+
437443
/**
438-
* The statue of this progress
444+
* The cost of the last sent message summing input + output tokens.
439445
*/
440-
state: 'running' | 'finished';
446+
messageCost?: string;
441447

442448
/**
443-
* The text detailing the progress
449+
* The cost of the whole chat session so far.
444450
*/
445-
text?: string;
451+
sessionCost?: string;
446452
}
447453

448454
/**

src/eca/db.clj

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
(set! *warn-on-reflection* true)
44

5+
(def ^:private one-million 1000000)
6+
57
(defonce initial-db
68
{:client-info {}
79
:workspace-folders []
@@ -10,17 +12,29 @@
1012
:chat-behaviors ["agent" "chat"]
1113
:chat-default-behavior "agent"
1214
:models {"o4-mini" {:tools true
13-
:web-search false}
15+
:web-search false
16+
:input-token-cost (/ 1.10 one-million)
17+
:output-token-cost (/ 4.40 one-million)}
1418
"o3" {:tools true
15-
:web-search false}
19+
:web-search false
20+
:input-token-cost (/ 2.0 one-million)
21+
:output-token-cost (/ 8.0 one-million)}
1622
"gpt-4.1" {:tools true
17-
:web-search true}
23+
:web-search true
24+
:input-token-cost (/ 2.0 one-million)
25+
:output-token-cost (/ 8.0 one-million)}
1826
"claude-sonnet-4-0" {:tools true
19-
:web-search true}
27+
:web-search true
28+
:input-token-cost (/ 3.0 one-million)
29+
:output-token-cost (/ 15.0 one-million)}
2030
"claude-opus-4-0" {:tools true
21-
:web-search true}
31+
:web-search true
32+
:input-token-cost (/ 15.0 one-million)
33+
:output-token-cost (/ 75.0 one-million)}
2234
"claude-3-5-haiku-latest" {:tools true
23-
:web-search true}} ;; + ollama local models + custom provider models
35+
:web-search true
36+
:input-token-cost (/ 0.8 one-million)
37+
:output-token-cost (/ 4.0 one-million)}} ;; + ollama local models + custom provider models
2438
:mcp-clients {}})
2539

2640
(defonce db* (atom initial-db))

src/eca/features/chat.clj

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
[eca.llm-api :as llm-api]
1111
[eca.logger :as logger]
1212
[eca.messenger :as messenger]
13-
[eca.shared :as shared]))
13+
[eca.shared :as shared :refer [assoc-some]]))
1414

1515
(set! *warn-on-reflection* true)
1616

@@ -79,6 +79,15 @@
7979
(defn ^:private tool-name->origin [name all-tools]
8080
(:origin (first (filter #(= name (:name %)) all-tools))))
8181

82+
(defn ^:private tokens->cost [input-tokens output-tokens model db]
83+
(let [normalized-model (if (string/includes? model "/")
84+
(last (string/split model #"/"))
85+
model)
86+
{:keys [input-token-cost output-token-cost]} (get-in db [:models normalized-model])]
87+
(when (and input-token-cost output-token-cost)
88+
(format "%.2f" (+ (* input-tokens input-token-cost)
89+
(* output-tokens output-token-cost))))))
90+
8291
(defn prompt
8392
[{:keys [message model behavior contexts chat-id request-id]}
8493
db*
@@ -119,7 +128,10 @@
119128
received-msgs* (atom "")
120129
tool-call-args-by-id* (atom {})
121130
add-to-history! (fn [msg]
122-
(swap! db* update-in [:chats chat-id :messages] (fnil conj []) msg))]
131+
(swap! db* update-in [:chats chat-id :messages] (fnil conj []) msg))
132+
sum-sesison-tokens! (fn [input-tokens output-tokens]
133+
(swap! db* update-in [:chats chat-id :total-input-tokens] (fnil + 0) input-tokens)
134+
(swap! db* update-in [:chats chat-id :total-output-tokens] (fnil + 0) output-tokens))]
123135
(messenger/chat-content-received
124136
messenger
125137
{:chat-id chat-id
@@ -178,6 +190,23 @@
178190
(finish-chat-prompt! chat-id :idle messenger db*))
179191
:finish (do
180192
(add-to-history! {:role "assistant" :content @received-msgs*})
193+
(when-let [{:keys [output-tokens input-tokens]} (:usage msg)]
194+
(when (and output-tokens input-tokens)
195+
(sum-sesison-tokens! input-tokens output-tokens)
196+
(let [db @db*
197+
total-input-tokens (get-in db [:chats chat-id :total-input-tokens] 0)
198+
total-output-tokens (get-in db [:chats chat-id :total-output-tokens] 0)]
199+
(messenger/chat-content-received
200+
messenger
201+
{:chat-id chat-id
202+
:request-id request-id
203+
:role :system
204+
:content (assoc-some {:type :usage
205+
:message-output-tokens output-tokens
206+
:message-input-tokens input-tokens
207+
:session-tokens (+ total-input-tokens total-output-tokens)}
208+
:message-cost (tokens->cost input-tokens output-tokens chosen-model db)
209+
:session-cost (tokens->cost total-input-tokens total-output-tokens chosen-model db))}))))
181210
(finish-chat-prompt! chat-id :idle messenger db*))))
182211
:on-prepare-tool-call (fn [{:keys [id name arguments-text]}]
183212
(assert-chat-not-stopped! chat-id db* messenger)

src/eca/llm_providers/anthropic.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@
138138
:on-error on-error
139139
:on-response handle-response}))))
140140
"end_turn" (on-message-received {:type :finish
141+
:usage {:input-tokens (-> data :usage :input_tokens)
142+
:output-tokens (-> data :usage :output_tokens)}
141143
:finish-reason (-> data :delta :stop_reason)})
142144
"max_tokens" (on-message-received {:type :limit-reached
143145
:tokens (:usage data)})

src/eca/llm_providers/openai.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@
127127
"response.completed"
128128
(when-not (= "function_call" (-> data :response :output last :type))
129129
(on-message-received {:type :finish
130+
:usage {:input-tokens (-> data :response :usage :input_tokens)
131+
:output-tokens (-> data :response :usage :output_tokens)}
130132
:finish-reason (-> data :response :status)}))
131133
nil))]
132134
(base-completion-request!

src/eca/shared.clj

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,21 @@
2020

2121
(defn deep-merge [v & vs]
2222
(letfn [(rec-merge [v1 v2]
23-
(if (and (map? v1) (map? v2))
24-
(merge-with deep-merge v1 v2)
25-
v2))]
23+
(if (and (map? v1) (map? v2))
24+
(merge-with deep-merge v1 v2)
25+
v2))]
2626
(when (some identity vs)
2727
(reduce #(rec-merge %1 %2) v vs))))
28+
29+
(defn assoc-some
30+
"Assoc[iate] if the value is not nil. "
31+
([m k v]
32+
(if (nil? v) m (assoc m k v)))
33+
([m k v & kvs]
34+
(let [ret (assoc-some m k v)]
35+
(if kvs
36+
(if (next kvs)
37+
(recur ret (first kvs) (second kvs) (nnext kvs))
38+
(throw (IllegalArgumentException.
39+
"assoc-some expects even number of arguments after map/vector, found odd number")))
40+
ret))))

0 commit comments

Comments
 (0)