Skip to content

Commit b31a9ff

Browse files
committed
Compute token costs
Fixes #203
1 parent 64edafd commit b31a9ff

20 files changed

+297
-70
lines changed

.Rbuildignore

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ _cache/
1313
^CRAN-SUBMISSION$
1414
^[\.]?air\.toml$
1515
^\.vscode$
16+
^data-raw$

DESCRIPTION

+2
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,5 @@ Collate:
9797
'utils-merge.R'
9898
'utils.R'
9999
'zzz.R'
100+
Depends:
101+
R (>= 3.5)

NAMESPACE

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Generated by roxygen2: do not edit by hand
22

3+
S3method(format,ellmer_price)
34
S3method(print,Chat)
5+
S3method(print,ellmer_price)
46
export(Content)
57
export(ContentImage)
68
export(ContentImageInline)

R/chat.R

+54-12
Original file line numberDiff line numberDiff line change
@@ -119,32 +119,59 @@ Chat <- R6::R6Class(
119119
assistant_turns <- keep(turns, function(x) x@role == "assistant")
120120

121121
n <- length(assistant_turns)
122-
tokens <- t(vapply(
122+
tokens_acc <- t(vapply(
123123
assistant_turns,
124124
function(turn) turn@tokens,
125125
double(2)
126126
))
127+
128+
tokens <- tokens_acc
127129
if (n > 1) {
128130
# Compute just the new tokens
129131
tokens[-1, 1] <- tokens[seq(2, n), 1] -
130132
(tokens[seq(1, n - 1), 1] + tokens[seq(1, n - 1), 2])
131133
}
132134
# collapse into a single vector
133135
tokens_v <- c(t(tokens))
136+
tokens_acc_v <- c(t(tokens_acc))
134137

135138
tokens_df <- data.frame(
136139
role = rep(c("user", "assistant"), times = n),
137-
tokens = tokens_v
140+
tokens = tokens_v,
141+
tokens_total = tokens_acc_v
138142
)
139143

140144
if (include_system_prompt && private$has_system_prompt()) {
141145
# How do we compute this?
142-
tokens_df <- rbind(data.frame(role = "system", tokens = 0), tokens_df)
146+
tokens_df <- rbind(
147+
data.frame(role = "system", tokens = 0, tokens_total = 0),
148+
tokens_df
149+
)
143150
}
144151

145152
tokens_df
146153
},
147154

155+
#' @description The total price of all turns in this chat.
156+
get_price = function() {
157+
turns <- self$get_turns(include_system_prompt = FALSE)
158+
assistant_turns <- keep(turns, function(x) x@role == "assistant")
159+
160+
n <- length(assistant_turns)
161+
tokens <- t(vapply(
162+
assistant_turns,
163+
function(turn) turn@tokens,
164+
double(2)
165+
))
166+
167+
find_price(
168+
private$provider@name,
169+
private$provider@model,
170+
sum(tokens[, 1]),
171+
sum(tokens[, 2])
172+
)
173+
},
174+
148175
#' @description The last turn returned by the assistant.
149176
#' @param role Optionally, specify a role to find the last turn with
150177
#' for the role.
@@ -645,16 +672,31 @@ print.Chat <- function(x, ...) {
645672
turns <- x$get_turns(include_system_prompt = TRUE)
646673

647674
tokens <- x$tokens(include_system_prompt = TRUE)
648-
tokens_user <- sum(tokens$tokens[tokens$role == "user"])
649-
tokens_assistant <- sum(tokens$tokens[tokens$role == "assistant"])
650-
651-
cat(paste_c(
652-
"<Chat",
653-
c(" ", provider@name, "/", provider@model),
654-
c(" turns=", length(turns)),
655-
c(" tokens=", tokens_user, "/", tokens_assistant),
675+
676+
tokens_user <- sum(tokens$tokens_total[tokens$role == "user"])
677+
tokens_assistant <- sum(tokens$tokens_total[tokens$role == "assistant"])
678+
price <- find_price(
679+
provider@name,
680+
provider@model,
681+
tokens_user,
682+
tokens_assistant
683+
)
684+
685+
cat(
686+
paste_c(
687+
"<Chat",
688+
c(" ", provider@name, "/", provider@model),
689+
c(" turns=", length(turns)),
690+
c(
691+
" tokens=",
692+
tokens_user,
693+
"/",
694+
tokens_assistant,
695+
if (!is.na(price)) c("/", format(price))
696+
)
697+
),
656698
">\n"
657-
))
699+
)
658700

659701
for (i in seq_along(turns)) {
660702
turn <- turns[[i]]

R/provider-bedrock.R

+5-2
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,11 @@ method(value_turn, ProviderBedrock) <- function(
263263
}
264264
})
265265

266-
tokens <- c(result$usage$inputTokens, result$usage$outputTokens)
267-
tokens_log(provider, tokens)
266+
tokens <- tokens_log(
267+
provider,
268+
input = result$usage$inputTokens,
269+
output = result$usage$outputTokens
270+
)
268271

269272
Turn(result$output$message$role, contents, json = result, tokens = tokens)
270273
}

R/provider-claude.R

+10-2
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,11 @@ method(value_turn, ProviderClaude) <- function(
278278
}
279279
})
280280

