Skip to content

Commit c39c7d6

Browse files
committed
Pad first system message with newline
1 parent 3f6f75a commit c39c7d6

File tree

2 files changed

+76
-7
lines changed

2 files changed

+76
-7
lines changed

src/index.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,30 @@ let encoder: Tiktoken | undefined;
1515
* @returns An estimate for the number of tokens the prompt will use
1616
*/
1717
export function promptTokensEstimate({ messages, functions }: { messages: Message[], functions?: Function[] }): number {
18-
let tokens = messages.map(messageTokensEstimate).reduce((a, b) => a + b, 0);
19-
tokens += 3; // Add three per completion
18+
// It appears that if functions are present, the first system message is padded with a trailing newline. This
19+
// was inferred by trying lots of combinations of messages and functions and seeing what the token counts were.
20+
let paddedSystem = false;
21+
let tokens = messages.map(m => {
22+
if (m.role === "system" && functions && !paddedSystem) {
23+
m = { ...m, content: m.content + "\n" }
24+
paddedSystem = true;
25+
}
26+
return messageTokensEstimate(m);
27+
}).reduce((a, b) => a + b, 0);
28+
29+
// Each completion (vs message) seems to carry a 3-token overhead
30+
tokens += 3;
31+
32+
// If there are functions, add the function definitions as they count towards token usage
2033
if (functions) {
2134
tokens += functionsTokensEstimate(functions as any as FunctionDef[]);
2235
}
2336

24-
// If there's a system message _and_ functions are present, subtract three tokens
37+
// If there's a system message _and_ functions are present, subtract four tokens. I assume this is because
38+
// functions typically add a system message, but reuse the first one if it's already there. This offsets
39+
// the extra 9 tokens added by the function definitions.
2540
if (functions && messages.find(m => m.role === "system")) {
26-
tokens -= 3;
41+
tokens -= 4;
2742
}
2843

2944
return tokens;

tests/token-counts.test.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@ type Example = {
1010
validate?: boolean
1111
};
1212

13+
const r: OpenAI.Chat.CompletionCreateParams.CreateChatCompletionRequestNonStreaming = {
14+
"model": "gpt-3.5-turbo",
15+
"temperature": 0,
16+
"functions": [
17+
{
18+
"name": "do_stuff",
19+
"parameters": {
20+
"type": "object",
21+
"properties": {}
22+
}
23+
}
24+
],
25+
"messages": [
26+
{
27+
"role": "system",
28+
"content": "hello:"
29+
},
30+
]
31+
};
32+
1333
const TEST_CASES: Example[] = [
1434
{
1535
messages: [
@@ -23,6 +43,18 @@ const TEST_CASES: Example[] = [
2343
],
2444
tokens: 9
2545
},
46+
{
47+
messages: [
48+
{ role: "system", content: "hello" }
49+
],
50+
tokens: 8,
51+
},
52+
{
53+
messages: [
54+
{ role: "system", content: "hello:" }
55+
],
56+
tokens: 9,
57+
},
2658
{
2759
messages: [
2860
{ role: "system", content: "# Important: you're the best robot" },
@@ -161,10 +193,32 @@ const TEST_CASES: Example[] = [
161193
}
162194
],
163195
tokens: 35,
164-
}
196+
},
197+
{
198+
messages: [
199+
{ "role": "system", "content": "Hello:" },
200+
{ "role": "user", "content": "Hi there" },
201+
],
202+
functions: [
203+
{ "name": "do_stuff", "parameters": { "type": "object", "properties": {} } }
204+
],
205+
tokens: 35,
206+
},
207+
{
208+
messages: [
209+
{ "role": "system", "content": "Hello:" },
210+
{ "role": "system", "content": "Hello" },
211+
{ "role": "user", "content": "Hi there" },
212+
],
213+
functions: [
214+
{ "name": "do_stuff", "parameters": { "type": "object", "properties": {} } }
215+
],
216+
tokens: 40,
217+
},
165218
];
166219

167220
const validateAll = false;
221+
const openAITimeout = 10000;
168222

169223
describe.each(TEST_CASES)("token counts (%j)", (example) => {
170224
const validateTest = ((validateAll || example.validate) ? test : test.skip)
@@ -174,10 +228,10 @@ describe.each(TEST_CASES)("token counts (%j)", (example) => {
174228
model: "gpt-3.5-turbo",
175229
messages: example.messages,
176230
functions: example.functions as any,
177-
max_tokens: 1,
231+
max_tokens: 10,
178232
});
179233
expect(response.usage?.prompt_tokens).toBe(example.tokens);
180-
});
234+
}, openAITimeout);
181235

182236
test("estimate is correct", async () => {
183237
expect(promptTokensEstimate({ messages: example.messages, functions: example.functions })).toBe(example.tokens);

0 commit comments

Comments
 (0)