Skip to content

Commit 0fd1858

Browse files
committed
gpt4o mini support
1 parent d79be12 commit 0fd1858

File tree

10 files changed

+106
-20
lines changed

10 files changed

+106
-20
lines changed

CHANGELOG.md

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

33
All notable changes to this project will be documented in this file.
44

5+
## [0.1.9] - Aug 7, 2024
6+
7+
- Add gpt-4o-mini support, by adding a 33.3x multiplier to the token cost.
8+
59
## [0.1.8] - Aug 3, 2024
610

711
- Fix the type for the tool_choice param to be inclusive of "auto" and other options.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "openai-messages-token-helper"
33
description = "A helper library for estimating tokens used by messages sent through OpenAI Chat Completions API."
4-
version = "0.1.8"
4+
version = "0.1.9"
55
authors = [{name = "Pamela Fox"}]
66
requires-python = ">=3.9"
77
readme = "README.md"

src/openai_messages_token_helper/images_helper.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import base64
22
import math
33
import re
4+
from fractions import Fraction
45
from io import BytesIO
56

67
from PIL import Image
@@ -16,20 +17,23 @@ def get_image_dims(image_uri: str) -> tuple[int, int]:
1617
raise ValueError("Image must be a base64 string.")
1718

1819

19-
def count_tokens_for_image(image_uri: str, detail: str = "auto") -> int:
20+
def count_tokens_for_image(image_uri: str, detail: str = "auto", model: str = None) -> int:
2021
# From https://github.com/openai/openai-cookbook/pull/881/files
2122
# Based on https://platform.openai.com/docs/guides/vision
22-
LOW_DETAIL_COST = 85
23-
HIGH_DETAIL_COST_PER_TILE = 170
24-
ADDITIONAL_COST = 85
23+
multiplier = 1
24+
if model == "gpt-4o-mini":
25+
multiplier = 33 + Fraction(1, 3)
26+
COST_PER_TILE = 85 * multiplier
27+
LOW_DETAIL_COST = COST_PER_TILE
28+
HIGH_DETAIL_COST_PER_TILE = COST_PER_TILE * 2
2529

2630
if detail == "auto":
2731
# assume high detail for now
2832
detail = "high"
2933

3034
if detail == "low":
3135
# Low detail images have a fixed cost
32-
return LOW_DETAIL_COST
36+
return int(LOW_DETAIL_COST)
3337
elif detail == "high":
3438
# Calculate token cost for high detail images
3539
width, height = get_image_dims(image_uri)
@@ -47,8 +51,8 @@ def count_tokens_for_image(image_uri: str, detail: str = "auto") -> int:
4751
# Calculate the number of 512px squares
4852
num_squares = math.ceil(width / 512) * math.ceil(height / 512)
4953
# Calculate the total token cost
50-
total_cost = num_squares * HIGH_DETAIL_COST_PER_TILE + ADDITIONAL_COST
51-
return total_cost
54+
total_cost = num_squares * HIGH_DETAIL_COST_PER_TILE + COST_PER_TILE
55+
return math.ceil(total_cost)
5256
else:
5357
# Invalid detail_option
5458
raise ValueError("Invalid value for detail parameter. Use 'low' or 'high'.")

src/openai_messages_token_helper/model_helper.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"gpt-4-32k": 32000,
2323
"gpt-4v": 128000,
2424
"gpt-4o": 128000,
25+
"gpt-4o-mini": 128000,
2526
}
2627

2728

@@ -106,7 +107,7 @@ def count_tokens_for_message(model: str, message: ChatCompletionMessageParam, de
106107
if item["type"] == "text":
107108
num_tokens += len(encoding.encode(item["text"]))
108109
elif item["type"] == "image_url":
109-
num_tokens += count_tokens_for_image(item["image_url"]["url"], item["image_url"]["detail"])
110+
num_tokens += count_tokens_for_image(item["image_url"]["url"], item["image_url"]["detail"], model)
110111
elif isinstance(value, str):
111112
num_tokens += len(encoding.encode(value))
112113
else:

tests/image_messages.py

Lines changed: 55 additions & 0 deletions
Large diffs are not rendered by default.

tests/messages.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,5 +146,4 @@
146146
assistant_message_perf,
147147
assistant_message_perf_short,
148148
assistant_message_dresscode,
149-
text_and_image_message,
150149
]

tests/test_imageshelper.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def large_image():
1818

1919
def test_count_tokens_for_image(small_image, large_image):
2020
assert count_tokens_for_image(small_image, "low") == 85
21+
assert count_tokens_for_image(small_image, "low", "gpt-4o-mini") == 2833
2122
assert count_tokens_for_image(small_image, "high") == 255
2223
assert count_tokens_for_image(small_image) == 255
2324
assert count_tokens_for_image(large_image, "low") == 85