281-
tokens <- c(result$usage$input_tokens, result$usage$output_tokens)
282-
tokens_log(provider, tokens)
281+
tokens <- tokens_log(
282+
provider,
283+
input = result$usage$input_tokens,
284+
output = result$usage$output_tokens
285+
)
283286

284287
Turn(result$role, contents, json = result, tokens = tokens)
285288
}
@@ -377,6 +380,11 @@ method(as_json, list(ProviderClaude, ToolDef)) <- function(provider, x) {
377380
)
378381
}
379382

383+
# Pricing ----------------------------------------------------------------------
384+
385+
method(standardise_model, ProviderClaude) <- function(provider, model) {
386+
gsub("-(latest|\\d{8})$", "", model)
387+
}
380388

381389
# Helpers ----------------------------------------------------------------
382390

R/provider-gemini.R

+4-4
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,11 @@ method(value_turn, ProviderGemini) <- function(
202202
})
203203
contents <- compact(contents)
204204
usage <- result$usageMetadata
205-
tokens <- c(
206-
usage$promptTokenCount %||% NA_integer_,
207-
usage$candidatesTokenCount %||% NA_integer_
205+
tokens <- tokens_log(
206+
provider,
207+
input = usage$promptTokenCount,
208+
output = usage$candidatesTokenCount
208209
)
209-
tokens_log(provider, tokens)
210210

211211
Turn("assistant", contents, json = result, tokens = tokens)
212212
}

R/provider-openai.R

+4-5
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,11 @@ method(value_turn, ProviderOpenAI) <- function(
237237
})
238238
content <- c(content, calls)
239239
}
240-
tokens <- c(
241-
result$usage$prompt_tokens %||% NA_integer_,
242-
result$usage$completion_tokens %||% NA_integer_
240+
tokens <- tokens_log(
241+
provider,
242+
input = result$usage$prompt_tokens,
243+
output = result$usage$completion_tokens
243244
)
244-
tokens_log(provider, tokens)
245-
246245
Turn(message$role, content, json = result, tokens = tokens)
247246
}
248247

R/provider-snowflake.R

