Skip to content

Commit d186cca

Browse files
authored
feat (provider/openai-compatible): add additional token usage metrics (#5494)
1 parent 232d386 commit d186cca

File tree

3 files changed

+341
-31
lines changed

3 files changed

+341
-31
lines changed

.changeset/poor-olives-wait.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/openai-compatible': patch
3+
---
4+
5+
feat (provider/openai-compatible): add additional token usage metrics

packages/openai-compatible/src/openai-compatible-chat-language-model.test.ts

+211-7
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ describe('doGenerate', () => {
103103
prompt_tokens?: number;
104104
total_tokens?: number;
105105
completion_tokens?: number;
106+
prompt_tokens_details?: {
107+
cached_tokens?: number;
108+
};
109+
completion_tokens_details?: {
110+
reasoning_tokens?: number;
111+
accepted_prediction_tokens?: number;
112+
rejected_prediction_tokens?: number;
113+
};
106114
};
107115
finish_reason?: string;
108116
created?: number;
@@ -861,6 +869,86 @@ describe('doGenerate', () => {
861869
body: '{"model":"grok-beta","messages":[{"role":"user","content":"Hello"}]}',
862870
});
863871
});
872+
873+
describe('usage details', () => {
874+
it('should extract detailed token usage when available', async () => {
875+
prepareJsonResponse({
876+
content: '',
877+
usage: {
878+
prompt_tokens: 20,
879+
completion_tokens: 30,
880+
prompt_tokens_details: {
881+
cached_tokens: 5,
882+
},
883+
completion_tokens_details: {
884+
reasoning_tokens: 10,
885+
accepted_prediction_tokens: 15,
886+
rejected_prediction_tokens: 5,
887+
},
888+
},
889+
});
890+
891+
const result = await model.doGenerate({
892+
inputFormat: 'prompt',
893+
mode: { type: 'regular' },
894+
prompt: TEST_PROMPT,
895+
});
896+
897+
expect(result.providerMetadata!['test-provider']).toStrictEqual({
898+
cachedPromptTokens: 5,
899+
reasoningTokens: 10,
900+
acceptedPredictionTokens: 15,
901+
rejectedPredictionTokens: 5,
902+
});
903+
});
904+
905+
it('should handle missing token details', async () => {
906+
prepareJsonResponse({
907+
content: '',
908+
usage: {
909+
prompt_tokens: 20,
910+
completion_tokens: 30,
911+
// No token details provided
912+
},
913+
});
914+
915+
const result = await model.doGenerate({
916+
inputFormat: 'prompt',
917+
mode: { type: 'regular' },
918+
prompt: TEST_PROMPT,
919+
});
920+
921+
expect(result.providerMetadata!['test-provider']).toStrictEqual({});
922+
});
923+
924+
it('should handle partial token details', async () => {
925+
prepareJsonResponse({
926+
content: '',
927+
usage: {
928+
prompt_tokens: 20,
929+
completion_tokens: 30,
930+
prompt_tokens_details: {
931+
cached_tokens: 5,
932+
},
933+
completion_tokens_details: {
934+
// Only reasoning tokens provided
935+
reasoning_tokens: 10,
936+
},
937+
},
938+
});
939+
940+
const result = await model.doGenerate({
941+
inputFormat: 'prompt',
942+
mode: { type: 'regular' },
943+
prompt: TEST_PROMPT,
944+
});
945+
946+
expect(result.providerMetadata!['test-provider']).toStrictEqual({
947+
cachedPromptTokens: 5,
948+
reasoningTokens: 10,
949+
});
950+
});
951+
});
864952
});
865953

