@lobehub/chat 1.68.9 → 1.68.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/docs/usage/providers/ppio.mdx +5 -5
- package/docs/usage/providers/ppio.zh-CN.mdx +7 -7
- package/locales/ar/chat.json +5 -1
- package/locales/ar/models.json +6 -9
- package/locales/bg-BG/chat.json +5 -1
- package/locales/bg-BG/models.json +6 -9
- package/locales/de-DE/chat.json +5 -1
- package/locales/de-DE/models.json +6 -9
- package/locales/en-US/chat.json +5 -1
- package/locales/en-US/models.json +6 -9
- package/locales/es-ES/chat.json +5 -1
- package/locales/es-ES/models.json +6 -9
- package/locales/fa-IR/chat.json +5 -1
- package/locales/fa-IR/models.json +6 -9
- package/locales/fr-FR/chat.json +5 -1
- package/locales/fr-FR/models.json +6 -9
- package/locales/it-IT/chat.json +5 -1
- package/locales/it-IT/models.json +6 -9
- package/locales/ja-JP/chat.json +5 -1
- package/locales/ja-JP/models.json +6 -9
- package/locales/ko-KR/chat.json +5 -1
- package/locales/ko-KR/models.json +6 -9
- package/locales/nl-NL/chat.json +5 -1
- package/locales/nl-NL/models.json +6 -9
- package/locales/pl-PL/chat.json +5 -1
- package/locales/pl-PL/models.json +6 -9
- package/locales/pt-BR/chat.json +5 -1
- package/locales/pt-BR/models.json +6 -9
- package/locales/ru-RU/chat.json +5 -1
- package/locales/ru-RU/models.json +6 -9
- package/locales/tr-TR/chat.json +5 -1
- package/locales/tr-TR/models.json +6 -9
- package/locales/vi-VN/chat.json +5 -1
- package/locales/vi-VN/models.json +6 -9
- package/locales/zh-CN/chat.json +5 -1
- package/locales/zh-CN/models.json +6 -9
- package/locales/zh-TW/chat.json +5 -1
- package/locales/zh-TW/models.json +6 -9
- package/package.json +1 -1
- package/src/config/aiModels/perplexity.ts +36 -20
- package/src/config/modelProviders/ppio.ts +1 -1
- package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +27 -9
- package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +77 -35
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +253 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +65 -46
- package/src/libs/agent-runtime/baichuan/index.test.ts +58 -1
- package/src/libs/agent-runtime/groq/index.test.ts +36 -284
- package/src/libs/agent-runtime/mistral/index.test.ts +39 -300
- package/src/libs/agent-runtime/perplexity/index.test.ts +12 -10
- package/src/libs/agent-runtime/providerTestUtils.ts +58 -0
- package/src/libs/agent-runtime/togetherai/index.test.ts +7 -295
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +3 -0
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +5 -2
- package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +89 -5
- package/src/libs/agent-runtime/utils/streams/anthropic.ts +25 -8
- package/src/libs/agent-runtime/utils/streams/openai.test.ts +188 -84
- package/src/libs/agent-runtime/utils/streams/openai.ts +8 -17
- package/src/libs/agent-runtime/utils/usageConverter.test.ts +249 -0
- package/src/libs/agent-runtime/utils/usageConverter.ts +50 -0
- package/src/libs/agent-runtime/zeroone/index.test.ts +7 -294
- package/src/locales/default/chat.ts +4 -0
- package/src/types/message/base.ts +14 -4
- package/src/utils/filter.test.ts +0 -122
- package/src/utils/filter.ts +0 -29
|
@@ -348,94 +348,198 @@ describe('OpenAIStream', () => {
|
|
|
348
348
|
]);
|
|
349
349
|
});
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
351
|
+
describe('token usage', () => {
|
|
352
|
+
it('should streaming token usage', async () => {
|
|
353
|
+
const data = [
|
|
354
|
+
{
|
|
355
|
+
id: 'chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
|
|
356
|
+
object: 'chat.completion.chunk',
|
|
357
|
+
created: 1741056525,
|
|
358
|
+
model: 'gpt-4o-mini-2024-07-18',
|
|
359
|
+
choices: [{ index: 0, delta: { role: 'assistant', content: '' } }],
|
|
360
|
+
service_tier: 'default',
|
|
361
|
+
system_fingerprint: 'fp_06737a9306',
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: 'chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
|
|
365
|
+
object: 'chat.completion.chunk',
|
|
366
|
+
created: 1741056525,
|
|
367
|
+
model: 'gpt-4o-mini-2024-07-18',
|
|
368
|
+
choices: [{ index: 0, delta: { content: '你好!' } }],
|
|
369
|
+
service_tier: 'default',
|
|
370
|
+
system_fingerprint: 'fp_06737a9306',
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
id: 'chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
|
|
374
|
+
object: 'chat.completion.chunk',
|
|
375
|
+
created: 1741056525,
|
|
376
|
+
model: 'gpt-4o-mini-2024-07-18',
|
|
377
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
378
|
+
service_tier: 'default',
|
|
379
|
+
system_fingerprint: 'fp_06737a9306',
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: 'chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
|
|
383
|
+
object: 'chat.completion.chunk',
|
|
384
|
+
created: 1741056525,
|
|
385
|
+
model: 'gpt-4o-mini-2024-07-18',
|
|
386
|
+
choices: [],
|
|
387
|
+
service_tier: 'default',
|
|
388
|
+
system_fingerprint: 'fp_06737a9306',
|
|
389
|
+
usage: {
|
|
390
|
+
prompt_tokens: 1646,
|
|
391
|
+
completion_tokens: 11,
|
|
392
|
+
total_tokens: 1657,
|
|
393
|
+
prompt_tokens_details: { audio_tokens: 0, cached_tokens: 0 },
|
|
394
|
+
completion_tokens_details: {
|
|
395
|
+
accepted_prediction_tokens: 0,
|
|
396
|
+
audio_tokens: 0,
|
|
397
|
+
reasoning_tokens: 0,
|
|
398
|
+
rejected_prediction_tokens: 0,
|
|
399
|
+
},
|
|
398
400
|
},
|
|
399
401
|
},
|
|
400
|
-
|
|
401
|
-
];
|
|
402
|
+
];
|
|
402
403
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
404
|
+
const mockOpenAIStream = new ReadableStream({
|
|
405
|
+
start(controller) {
|
|
406
|
+
data.forEach((chunk) => {
|
|
407
|
+
controller.enqueue(chunk);
|
|
408
|
+
});
|
|
408
409
|
|
|
409
|
-
|
|
410
|
-
|
|
410
|
+
controller.close();
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const protocolStream = OpenAIStream(mockOpenAIStream);
|
|
415
|
+
|
|
416
|
+
const decoder = new TextDecoder();
|
|
417
|
+
const chunks = [];
|
|
418
|
+
|
|
419
|
+
// @ts-ignore
|
|
420
|
+
for await (const chunk of protocolStream) {
|
|
421
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
expect(chunks).toEqual(
|
|
425
|
+
[
|
|
426
|
+
'id: chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
|
|
427
|
+
'event: text',
|
|
428
|
+
`data: ""\n`,
|
|
429
|
+
'id: chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
|
|
430
|
+
'event: text',
|
|
431
|
+
`data: "你好!"\n`,
|
|
432
|
+
'id: chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
|
|
433
|
+
'event: stop',
|
|
434
|
+
`data: "stop"\n`,
|
|
435
|
+
'id: chatcmpl-B7CcnaeK3jqWBMOhxg7SSKFwlk7dC',
|
|
436
|
+
'event: usage',
|
|
437
|
+
`data: {"inputCacheMissTokens":1646,"inputTextTokens":1646,"outputTextTokens":11,"totalInputTokens":1646,"totalOutputTokens":11,"totalTokens":1657}\n`,
|
|
438
|
+
].map((i) => `${i}\n`),
|
|
439
|
+
);
|
|
411
440
|
});
|
|
412
441
|
|
|
413
|
-
|
|
442
|
+
it('should streaming litellm token usage', async () => {
|
|
443
|
+
const data = [
|
|
444
|
+
{
|
|
445
|
+
id: 'chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5',
|
|
446
|
+
created: 1741188058,
|
|
447
|
+
model: 'gpt-4o-mini',
|
|
448
|
+
object: 'chat.completion.chunk',
|
|
449
|
+
system_fingerprint: 'fp_06737a9306',
|
|
450
|
+
choices: [{ index: 0, delta: { content: ' #' } }],
|
|
451
|
+
stream_options: { include_usage: true },
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
id: 'chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5',
|
|
455
|
+
created: 1741188068,
|
|
456
|
+
model: 'gpt-4o-mini',
|
|
457
|
+
object: 'chat.completion.chunk',
|
|
458
|
+
system_fingerprint: 'fp_06737a9306',
|
|
459
|
+
choices: [{ index: 0, delta: { content: '.' } }],
|
|
460
|
+
stream_options: { include_usage: true },
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: 'chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5',
|
|
464
|
+
created: 1741188068,
|
|
465
|
+
model: 'gpt-4o-mini',
|
|
466
|
+
object: 'chat.completion.chunk',
|
|
467
|
+
system_fingerprint: 'fp_06737a9306',
|
|
468
|
+
choices: [{ finish_reason: 'stop', index: 0, delta: {} }],
|
|
469
|
+
stream_options: { include_usage: true },
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: 'chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5',
|
|
473
|
+
created: 1741188068,
|
|
474
|
+
model: 'gpt-4o-mini',
|
|
475
|
+
object: 'chat.completion.chunk',
|
|
476
|
+
system_fingerprint: 'fp_06737a9306',
|
|
477
|
+
choices: [{ index: 0, delta: {} }],
|
|
478
|
+
stream_options: { include_usage: true },
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
id: 'chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5',
|
|
482
|
+
created: 1741188068,
|
|
483
|
+
model: 'gpt-4o-mini',
|
|
484
|
+
object: 'chat.completion.chunk',
|
|
485
|
+
system_fingerprint: 'fp_06737a9306',
|
|
486
|
+
choices: [{ index: 0, delta: {} }],
|
|
487
|
+
stream_options: { include_usage: true },
|
|
488
|
+
usage: {
|
|
489
|
+
completion_tokens: 1720,
|
|
490
|
+
prompt_tokens: 1797,
|
|
491
|
+
total_tokens: 3517,
|
|
492
|
+
completion_tokens_details: {
|
|
493
|
+
accepted_prediction_tokens: 0,
|
|
494
|
+
audio_tokens: 0,
|
|
495
|
+
reasoning_tokens: 0,
|
|
496
|
+
rejected_prediction_tokens: 0,
|
|
497
|
+
},
|
|
498
|
+
prompt_tokens_details: { audio_tokens: 0, cached_tokens: 0 },
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
];
|
|
414
502
|
|
|
415
|
-
|
|
416
|
-
|
|
503
|
+
const mockOpenAIStream = new ReadableStream({
|
|
504
|
+
start(controller) {
|
|
505
|
+
data.forEach((chunk) => {
|
|
506
|
+
controller.enqueue(chunk);
|
|
507
|
+
});
|
|
417
508
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
509
|
+
controller.close();
|
|
510
|
+
},
|
|
511
|
+
});
|
|
422
512
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
513
|
+
const protocolStream = OpenAIStream(mockOpenAIStream);
|
|
514
|
+
|
|
515
|
+
const decoder = new TextDecoder();
|
|
516
|
+
const chunks = [];
|
|
517
|
+
|
|
518
|
+
// @ts-ignore
|
|
519
|
+
for await (const chunk of protocolStream) {
|
|
520
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
expect(chunks).toEqual(
|
|
524
|
+
[
|
|
525
|
+
'id: chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5',
|
|
526
|
+
'event: text',
|
|
527
|
+
`data: " #"\n`,
|
|
528
|
+
'id: chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5',
|
|
529
|
+
'event: text',
|
|
530
|
+
`data: "."\n`,
|
|
531
|
+
'id: chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5',
|
|
532
|
+
'event: stop',
|
|
533
|
+
`data: "stop"\n`,
|
|
534
|
+
'id: chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5',
|
|
535
|
+
'event: data',
|
|
536
|
+
`data: {"delta":{},"id":"chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5","index":0}\n`,
|
|
537
|
+
'id: chatcmpl-c1f6a6a6-fcf8-463a-96bf-cf634d3e98a5',
|
|
538
|
+
'event: usage',
|
|
539
|
+
`data: {"inputCacheMissTokens":1797,"inputTextTokens":1797,"outputTextTokens":1720,"totalInputTokens":1797,"totalOutputTokens":1720,"totalTokens":3517}\n`,
|
|
540
|
+
].map((i) => `${i}\n`),
|
|
541
|
+
);
|
|
542
|
+
});
|
|
439
543
|
});
|
|
440
544
|
|
|
441
545
|
describe('Tools Calling', () => {
|
|
@@ -840,7 +944,7 @@ describe('OpenAIStream', () => {
|
|
|
840
944
|
`data: "帮助。"\n`,
|
|
841
945
|
'id: 1',
|
|
842
946
|
'event: usage',
|
|
843
|
-
`data: {"
|
|
947
|
+
`data: {"inputCacheMissTokens":6,"inputTextTokens":6,"outputReasoningTokens":70,"outputTextTokens":34,"totalInputTokens":6,"totalOutputTokens":104,"totalTokens":110}\n`,
|
|
844
948
|
].map((i) => `${i}\n`),
|
|
845
949
|
);
|
|
846
950
|
});
|
|
@@ -1059,7 +1163,7 @@ describe('OpenAIStream', () => {
|
|
|
1059
1163
|
`data: "帮助。"\n`,
|
|
1060
1164
|
'id: 1',
|
|
1061
1165
|
'event: usage',
|
|
1062
|
-
`data: {"
|
|
1166
|
+
`data: {"inputCacheMissTokens":6,"inputTextTokens":6,"outputReasoningTokens":70,"outputTextTokens":34,"totalInputTokens":6,"totalOutputTokens":104,"totalTokens":110}\n`,
|
|
1063
1167
|
].map((i) => `${i}\n`),
|
|
1064
1168
|
);
|
|
1065
1169
|
});
|
|
@@ -1260,7 +1364,7 @@ describe('OpenAIStream', () => {
|
|
|
1260
1364
|
`data: "帮助。"\n`,
|
|
1261
1365
|
'id: 1',
|
|
1262
1366
|
'event: usage',
|
|
1263
|
-
`data: {"
|
|
1367
|
+
`data: {"inputCacheMissTokens":6,"inputTextTokens":6,"outputReasoningTokens":70,"outputTextTokens":34,"totalInputTokens":6,"totalOutputTokens":104,"totalTokens":110}\n`,
|
|
1264
1368
|
].map((i) => `${i}\n`),
|
|
1265
1369
|
);
|
|
1266
1370
|
});
|
|
@@ -1461,7 +1565,7 @@ describe('OpenAIStream', () => {
|
|
|
1461
1565
|
`data: "帮助。"\n`,
|
|
1462
1566
|
'id: 1',
|
|
1463
1567
|
'event: usage',
|
|
1464
|
-
`data: {"
|
|
1568
|
+
`data: {"inputCacheMissTokens":6,"inputTextTokens":6,"outputReasoningTokens":70,"outputTextTokens":34,"totalInputTokens":6,"totalOutputTokens":104,"totalTokens":110}\n`,
|
|
1465
1569
|
].map((i) => `${i}\n`),
|
|
1466
1570
|
);
|
|
1467
1571
|
});
|
|
@@ -1662,7 +1766,7 @@ describe('OpenAIStream', () => {
|
|
|
1662
1766
|
`data: "帮助。"\n`,
|
|
1663
1767
|
'id: 1',
|
|
1664
1768
|
'event: usage',
|
|
1665
|
-
`data: {"
|
|
1769
|
+
`data: {"inputCacheMissTokens":6,"inputTextTokens":6,"outputReasoningTokens":70,"outputTextTokens":34,"totalInputTokens":6,"totalOutputTokens":104,"totalTokens":110}\n`,
|
|
1666
1770
|
].map((i) => `${i}\n`),
|
|
1667
1771
|
);
|
|
1668
1772
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import type { Stream } from 'openai/streaming';
|
|
3
3
|
|
|
4
|
-
import { ChatMessageError, CitationItem
|
|
4
|
+
import { ChatMessageError, CitationItem } from '@/types/message';
|
|
5
5
|
|
|
6
6
|
import { AgentRuntimeErrorType, ILobeAgentRuntimeErrorType } from '../../error';
|
|
7
7
|
import { ChatStreamCallbacks } from '../../types';
|
|
8
|
+
import { convertUsage } from '../usageConverter';
|
|
8
9
|
import {
|
|
9
10
|
FIRST_CHUNK_ERROR_KEY,
|
|
10
11
|
StreamContext,
|
|
@@ -18,22 +19,6 @@ import {
|
|
|
18
19
|
generateToolCallId,
|
|
19
20
|
} from './protocol';
|
|
20
21
|
|
|
21
|
-
const convertUsage = (usage: OpenAI.Completions.CompletionUsage): ModelTokensUsage => {
|
|
22
|
-
return {
|
|
23
|
-
acceptedPredictionTokens: usage.completion_tokens_details?.accepted_prediction_tokens,
|
|
24
|
-
cachedTokens:
|
|
25
|
-
(usage as any).prompt_cache_hit_tokens || usage.prompt_tokens_details?.cached_tokens,
|
|
26
|
-
inputAudioTokens: usage.prompt_tokens_details?.audio_tokens,
|
|
27
|
-
inputCacheMissTokens: (usage as any).prompt_cache_miss_tokens,
|
|
28
|
-
inputTokens: usage.prompt_tokens,
|
|
29
|
-
outputAudioTokens: usage.completion_tokens_details?.audio_tokens,
|
|
30
|
-
outputTokens: usage.completion_tokens,
|
|
31
|
-
reasoningTokens: usage.completion_tokens_details?.reasoning_tokens,
|
|
32
|
-
rejectedPredictionTokens: usage.completion_tokens_details?.rejected_prediction_tokens,
|
|
33
|
-
totalTokens: usage.total_tokens,
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
|
|
37
22
|
export const transformOpenAIStream = (
|
|
38
23
|
chunk: OpenAI.ChatCompletionChunk,
|
|
39
24
|
streamContext: StreamContext,
|
|
@@ -193,6 +178,12 @@ export const transformOpenAIStream = (
|
|
|
193
178
|
return { data: item.delta, id: chunk.id, type: 'data' };
|
|
194
179
|
}
|
|
195
180
|
|
|
181
|
+
// litellm 的返回结果中,存在 delta 为空,但是有 usage 的情况
|
|
182
|
+
if (chunk.usage) {
|
|
183
|
+
const usage = chunk.usage;
|
|
184
|
+
return { data: convertUsage(usage), id: chunk.id, type: 'usage' };
|
|
185
|
+
}
|
|
186
|
+
|
|
196
187
|
// 其余情况下,返回 delta 和 index
|
|
197
188
|
return {
|
|
198
189
|
data: { delta: item.delta, id: chunk.id, index: item.index },
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { convertUsage } from './usageConverter';
|
|
5
|
+
|
|
6
|
+
describe('convertUsage', () => {
|
|
7
|
+
it('should convert basic OpenAI usage data correctly', () => {
|
|
8
|
+
// Arrange
|
|
9
|
+
const openaiUsage: OpenAI.Completions.CompletionUsage = {
|
|
10
|
+
prompt_tokens: 100,
|
|
11
|
+
completion_tokens: 50,
|
|
12
|
+
total_tokens: 150,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Act
|
|
16
|
+
const result = convertUsage(openaiUsage);
|
|
17
|
+
|
|
18
|
+
// Assert
|
|
19
|
+
expect(result).toEqual({
|
|
20
|
+
inputTextTokens: 100,
|
|
21
|
+
totalInputTokens: 100,
|
|
22
|
+
totalOutputTokens: 50,
|
|
23
|
+
outputTextTokens: 50,
|
|
24
|
+
totalTokens: 150,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should handle PPLX citation tokens correctly', () => {
|
|
29
|
+
// Arrange
|
|
30
|
+
const pplxUsage = {
|
|
31
|
+
prompt_tokens: 80,
|
|
32
|
+
citation_tokens: 20,
|
|
33
|
+
completion_tokens: 50,
|
|
34
|
+
total_tokens: 150,
|
|
35
|
+
} as OpenAI.Completions.CompletionUsage;
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
const result = convertUsage(pplxUsage);
|
|
39
|
+
|
|
40
|
+
// Assert
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
inputTextTokens: 80,
|
|
43
|
+
inputCitationTokens: 20,
|
|
44
|
+
totalInputTokens: 100,
|
|
45
|
+
totalOutputTokens: 50,
|
|
46
|
+
outputTextTokens: 50,
|
|
47
|
+
totalTokens: 170, // 150 + 20 (citation tokens)
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle cached tokens correctly', () => {
|
|
52
|
+
// Arrange
|
|
53
|
+
const usageWithCache = {
|
|
54
|
+
prompt_tokens: 100,
|
|
55
|
+
prompt_cache_hit_tokens: 30,
|
|
56
|
+
prompt_cache_miss_tokens: 70,
|
|
57
|
+
completion_tokens: 50,
|
|
58
|
+
total_tokens: 150,
|
|
59
|
+
} as OpenAI.Completions.CompletionUsage;
|
|
60
|
+
|
|
61
|
+
// Act
|
|
62
|
+
const result = convertUsage(usageWithCache);
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect(result).toEqual({
|
|
66
|
+
inputTextTokens: 100,
|
|
67
|
+
inputCachedTokens: 30,
|
|
68
|
+
inputCacheMissTokens: 70,
|
|
69
|
+
totalInputTokens: 100,
|
|
70
|
+
totalOutputTokens: 50,
|
|
71
|
+
outputTextTokens: 50,
|
|
72
|
+
totalTokens: 150,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle cached tokens using prompt_tokens_details', () => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const usageWithTokenDetails = {
|
|
79
|
+
prompt_tokens: 100,
|
|
80
|
+
prompt_tokens_details: {
|
|
81
|
+
cached_tokens: 30,
|
|
82
|
+
},
|
|
83
|
+
completion_tokens: 50,
|
|
84
|
+
total_tokens: 150,
|
|
85
|
+
} as OpenAI.Completions.CompletionUsage;
|
|
86
|
+
|
|
87
|
+
// Act
|
|
88
|
+
const result = convertUsage(usageWithTokenDetails);
|
|
89
|
+
|
|
90
|
+
// Assert
|
|
91
|
+
expect(result).toEqual({
|
|
92
|
+
inputTextTokens: 100,
|
|
93
|
+
inputCachedTokens: 30,
|
|
94
|
+
inputCacheMissTokens: 70, // 100 - 30
|
|
95
|
+
totalInputTokens: 100,
|
|
96
|
+
totalOutputTokens: 50,
|
|
97
|
+
outputTextTokens: 50,
|
|
98
|
+
totalTokens: 150,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle audio tokens in input correctly', () => {
|
|
103
|
+
// Arrange
|
|
104
|
+
const usageWithAudioInput = {
|
|
105
|
+
prompt_tokens: 100,
|
|
106
|
+
prompt_tokens_details: {
|
|
107
|
+
audio_tokens: 20,
|
|
108
|
+
},
|
|
109
|
+
completion_tokens: 50,
|
|
110
|
+
total_tokens: 150,
|
|
111
|
+
} as OpenAI.Completions.CompletionUsage;
|
|
112
|
+
|
|
113
|
+
// Act
|
|
114
|
+
const result = convertUsage(usageWithAudioInput);
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
inputTextTokens: 100,
|
|
119
|
+
inputAudioTokens: 20,
|
|
120
|
+
totalInputTokens: 100,
|
|
121
|
+
totalOutputTokens: 50,
|
|
122
|
+
outputTextTokens: 50,
|
|
123
|
+
totalTokens: 150,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle detailed output tokens correctly', () => {
|
|
128
|
+
// Arrange
|
|
129
|
+
const usageWithOutputDetails = {
|
|
130
|
+
prompt_tokens: 100,
|
|
131
|
+
completion_tokens: 100,
|
|
132
|
+
completion_tokens_details: {
|
|
133
|
+
reasoning_tokens: 30,
|
|
134
|
+
audio_tokens: 20,
|
|
135
|
+
},
|
|
136
|
+
total_tokens: 200,
|
|
137
|
+
} as OpenAI.Completions.CompletionUsage;
|
|
138
|
+
|
|
139
|
+
// Act
|
|
140
|
+
const result = convertUsage(usageWithOutputDetails);
|
|
141
|
+
|
|
142
|
+
// Assert
|
|
143
|
+
expect(result).toEqual({
|
|
144
|
+
inputTextTokens: 100,
|
|
145
|
+
totalInputTokens: 100,
|
|
146
|
+
totalOutputTokens: 100,
|
|
147
|
+
outputReasoningTokens: 30,
|
|
148
|
+
outputAudioTokens: 20,
|
|
149
|
+
outputTextTokens: 50, // 100 - 30 - 20
|
|
150
|
+
totalTokens: 200,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should handle prediction tokens correctly', () => {
|
|
155
|
+
// Arrange
|
|
156
|
+
const usageWithPredictions = {
|
|
157
|
+
prompt_tokens: 100,
|
|
158
|
+
completion_tokens: 80,
|
|
159
|
+
completion_tokens_details: {
|
|
160
|
+
accepted_prediction_tokens: 30,
|
|
161
|
+
rejected_prediction_tokens: 10,
|
|
162
|
+
},
|
|
163
|
+
total_tokens: 180,
|
|
164
|
+
} as OpenAI.Completions.CompletionUsage;
|
|
165
|
+
|
|
166
|
+
// Act
|
|
167
|
+
const result = convertUsage(usageWithPredictions);
|
|
168
|
+
|
|
169
|
+
// Assert
|
|
170
|
+
expect(result).toEqual({
|
|
171
|
+
inputTextTokens: 100,
|
|
172
|
+
totalInputTokens: 100,
|
|
173
|
+
totalOutputTokens: 80,
|
|
174
|
+
outputTextTokens: 80,
|
|
175
|
+
acceptedPredictionTokens: 30,
|
|
176
|
+
rejectedPredictionTokens: 10,
|
|
177
|
+
totalTokens: 180,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle complex usage with all fields correctly', () => {
|
|
182
|
+
// Arrange
|
|
183
|
+
const complexUsage = {
|
|
184
|
+
prompt_tokens: 150,
|
|
185
|
+
prompt_tokens_details: {
|
|
186
|
+
audio_tokens: 50,
|
|
187
|
+
cached_tokens: 40,
|
|
188
|
+
},
|
|
189
|
+
citation_tokens: 30,
|
|
190
|
+
completion_tokens: 120,
|
|
191
|
+
completion_tokens_details: {
|
|
192
|
+
reasoning_tokens: 40,
|
|
193
|
+
audio_tokens: 30,
|
|
194
|
+
accepted_prediction_tokens: 20,
|
|
195
|
+
rejected_prediction_tokens: 5,
|
|
196
|
+
},
|
|
197
|
+
total_tokens: 300,
|
|
198
|
+
} as OpenAI.Completions.CompletionUsage;
|
|
199
|
+
|
|
200
|
+
// Act
|
|
201
|
+
const result = convertUsage(complexUsage);
|
|
202
|
+
|
|
203
|
+
// Assert
|
|
204
|
+
expect(result).toEqual({
|
|
205
|
+
inputTextTokens: 150,
|
|
206
|
+
inputAudioTokens: 50,
|
|
207
|
+
inputCachedTokens: 40,
|
|
208
|
+
inputCacheMissTokens: 140, // 180 - 40 (totalInputTokens - cachedTokens)
|
|
209
|
+
inputCitationTokens: 30,
|
|
210
|
+
totalInputTokens: 180, // 150 + 30
|
|
211
|
+
outputTextTokens: 50, // 120 - 40 - 30
|
|
212
|
+
outputReasoningTokens: 40,
|
|
213
|
+
outputAudioTokens: 30,
|
|
214
|
+
totalOutputTokens: 120,
|
|
215
|
+
acceptedPredictionTokens: 20,
|
|
216
|
+
rejectedPredictionTokens: 5,
|
|
217
|
+
totalTokens: 330, // 300 + 30 (citation_tokens)
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should omit zero or undefined values in the final output', () => {
|
|
222
|
+
// Arrange
|
|
223
|
+
const usageWithZeros = {
|
|
224
|
+
prompt_tokens: 100,
|
|
225
|
+
completion_tokens: 50,
|
|
226
|
+
total_tokens: 150,
|
|
227
|
+
completion_tokens_details: {
|
|
228
|
+
reasoning_tokens: 0,
|
|
229
|
+
audio_tokens: undefined,
|
|
230
|
+
},
|
|
231
|
+
} as OpenAI.Completions.CompletionUsage;
|
|
232
|
+
|
|
233
|
+
// Act
|
|
234
|
+
const result = convertUsage(usageWithZeros);
|
|
235
|
+
|
|
236
|
+
// Assert
|
|
237
|
+
expect(result).toEqual({
|
|
238
|
+
inputTextTokens: 100,
|
|
239
|
+
totalInputTokens: 100,
|
|
240
|
+
totalOutputTokens: 50,
|
|
241
|
+
outputTextTokens: 50,
|
|
242
|
+
totalTokens: 150,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// These should not be present in the result
|
|
246
|
+
expect(result).not.toHaveProperty('outputReasoningTokens');
|
|
247
|
+
expect(result).not.toHaveProperty('outputAudioTokens');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
|
|
3
|
+
import { ModelTokensUsage } from '@/types/message';
|
|
4
|
+
|
|
5
|
+
export const convertUsage = (usage: OpenAI.Completions.CompletionUsage): ModelTokensUsage => {
|
|
6
|
+
// 目前只有 pplx 才有 citation_tokens
|
|
7
|
+
const inputTextTokens = usage.prompt_tokens || 0;
|
|
8
|
+
const inputCitationTokens = (usage as any).citation_tokens || 0;
|
|
9
|
+
const totalInputTokens = inputCitationTokens + inputTextTokens;
|
|
10
|
+
|
|
11
|
+
const cachedTokens =
|
|
12
|
+
(usage as any).prompt_cache_hit_tokens || usage.prompt_tokens_details?.cached_tokens;
|
|
13
|
+
|
|
14
|
+
const inputCacheMissTokens =
|
|
15
|
+
(usage as any).prompt_cache_miss_tokens || totalInputTokens - cachedTokens;
|
|
16
|
+
|
|
17
|
+
const totalOutputTokens = usage.completion_tokens;
|
|
18
|
+
const outputReasoning = usage.completion_tokens_details?.reasoning_tokens || 0;
|
|
19
|
+
const outputAudioTokens = usage.completion_tokens_details?.audio_tokens || 0;
|
|
20
|
+
const outputTextTokens = totalOutputTokens - outputReasoning - outputAudioTokens;
|
|
21
|
+
|
|
22
|
+
const totalTokens = inputCitationTokens + usage.total_tokens;
|
|
23
|
+
|
|
24
|
+
const data = {
|
|
25
|
+
acceptedPredictionTokens: usage.completion_tokens_details?.accepted_prediction_tokens,
|
|
26
|
+
inputAudioTokens: usage.prompt_tokens_details?.audio_tokens,
|
|
27
|
+
inputCacheMissTokens: inputCacheMissTokens,
|
|
28
|
+
inputCachedTokens: cachedTokens,
|
|
29
|
+
inputCitationTokens: inputCitationTokens,
|
|
30
|
+
inputTextTokens: inputTextTokens,
|
|
31
|
+
outputAudioTokens: outputAudioTokens,
|
|
32
|
+
outputReasoningTokens: outputReasoning,
|
|
33
|
+
outputTextTokens: outputTextTokens,
|
|
34
|
+
rejectedPredictionTokens: usage.completion_tokens_details?.rejected_prediction_tokens,
|
|
35
|
+
totalInputTokens,
|
|
36
|
+
totalOutputTokens: totalOutputTokens,
|
|
37
|
+
totalTokens,
|
|
38
|
+
} satisfies ModelTokensUsage;
|
|
39
|
+
|
|
40
|
+
const finalData = {};
|
|
41
|
+
|
|
42
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
43
|
+
if (!!value) {
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
finalData[key] = value;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return finalData;
|
|
50
|
+
};
|