+4-4
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@ method(value_turn, ProviderSnowflake) <- function(
142142
) {
143143
deltas <- compact(sapply(result$choices, function(x) x$delta$content))
144144
content <- list(as_content(paste(deltas, collapse = "")))
145-
tokens <- c(
146-
result$usage$prompt_tokens %||% NA_integer_,
147-
result$usage$completion_tokens %||% NA_integer_
145+
tokens <- tokens_log(
146+
provider,
147+
input = result$usage$prompt_tokens,
148+
output = result$usage$completion_tokens
148149
)
149-
tokens_log(provider, tokens)
150150
Turn(
151151
# Snowflake's response format seems to omit the role.
152152
"assistant",

R/provider.R

+14
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,17 @@ method(as_json, list(Provider, class_list)) <- function(provider, x) {
113113
method(as_json, list(Provider, ContentJson)) <- function(provider, x) {
114114
as_json(provider, ContentText("<structured data/>"))
115115
}
116+
117+
# Pricing ---------------------------------------------------------------------
118+
119+
standardise_model <- new_generic(
120+
"standardise_model",
121+
"provider",
122+
function(provider, model) {
123+
S7_dispatch()
124+
}
125+
)
126+
127+
method(standardise_model, Provider) <- function(provider, model) {
128+
model
129+
}

R/sysdata.rda

737 Bytes
Binary file not shown.

R/tokens.R

+67-27
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,92 @@
1-
tokens_log <- function(provider, tokens) {
2-
# TODO: probably should make this store in a data frame, but will tackle
3-
# when implementing token costs.
1+
on_load(
2+
the$tokens <- tokens_row(character(), character(), numeric(), numeric())
3+
)
44

5-
name <- paste0(provider@name, "/", provider@model)
5+
tokens_log <- function(provider, input = NULL, output = NULL) {
6+
input <- input %||% 0
7+
output <- output %||% 0
68

7-
if (is.null(the$tokens)) {
8-
the$tokens <- list()
9-
}
10-
if (is.null(the$tokens[[name]])) {
11-
the$tokens[[name]] <- c(0, 0)
9+
model <- standardise_model(provider, provider@model)
10+
11+
name <- function(provider, model) paste0(provider, "/", model)
12+
i <- tokens_match(provider@name, model, the$tokens$provider, the$tokens$model)
13+
14+
if (is.na(i)) {
15+
new_row <- tokens_row(provider@name, model, input, output)
16+
the$tokens <- rbind(the$tokens, new_row)
17+
} else {
18+
the$tokens$input[i] <- the$tokens$input[i] + input
19+
the$tokens$output[i] <- the$tokens$output[i] + output
1220
}
1321

14-
tokens[is.na(tokens)] <- 0
15-
the$tokens[[name]] <- the$tokens[[name]] + tokens
16-
invisible()
22+
# Returns value to be passed to Turn
23+
c(input, output)
24+
}
25+
26+
tokens_row <- function(provider, model, input, output) {
27+
data.frame(provider = provider, model = model, input = input, output = output)
1728
}
1829

30+
tokens_match <- function(
31+
provider_needle,
32+
model_needle,
33+
provider_haystack,
34+
model_haystack
35+
) {
36+
match(
37+
paste0(provider_needle, "/", model_needle),
38+
paste0(provider_haystack, "/", model_haystack)
39+
)
40+
}
41+
42+
1943
local_tokens <- function(frame = parent.frame()) {
2044
old <- the$tokens
21-
the$tokens <- NULL
45+
the$tokens <- tokens_row(character(), character(), numeric(), numeric())
2246

2347
defer(the$tokens <- old, env = frame)
2448
}
2549

26-
tokens_set <- function() {
27-
the$tokens <- NULL
28-
invisible()
29-
}
30-
3150
#' Report on token usage in the current session
3251
#'
3352
#' Call this function to find out the cumulative number of tokens that you
34-
#' have sent and recieved in the current session.
53+
#' have sent and recieved in the current session. The price will be shown
54+
#' if known.
3555
#'
3656
#' @export
3757
#' @return A data frame
3858
#' @examples
3959
#' token_usage()
4060
token_usage <- function() {
41-
if (is.null(the$tokens)) {
61+
if (nrow(the$tokens) == 0) {
4262
cli::cli_inform(c(x = "No recorded usage in this session"))
43-
return(invisible(
44-
data.frame(name = character(), input = numeric(), output = numeric())
45-
))
63+
return(invisible(the$tokens))
4664
}
4765

48-
rows <- map2(names(the$tokens), the$tokens, function(name, tokens) {
49-
data.frame(name = name, input = tokens[[1]], output = tokens[[2]])
50-
})
51-
do.call("rbind", rows)
66+
out <- the$tokens
67+
out$price <- find_price(out$provider, out$model, out$input, out$output)
68+
out
69+
}
70+
71+
# Pricing ----------------------------------------------------------------------
72+
73+
find_price <- function(provider, model, input, output) {
74+
idx <- tokens_match(provider, model, prices$provider, prices$model)
75+
76+
input_price <- input * prices$input[idx] / 1e6
77+
output_price <- output * prices$output[idx] / 1e6
78+
ellmer_price(input_price + output_price)
79+
}
80+
81+
ellmer_price <- function(x) {
82+
structure(x, class = c("ellmer_price", "numeric"))
83+
}
84+
#' @export
85+
format.ellmer_price <- function(x, ...) {
86+
paste0("$", format(unclass(x), nsmall = 2))
87+
}
88+
#' @export
89+
print.ellmer_price <- function(x, ...) {
90+
print(format(x), quote = FALSE)
91+
invisible(x)
5292
}

data-raw/openai.csv

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
model,cached_input,input,output
2+
gpt-4.5-preview,37.50,75.00,150.00
3+
gpt-4.5-preview-2025-02-27,37.50,75.00,150.00
4+
gpt-4o,1.25,2.50,10.00
5+
gpt-4o-2024-11-20,1.25,2.50,10.00
6+
gpt-4o-2024-08-06,1.25,2.50,10.00
7+
gpt-4o-2024-05-13,,5.00,15.00
8+
gpt-4o-mini,0.075,0.15,0.60
9+
gpt-4o-mini-2024-07-18,0.075,0.15,0.60
10+
o1,7.50,15.00,60.00
11+
o1-2024-12-17,7.50,15.00,60.00
12+
o1-preview-2024-09-12,7.50,15.00,60.00
13+
o1-pro,,150.00,600.00
14+
o1-pro-2025-03-19,,150.00,600.00
15+
o3-mini,0.55,1.10,4.40
16+
o3-mini-2025-01-31,0.55,1.10,4.40
17+
o1-mini,0.55,1.10,4.40
18+
o1-mini-2024-09-12,0.55,1.10,4.40
19+
gpt-4o-mini-search-preview,,0.15,0.60
20+
gpt-4o-mini-search-preview-2025-03-11,,0.15,0.60
21+
gpt-4o-search-preview,,2.50,10.00
22+
gpt-4o-search-preview-2025-03-11,,2.50,10.00
23+
computer-use-preview,,3.00,12.00
24+
computer-use-preview-2025-03-11,,3.00,12.00

0 commit comments

Comments
 (0)