866954
describe('doStream', () => {
@@ -924,6 +1012,9 @@ describe('doStream', () => {
9241012
type: 'finish',
9251013
finishReason: 'stop',
9261014
usage: { promptTokens: 18, completionTokens: 439 },
1015+
providerMetadata: {
1016+
'test-provider': {},
1017+
},
9271018
},
9281019
]);
9291020
});
@@ -980,6 +1071,9 @@ describe('doStream', () => {
9801071
type: 'finish',
9811072
finishReason: 'stop',
9821073
usage: { promptTokens: 18, completionTokens: 439 },
1074+
providerMetadata: {
1075+
'test-provider': {},
1076+
},
9831077
},
9841078
]);
9851079
});
@@ -1109,6 +1203,9 @@ describe('doStream', () => {
11091203
type: 'finish',
11101204
finishReason: 'tool-calls',
11111205
usage: { promptTokens: 18, completionTokens: 439 },
1206+
providerMetadata: {
1207+
'test-provider': {},
1208+
},
11121209
},
11131210
]);
11141211
});
@@ -1245,6 +1342,9 @@ describe('doStream', () => {
12451342
type: 'finish',
12461343
finishReason: 'tool-calls',
12471344
usage: { promptTokens: 18, completionTokens: 439 },
1345+
providerMetadata: {
1346+
'test-provider': {},
1347+
},
12481348
},
12491349
]);
12501350
});
@@ -1370,6 +1470,9 @@ describe('doStream', () => {
13701470
type: 'finish',
13711471
finishReason: 'tool-calls',
13721472
usage: { promptTokens: 226, completionTokens: 20 },
1473+
providerMetadata: {
1474+
'test-provider': {},
1475+
},
13731476
},
13741477
]);
13751478
});
@@ -1436,6 +1539,9 @@ describe('doStream', () => {
14361539
type: 'finish',
14371540
finishReason: 'tool-calls',
14381541
usage: { promptTokens: 18, completionTokens: 439 },
1542+
providerMetadata: {
1543+
'test-provider': {},
1544+
},
14391545
},
14401546
]);
14411547
});
@@ -1468,6 +1574,9 @@ describe('doStream', () => {
14681574
promptTokens: NaN,
14691575
completionTokens: NaN,
14701576
},
1577+
providerMetadata: {
1578+
'test-provider': {},
1579+
},
14711580
},
14721581
]);
14731582
});
@@ -1495,6 +1604,9 @@ describe('doStream', () => {
14951604
completionTokens: NaN,
14961605
promptTokens: NaN,
14971606
},
1607+
providerMetadata: {
1608+
'test-provider': {},
1609+
},
14981610
});
14991611
});
15001612

@@ -1621,6 +1733,92 @@ describe('doStream', () => {
16211733
body: '{"model":"grok-beta","messages":[{"role":"user","content":"Hello"}],"stream":true}',
16221734
});
16231735
});
1736+
1737+
describe('usage details in streaming', () => {
1738+
it('should extract detailed token usage from stream finish', async () => {
1739+
server.urls['https://my.api.com/v1/chat/completions'].response = {
1740+
type: 'stream-chunks',
1741+
chunks: [
1742+
`data: {"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}\n\n`,
1743+
`data: {"choices":[{"delta":{},"finish_reason":"stop"}],` +
1744+
`"usage":{"prompt_tokens":20,"completion_tokens":30,` +
1745+
`"prompt_tokens_details":{"cached_tokens":5},` +
1746+
`"completion_tokens_details":{` +
1747+
`"reasoning_tokens":10,` +
1748+
`"accepted_prediction_tokens":15,` +
1749+
`"rejected_prediction_tokens":5}}}\n\n`,
1750+
'data: [DONE]\n\n',
1751+
],
1752+
};
1753+
1754+
const { stream } = await model.doStream({
1755+
inputFormat: 'prompt',
1756+
mode: { type: 'regular' },
1757+
prompt: TEST_PROMPT,
1758+
});
1759+
1760+
const parts = await convertReadableStreamToArray(stream);
1761+
const finishPart = parts.find(part => part.type === 'finish');
1762+
1763+
expect(finishPart?.providerMetadata!['test-provider']).toStrictEqual({
1764+
cachedPromptTokens: 5,
1765+
reasoningTokens: 10,
1766+
acceptedPredictionTokens: 15,
1767+
rejectedPredictionTokens: 5,
1768+
});
1769+
});
1770+
1771+
it('should handle missing token details in stream', async () => {
1772+
server.urls['https://my.api.com/v1/chat/completions'].response = {
1773+
type: 'stream-chunks',
1774+
chunks: [
1775+
`data: {"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}\n\n`,
1776+
`data: {"choices":[{"delta":{},"finish_reason":"stop"}],` +
1777+
`"usage":{"prompt_tokens":20,"completion_tokens":30}}\n\n`,
1778+
'data: [DONE]\n\n',
1779+
],
1780+
};
1781+
1782+
const { stream } = await model.doStream({
1783+
inputFormat: 'prompt',
1784+
mode: { type: 'regular' },
1785+
prompt: TEST_PROMPT,
1786+
});
1787+
1788+
const parts = await convertReadableStreamToArray(stream);
1789+
const finishPart = parts.find(part => part.type === 'finish');
1790+
1791+
expect(finishPart?.providerMetadata!['test-provider']).toStrictEqual({});
1792+
});
1793+
1794+
it('should handle partial token details in stream', async () => {
1795+
server.urls['https://my.api.com/v1/chat/completions'].response = {
1796+
type: 'stream-chunks',
1797+
chunks: [
1798+
`data: {"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}\n\n`,
1799+
`data: {"choices":[{"delta":{},"finish_reason":"stop"}],` +
1800+
`"usage":{"prompt_tokens":20,"completion_tokens":30,` +
1801+
`"prompt_tokens_details":{"cached_tokens":5},` +
1802+
`"completion_tokens_details":{"reasoning_tokens":10}}}\n\n`,
1803+
'data: [DONE]\n\n',
1804+
],
1805+
};
1806+
1807+
const { stream } = await model.doStream({
1808+
inputFormat: 'prompt',
1809+
mode: { type: 'regular' },
1810+
prompt: TEST_PROMPT,
1811+
});
1812+
1813+
const parts = await convertReadableStreamToArray(stream);
1814+
const finishPart = parts.find(part => part.type === 'finish');
1815+
1816+
expect(finishPart?.providerMetadata!['test-provider']).toStrictEqual({
1817+
cachedPromptTokens: 5,
1818+
reasoningTokens: 10,
1819+
});
1820+
});
1821+
});
16241822
});
16251823