tests/test_messagebuilder.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
from openai_messages_token_helper import build_messages, count_tokens_for_message
1010

1111
from .functions import search_sources_toolchoice_auto
12+
from .image_messages import text_and_tiny_image_message
1213
from .messages import (
1314
assistant_message_dresscode,
1415
assistant_message_perf,
1516
assistant_message_perf_short,
1617
system_message_long,
1718
system_message_short,
1819
system_message_unicode,
19-
text_and_image_message,
2020
user_message,
2121
user_message_dresscode,
2222
user_message_perf,
@@ -35,9 +35,9 @@ def test_messagebuilder_imagemessage():
3535
messages = build_messages(
3636
"gpt-35-turbo",
3737
system_message_short["message"]["content"],
38-
new_user_content=text_and_image_message["message"]["content"],
38+
new_user_content=text_and_tiny_image_message["message"]["content"],
3939
)
40-
assert messages == [system_message_short["message"], text_and_image_message["message"]]
40+
assert messages == [system_message_short["message"], text_and_tiny_image_message["message"]]
4141

4242

4343
def test_messagebuilder_append():

tests/test_modelhelper.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from openai_messages_token_helper import count_tokens_for_message, count_tokens_for_system_and_tools, get_token_limit
33

44
from .functions import FUNCTION_COUNTS, search_sources_toolchoice_auto
5-
from .messages import system_message, system_message_with_name, text_and_image_message, user_message
5+
from .image_messages import IMAGE_MESSAGE_COUNTS
6+
from .messages import system_message, system_message_with_name, user_message
67

78

89
def test_get_token_limit():
@@ -56,11 +57,13 @@ def test_count_tokens_for_message(model, count_key, message):
5657
"model, count_key",
5758
[
5859
("gpt-4", "count"),
59-
("gpt-4o", "count_omni"),
60+
("gpt-4o", "count"),
61+
("gpt-4o-mini", "count_4o_mini"),
6062
],
6163
)
6264
def test_count_tokens_for_message_list(model, count_key):
63-
assert count_tokens_for_message(model, text_and_image_message["message"]) == text_and_image_message[count_key]
65+
for message_count_pair in IMAGE_MESSAGE_COUNTS:
66+
assert count_tokens_for_message(model, message_count_pair["message"]) == message_count_pair[count_key]
6467

6568

6669
def test_count_tokens_for_message_error():

tests/verify_openai.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import azure.identity
55
import openai
66
from dotenv import load_dotenv
7-
from messages import MESSAGE_COUNTS # type: ignore[import-not-found]
7+
from image_messages import IMAGE_MESSAGE_COUNTS
8+
from messages import MESSAGE_COUNTS
89

910
# Setup the OpenAI client to use either Azure OpenAI or OpenAI API
1011
load_dotenv()
@@ -38,10 +39,29 @@
3839
MODEL_NAME = openai_model
3940

4041
# Test the token count for each message
42+
4143
for message_count_pair in MESSAGE_COUNTS:
44+
for model, expected_tokens in [("gpt-4o", message_count_pair["count_omni"])]:
45+
message = message_count_pair["message"]
46+
expected_tokens = message_count_pair["count"]
47+
response = client.chat.completions.create(
48+
model=MODEL_NAME,
49+
temperature=0.7,
50+
n=1,
51+
messages=[message], # type: ignore[list-item]
52+
)
53+
54+
print(message)
55+
assert response.usage is not None, "Expected usage to be present"
56+
assert (
57+
response.usage.prompt_tokens == expected_tokens
58+
), f"Expected {expected_tokens} tokens, got {response.usage.prompt_tokens} for model {MODEL_NAME}"
59+
60+
61+
for message_count_pair in IMAGE_MESSAGE_COUNTS:
4262
for model, expected_tokens in [
43-
(MODEL_NAME, message_count_pair["count"]),
44-
("gpt-4o", message_count_pair["count_omni"]),
63+
("gpt-4o", message_count_pair["count"]),
64+
("gpt-4o-mini", message_count_pair["count_4o_mini"]),
4565
]:
4666
response = client.chat.completions.create(
4767
model=model,
@@ -50,7 +70,6 @@
5070
messages=[message_count_pair["message"]], # type: ignore[list-item]
5171
)
5272

53-
print(message_count_pair["message"])
5473
assert response.usage is not None, "Expected usage to be present"
5574
assert (
5675
response.usage.prompt_tokens == expected_tokens

0 commit comments

Comments
 (0)