16261824
describe('doStream simulated streaming', () => {
@@ -1709,7 +1907,9 @@ describe('doStream simulated streaming', () => {
17091907
finishReason: 'stop',
17101908
usage: { promptTokens: 4, completionTokens: 30 },
17111909
logprobs: undefined,
1712-
providerMetadata: undefined,
1910+
providerMetadata: {
1911+
'test-provider': {},
1912+
},
17131913
},
17141914
]);
17151915
});
@@ -1751,7 +1951,9 @@ describe('doStream simulated streaming', () => {
17511951
finishReason: 'stop',
17521952
usage: { promptTokens: 4, completionTokens: 30 },
17531953
logprobs: undefined,
1754-
providerMetadata: undefined,
1954+
providerMetadata: {
1955+
'test-provider': {},
1956+
},
17551957
},
17561958
]);
17571959
});
@@ -1815,7 +2017,9 @@ describe('doStream simulated streaming', () => {
18152017
finishReason: 'stop',
18162018
usage: { promptTokens: 4, completionTokens: 30 },
18172019
logprobs: undefined,
1818-
providerMetadata: undefined,
2020+
providerMetadata: {
2021+
'test-provider': {},
2022+
},
18192023
},
18202024
]);
18212025
});
@@ -1832,7 +2036,7 @@ describe('metadata extraction', () => {
18322036
return undefined;
18332037
}
18342038
return {
1835-
test: {
2039+
'test-provider': {
18362040
value: parsedBody.test_field as string,
18372041
},
18382042
};
@@ -1856,7 +2060,7 @@ describe('metadata extraction', () => {
18562060
buildMetadata: () =>
18572061
accumulatedValue
18582062
? {
1859-
test: {
2063+
'test-provider': {
18602064
value: accumulatedValue,
18612065
},
18622066
}
@@ -1905,7 +2109,7 @@ describe('metadata extraction', () => {
19052109
});
19062110

19072111
expect(result.providerMetadata).toEqual({
1908-
test: {
2112+
'test-provider': {
19092113
value: 'test_value',
19102114
},
19112115
});
@@ -1947,7 +2151,7 @@ describe('metadata extraction', () => {
19472151
const finishPart = parts.find(part => part.type === 'finish');
19482152

19492153
expect(finishPart?.providerMetadata).toEqual({
1950-
test: {
2154+
'test-provider': {
19512155
value: 'test_value',
19522156
},
19532157
});

0 commit comments

Comments
 (0)