@tanstack/openai-base 0.5.0 → 0.6.1
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/dist/esm/adapters/chat-completions-text.js +3 -10
- package/dist/esm/adapters/chat-completions-text.js.map +1 -1
- package/dist/esm/adapters/responses-text.js +6 -9
- package/dist/esm/adapters/responses-text.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/usage.d.ts +34 -0
- package/dist/esm/usage.js +83 -0
- package/dist/esm/usage.js.map +1 -0
- package/package.json +3 -3
- package/src/adapters/chat-completions-text.ts +3 -10
- package/src/adapters/responses-text.ts +7 -10
- package/src/index.ts +5 -0
- package/src/usage.ts +159 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { buildBaseUsage } from "@tanstack/ai";
|
|
2
|
+
function buildChatCompletionsUsage(usage) {
|
|
3
|
+
if (!usage) return void 0;
|
|
4
|
+
const result = buildBaseUsage({
|
|
5
|
+
promptTokens: usage.prompt_tokens || 0,
|
|
6
|
+
completionTokens: usage.completion_tokens || 0,
|
|
7
|
+
totalTokens: usage.total_tokens || 0
|
|
8
|
+
});
|
|
9
|
+
const completionDetails = usage.completion_tokens_details;
|
|
10
|
+
const completionTokensDetails = {
|
|
11
|
+
...completionDetails?.reasoning_tokens ? { reasoningTokens: completionDetails.reasoning_tokens } : {},
|
|
12
|
+
...completionDetails?.audio_tokens ? { audioTokens: completionDetails.audio_tokens } : {}
|
|
13
|
+
};
|
|
14
|
+
const promptDetails = usage.prompt_tokens_details;
|
|
15
|
+
const promptTokensDetails = {
|
|
16
|
+
...promptDetails?.cached_tokens ? { cachedTokens: promptDetails.cached_tokens } : {},
|
|
17
|
+
...promptDetails?.audio_tokens ? { audioTokens: promptDetails.audio_tokens } : {}
|
|
18
|
+
};
|
|
19
|
+
if (Object.keys(completionTokensDetails).length > 0) {
|
|
20
|
+
result.completionTokensDetails = completionTokensDetails;
|
|
21
|
+
}
|
|
22
|
+
if (Object.keys(promptTokensDetails).length > 0) {
|
|
23
|
+
result.promptTokensDetails = promptTokensDetails;
|
|
24
|
+
}
|
|
25
|
+
const providerUsageDetails = {
|
|
26
|
+
...completionDetails?.accepted_prediction_tokens ? {
|
|
27
|
+
acceptedPredictionTokens: completionDetails.accepted_prediction_tokens
|
|
28
|
+
} : {},
|
|
29
|
+
...completionDetails?.rejected_prediction_tokens ? {
|
|
30
|
+
rejectedPredictionTokens: completionDetails.rejected_prediction_tokens
|
|
31
|
+
} : {}
|
|
32
|
+
};
|
|
33
|
+
if (Object.keys(providerUsageDetails).length > 0) {
|
|
34
|
+
result.providerUsageDetails = providerUsageDetails;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
function buildResponsesUsage(usage) {
|
|
39
|
+
if (!usage) return void 0;
|
|
40
|
+
const result = buildBaseUsage({
|
|
41
|
+
promptTokens: usage.input_tokens || 0,
|
|
42
|
+
completionTokens: usage.output_tokens || 0,
|
|
43
|
+
totalTokens: usage.total_tokens || 0
|
|
44
|
+
});
|
|
45
|
+
const cachedTokens = usage.input_tokens_details?.cached_tokens;
|
|
46
|
+
if (cachedTokens && cachedTokens > 0) {
|
|
47
|
+
result.promptTokensDetails = {
|
|
48
|
+
...result.promptTokensDetails,
|
|
49
|
+
cachedTokens
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens;
|
|
53
|
+
if (reasoningTokens && reasoningTokens > 0) {
|
|
54
|
+
result.completionTokensDetails = {
|
|
55
|
+
...result.completionTokensDetails,
|
|
56
|
+
reasoningTokens
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function buildImagesUsage(usage) {
|
|
62
|
+
if (!usage) return void 0;
|
|
63
|
+
const result = buildBaseUsage({
|
|
64
|
+
promptTokens: usage.input_tokens || 0,
|
|
65
|
+
completionTokens: usage.output_tokens || 0,
|
|
66
|
+
totalTokens: usage.total_tokens || 0
|
|
67
|
+
});
|
|
68
|
+
const inputDetails = usage.input_tokens_details;
|
|
69
|
+
const promptTokensDetails = {
|
|
70
|
+
...inputDetails?.text_tokens ? { textTokens: inputDetails.text_tokens } : {},
|
|
71
|
+
...inputDetails?.image_tokens ? { imageTokens: inputDetails.image_tokens } : {}
|
|
72
|
+
};
|
|
73
|
+
if (Object.keys(promptTokensDetails).length > 0) {
|
|
74
|
+
result.promptTokensDetails = promptTokensDetails;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
buildChatCompletionsUsage,
|
|
80
|
+
buildImagesUsage,
|
|
81
|
+
buildResponsesUsage
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.js","sources":["../../src/usage.ts"],"sourcesContent":["import { buildBaseUsage } from '@tanstack/ai'\nimport type { TokenUsage } from '@tanstack/ai'\nimport type OpenAI from 'openai'\n\n/**\n * Build normalized {@link TokenUsage} from an OpenAI-compatible Chat\n * Completions `usage` object.\n *\n * Shared by every provider that routes through\n * {@link OpenAIBaseChatCompletionsTextAdapter} (OpenAI Chat Completions, Grok,\n * Groq). Surfaces cached prompt tokens and reasoning/audio detail tokens when\n * the provider reports them. Returns `undefined` when the provider reported no\n * usage object, so callers omit the field rather than fabricating zeroed totals.\n */\nexport function buildChatCompletionsUsage(\n usage: OpenAI.Chat.Completions.ChatCompletion['usage'] | undefined | null,\n): TokenUsage | undefined {\n if (!usage) return undefined\n\n const result = buildBaseUsage({\n promptTokens: usage.prompt_tokens || 0,\n completionTokens: usage.completion_tokens || 0,\n totalTokens: usage.total_tokens || 0,\n })\n\n const completionDetails = usage.completion_tokens_details\n const completionTokensDetails = {\n ...(completionDetails?.reasoning_tokens\n ? { reasoningTokens: completionDetails.reasoning_tokens }\n : {}),\n ...(completionDetails?.audio_tokens\n ? { audioTokens: completionDetails.audio_tokens }\n : {}),\n }\n\n const promptDetails = usage.prompt_tokens_details\n const promptTokensDetails = {\n ...(promptDetails?.cached_tokens\n ? { cachedTokens: promptDetails.cached_tokens }\n : {}),\n ...(promptDetails?.audio_tokens\n ? { audioTokens: promptDetails.audio_tokens }\n : {}),\n }\n\n if (Object.keys(completionTokensDetails).length > 0) {\n result.completionTokensDetails = completionTokensDetails\n }\n if (Object.keys(promptTokensDetails).length > 0) {\n result.promptTokensDetails = promptTokensDetails\n }\n\n // Predicted Outputs accepted/rejected counts have no canonical TokenUsage\n // slot but are still billed (rejected tokens included), so surface them under\n // providerUsageDetails — matching how the OpenRouter adapter exposes them.\n const providerUsageDetails = {\n ...(completionDetails?.accepted_prediction_tokens\n ? {\n acceptedPredictionTokens:\n completionDetails.accepted_prediction_tokens,\n }\n : {}),\n ...(completionDetails?.rejected_prediction_tokens\n ? {\n rejectedPredictionTokens:\n completionDetails.rejected_prediction_tokens,\n }\n : {}),\n }\n if (Object.keys(providerUsageDetails).length > 0) {\n result.providerUsageDetails = providerUsageDetails\n }\n\n return result\n}\n\n/**\n * Build normalized {@link TokenUsage} from an OpenAI Responses API\n * `ResponseUsage` object.\n *\n * Shared by every provider that routes through\n * {@link OpenAIBaseResponsesTextAdapter}. Surfaces cached prompt tokens and\n * reasoning detail tokens when present. Returns `undefined` when the provider\n * reported no usage object, so callers omit the field rather than fabricating\n * zeroed totals.\n */\nexport function buildResponsesUsage(\n usage: OpenAI.Responses.ResponseUsage | undefined | null,\n): TokenUsage | undefined {\n if (!usage) return undefined\n\n const result = buildBaseUsage({\n promptTokens: usage.input_tokens || 0,\n completionTokens: usage.output_tokens || 0,\n totalTokens: usage.total_tokens || 0,\n })\n\n // Despite the SDK types marking these required, they can be undefined at runtime.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const cachedTokens = usage.input_tokens_details?.cached_tokens\n if (cachedTokens && cachedTokens > 0) {\n result.promptTokensDetails = {\n ...result.promptTokensDetails,\n cachedTokens,\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const reasoningTokens = usage.output_tokens_details?.reasoning_tokens\n if (reasoningTokens && reasoningTokens > 0) {\n result.completionTokensDetails = {\n ...result.completionTokensDetails,\n reasoningTokens,\n }\n }\n\n return result\n}\n\n/**\n * Build normalized {@link TokenUsage} from an OpenAI Images API `usage` object.\n *\n * Shared by every provider that generates images through the OpenAI Images SDK\n * (OpenAI, Grok). Token-billed image models (e.g. gpt-image-1) report an input\n * breakdown of text vs image tokens, which is surfaced on `promptTokensDetails`.\n * Models that don't return usage (e.g. DALL·E) yield `undefined` so callers can\n * omit the field rather than emit zeroed totals.\n */\nexport function buildImagesUsage(\n usage: OpenAI.Images.ImagesResponse['usage'] | undefined | null,\n): TokenUsage | undefined {\n if (!usage) return undefined\n\n const result = buildBaseUsage({\n promptTokens: usage.input_tokens || 0,\n completionTokens: usage.output_tokens || 0,\n totalTokens: usage.total_tokens || 0,\n })\n\n // The SDK types input_tokens_details (and its numeric fields) as required, but\n // real responses — e.g. from DALL·E or other non-token-billed models — can\n // omit them, so treat the breakdown as optional.\n const inputDetails = usage.input_tokens_details as\n | { text_tokens?: number; image_tokens?: number }\n | undefined\n const promptTokensDetails = {\n ...(inputDetails?.text_tokens\n ? { textTokens: inputDetails.text_tokens }\n : {}),\n ...(inputDetails?.image_tokens\n ? { imageTokens: inputDetails.image_tokens }\n : {}),\n }\n if (Object.keys(promptTokensDetails).length > 0) {\n result.promptTokensDetails = promptTokensDetails\n }\n\n return result\n}\n"],"names":[],"mappings":";AAcO,SAAS,0BACd,OACwB;AACxB,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,SAAS,eAAe;AAAA,IAC5B,cAAc,MAAM,iBAAiB;AAAA,IACrC,kBAAkB,MAAM,qBAAqB;AAAA,IAC7C,aAAa,MAAM,gBAAgB;AAAA,EAAA,CACpC;AAED,QAAM,oBAAoB,MAAM;AAChC,QAAM,0BAA0B;AAAA,IAC9B,GAAI,mBAAmB,mBACnB,EAAE,iBAAiB,kBAAkB,iBAAA,IACrC,CAAA;AAAA,IACJ,GAAI,mBAAmB,eACnB,EAAE,aAAa,kBAAkB,aAAA,IACjC,CAAA;AAAA,EAAC;AAGP,QAAM,gBAAgB,MAAM;AAC5B,QAAM,sBAAsB;AAAA,IAC1B,GAAI,eAAe,gBACf,EAAE,cAAc,cAAc,cAAA,IAC9B,CAAA;AAAA,IACJ,GAAI,eAAe,eACf,EAAE,aAAa,cAAc,aAAA,IAC7B,CAAA;AAAA,EAAC;AAGP,MAAI,OAAO,KAAK,uBAAuB,EAAE,SAAS,GAAG;AACnD,WAAO,0BAA0B;AAAA,EACnC;AACA,MAAI,OAAO,KAAK,mBAAmB,EAAE,SAAS,GAAG;AAC/C,WAAO,sBAAsB;AAAA,EAC/B;AAKA,QAAM,uBAAuB;AAAA,IAC3B,GAAI,mBAAmB,6BACnB;AAAA,MACE,0BACE,kBAAkB;AAAA,IAAA,IAEtB,CAAA;AAAA,IACJ,GAAI,mBAAmB,6BACnB;AAAA,MACE,0BACE,kBAAkB;AAAA,IAAA,IAEtB,CAAA;AAAA,EAAC;AAEP,MAAI,OAAO,KAAK,oBAAoB,EAAE,SAAS,GAAG;AAChD,WAAO,uBAAuB;AAAA,EAChC;AAEA,SAAO;AACT;AAYO,SAAS,oBACd,OACwB;AACxB,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,SAAS,eAAe;AAAA,IAC5B,cAAc,MAAM,gBAAgB;AAAA,IACpC,kBAAkB,MAAM,iBAAiB;AAAA,IACzC,aAAa,MAAM,gBAAgB;AAAA,EAAA,CACpC;AAID,QAAM,eAAe,MAAM,sBAAsB;AACjD,MAAI,gBAAgB,eAAe,GAAG;AACpC,WAAO,sBAAsB;AAAA,MAC3B,GAAG,OAAO;AAAA,MACV;AAAA,IAAA;AAAA,EAEJ;AAGA,QAAM,kBAAkB,MAAM,uBAAuB;AACrD,MAAI,mBAAmB,kBAAkB,GAAG;AAC1C,WAAO,0BAA0B;AAAA,MAC/B,GAAG,OAAO;AAAA,MACV;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO;AACT;AAWO,SAAS,iBACd,OACwB;AACxB,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,SAAS,eAAe;AAAA,IAC5B,cAAc,MAAM,gBAAgB;AAAA,IACpC,kBAAkB,MAAM,iBAAiB;AAAA,IACzC,aAAa,MAAM,gBAAgB;AAAA,EAAA,CACpC;AAKD,QAAM,eAAe,MAAM;AAG3B,QAAM,sBAAsB;AAAA,IAC1B,GAAI,cAAc,cACd,EAAE,YAAY,aAAa,YAAA,IAC3B,CAAA;AAAA,IACJ,GAAI,cAAc,eACd,EAAE,aAAa,aAAa,aAAA,IAC5B,CAAA;AAAA,EAAC;AAEP,MAAI,OAAO,KAAK,mBAAmB,EAAE,SAAS,GAAG;AAC/C,WAAO,sBAAsB;AAAA,EAC/B;AAEA,SAAO;AACT;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/openai-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Shared OpenAI SDK base adapters for TanStack AI providers using Chat Completions and Responses APIs.",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"@tanstack/ai-utils": "0.2.1"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@tanstack/ai": "^0.
|
|
42
|
+
"@tanstack/ai": "^0.26.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@vitest/coverage-v8": "4.0.14",
|
|
46
46
|
"vite": "^7.3.3",
|
|
47
47
|
"zod": "^4.2.0",
|
|
48
|
-
"@tanstack/ai": "0.
|
|
48
|
+
"@tanstack/ai": "0.26.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "vite build",
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { generateId, transformNullsToUndefined } from '@tanstack/ai-utils'
|
|
8
8
|
import { extractRequestOptions } from '../utils/request-options'
|
|
9
9
|
import { makeStructuredOutputCompatible } from '../utils/schema-converter'
|
|
10
|
+
import { buildChatCompletionsUsage } from '../usage'
|
|
10
11
|
import { convertToolsToChatCompletionsFormat } from './chat-completions-tool-converter'
|
|
11
12
|
import type OpenAI from 'openai'
|
|
12
13
|
import type {
|
|
@@ -506,11 +507,7 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
506
507
|
timestamp,
|
|
507
508
|
finishReason: 'stop',
|
|
508
509
|
...(lastUsage && {
|
|
509
|
-
usage:
|
|
510
|
-
promptTokens: lastUsage.prompt_tokens,
|
|
511
|
-
completionTokens: lastUsage.completion_tokens,
|
|
512
|
-
totalTokens: lastUsage.total_tokens,
|
|
513
|
-
},
|
|
510
|
+
usage: buildChatCompletionsUsage(lastUsage),
|
|
514
511
|
}),
|
|
515
512
|
}
|
|
516
513
|
} catch (error: unknown) {
|
|
@@ -1063,11 +1060,7 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
1063
1060
|
model: lastModel || options.model,
|
|
1064
1061
|
timestamp: Date.now(),
|
|
1065
1062
|
...(lastUsage && {
|
|
1066
|
-
usage:
|
|
1067
|
-
promptTokens: lastUsage.prompt_tokens || 0,
|
|
1068
|
-
completionTokens: lastUsage.completion_tokens || 0,
|
|
1069
|
-
totalTokens: lastUsage.total_tokens || 0,
|
|
1070
|
-
},
|
|
1063
|
+
usage: buildChatCompletionsUsage(lastUsage),
|
|
1071
1064
|
}),
|
|
1072
1065
|
finishReason,
|
|
1073
1066
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { generateId, transformNullsToUndefined } from '@tanstack/ai-utils'
|
|
8
8
|
import { extractRequestOptions } from '../utils/request-options'
|
|
9
9
|
import { makeStructuredOutputCompatible } from '../utils/schema-converter'
|
|
10
|
+
import { buildResponsesUsage } from '../usage'
|
|
10
11
|
import { convertToolsToResponsesFormat } from './responses-tool-converter'
|
|
11
12
|
import type OpenAI from 'openai'
|
|
12
13
|
import type {
|
|
@@ -598,11 +599,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
598
599
|
timestamp,
|
|
599
600
|
finishReason: 'stop',
|
|
600
601
|
...(usage && {
|
|
601
|
-
usage:
|
|
602
|
-
promptTokens: usage.input_tokens,
|
|
603
|
-
completionTokens: usage.output_tokens,
|
|
604
|
-
totalTokens: usage.total_tokens,
|
|
605
|
-
},
|
|
602
|
+
usage: buildResponsesUsage(usage),
|
|
606
603
|
}),
|
|
607
604
|
}
|
|
608
605
|
} catch (error: unknown) {
|
|
@@ -1511,11 +1508,11 @@ export abstract class OpenAIBaseResponsesTextAdapter<
|
|
|
1511
1508
|
threadId: aguiState.threadId,
|
|
1512
1509
|
model: model || options.model,
|
|
1513
1510
|
timestamp: Date.now(),
|
|
1514
|
-
usage
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
},
|
|
1511
|
+
// Omit usage entirely when the provider reported none rather than
|
|
1512
|
+
// emitting fabricated zeros (also satisfies exactOptionalPropertyTypes).
|
|
1513
|
+
...(chunk.response.usage && {
|
|
1514
|
+
usage: buildResponsesUsage(chunk.response.usage),
|
|
1515
|
+
}),
|
|
1519
1516
|
finishReason,
|
|
1520
1517
|
}
|
|
1521
1518
|
runFinishedEmitted = true
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export { makeStructuredOutputCompatible } from './utils/schema-converter'
|
|
2
|
+
export {
|
|
3
|
+
buildChatCompletionsUsage,
|
|
4
|
+
buildResponsesUsage,
|
|
5
|
+
buildImagesUsage,
|
|
6
|
+
} from './usage'
|
|
2
7
|
export * from './tools/index'
|
|
3
8
|
export { OpenAIBaseChatCompletionsTextAdapter } from './adapters/chat-completions-text'
|
|
4
9
|
export {
|
package/src/usage.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { buildBaseUsage } from '@tanstack/ai'
|
|
2
|
+
import type { TokenUsage } from '@tanstack/ai'
|
|
3
|
+
import type OpenAI from 'openai'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build normalized {@link TokenUsage} from an OpenAI-compatible Chat
|
|
7
|
+
* Completions `usage` object.
|
|
8
|
+
*
|
|
9
|
+
* Shared by every provider that routes through
|
|
10
|
+
* {@link OpenAIBaseChatCompletionsTextAdapter} (OpenAI Chat Completions, Grok,
|
|
11
|
+
* Groq). Surfaces cached prompt tokens and reasoning/audio detail tokens when
|
|
12
|
+
* the provider reports them. Returns `undefined` when the provider reported no
|
|
13
|
+
* usage object, so callers omit the field rather than fabricating zeroed totals.
|
|
14
|
+
*/
|
|
15
|
+
export function buildChatCompletionsUsage(
|
|
16
|
+
usage: OpenAI.Chat.Completions.ChatCompletion['usage'] | undefined | null,
|
|
17
|
+
): TokenUsage | undefined {
|
|
18
|
+
if (!usage) return undefined
|
|
19
|
+
|
|
20
|
+
const result = buildBaseUsage({
|
|
21
|
+
promptTokens: usage.prompt_tokens || 0,
|
|
22
|
+
completionTokens: usage.completion_tokens || 0,
|
|
23
|
+
totalTokens: usage.total_tokens || 0,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const completionDetails = usage.completion_tokens_details
|
|
27
|
+
const completionTokensDetails = {
|
|
28
|
+
...(completionDetails?.reasoning_tokens
|
|
29
|
+
? { reasoningTokens: completionDetails.reasoning_tokens }
|
|
30
|
+
: {}),
|
|
31
|
+
...(completionDetails?.audio_tokens
|
|
32
|
+
? { audioTokens: completionDetails.audio_tokens }
|
|
33
|
+
: {}),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const promptDetails = usage.prompt_tokens_details
|
|
37
|
+
const promptTokensDetails = {
|
|
38
|
+
...(promptDetails?.cached_tokens
|
|
39
|
+
? { cachedTokens: promptDetails.cached_tokens }
|
|
40
|
+
: {}),
|
|
41
|
+
...(promptDetails?.audio_tokens
|
|
42
|
+
? { audioTokens: promptDetails.audio_tokens }
|
|
43
|
+
: {}),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (Object.keys(completionTokensDetails).length > 0) {
|
|
47
|
+
result.completionTokensDetails = completionTokensDetails
|
|
48
|
+
}
|
|
49
|
+
if (Object.keys(promptTokensDetails).length > 0) {
|
|
50
|
+
result.promptTokensDetails = promptTokensDetails
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Predicted Outputs accepted/rejected counts have no canonical TokenUsage
|
|
54
|
+
// slot but are still billed (rejected tokens included), so surface them under
|
|
55
|
+
// providerUsageDetails — matching how the OpenRouter adapter exposes them.
|
|
56
|
+
const providerUsageDetails = {
|
|
57
|
+
...(completionDetails?.accepted_prediction_tokens
|
|
58
|
+
? {
|
|
59
|
+
acceptedPredictionTokens:
|
|
60
|
+
completionDetails.accepted_prediction_tokens,
|
|
61
|
+
}
|
|
62
|
+
: {}),
|
|
63
|
+
...(completionDetails?.rejected_prediction_tokens
|
|
64
|
+
? {
|
|
65
|
+
rejectedPredictionTokens:
|
|
66
|
+
completionDetails.rejected_prediction_tokens,
|
|
67
|
+
}
|
|
68
|
+
: {}),
|
|
69
|
+
}
|
|
70
|
+
if (Object.keys(providerUsageDetails).length > 0) {
|
|
71
|
+
result.providerUsageDetails = providerUsageDetails
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build normalized {@link TokenUsage} from an OpenAI Responses API
|
|
79
|
+
* `ResponseUsage` object.
|
|
80
|
+
*
|
|
81
|
+
* Shared by every provider that routes through
|
|
82
|
+
* {@link OpenAIBaseResponsesTextAdapter}. Surfaces cached prompt tokens and
|
|
83
|
+
* reasoning detail tokens when present. Returns `undefined` when the provider
|
|
84
|
+
* reported no usage object, so callers omit the field rather than fabricating
|
|
85
|
+
* zeroed totals.
|
|
86
|
+
*/
|
|
87
|
+
export function buildResponsesUsage(
|
|
88
|
+
usage: OpenAI.Responses.ResponseUsage | undefined | null,
|
|
89
|
+
): TokenUsage | undefined {
|
|
90
|
+
if (!usage) return undefined
|
|
91
|
+
|
|
92
|
+
const result = buildBaseUsage({
|
|
93
|
+
promptTokens: usage.input_tokens || 0,
|
|
94
|
+
completionTokens: usage.output_tokens || 0,
|
|
95
|
+
totalTokens: usage.total_tokens || 0,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Despite the SDK types marking these required, they can be undefined at runtime.
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
100
|
+
const cachedTokens = usage.input_tokens_details?.cached_tokens
|
|
101
|
+
if (cachedTokens && cachedTokens > 0) {
|
|
102
|
+
result.promptTokensDetails = {
|
|
103
|
+
...result.promptTokensDetails,
|
|
104
|
+
cachedTokens,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
109
|
+
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens
|
|
110
|
+
if (reasoningTokens && reasoningTokens > 0) {
|
|
111
|
+
result.completionTokensDetails = {
|
|
112
|
+
...result.completionTokensDetails,
|
|
113
|
+
reasoningTokens,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build normalized {@link TokenUsage} from an OpenAI Images API `usage` object.
|
|
122
|
+
*
|
|
123
|
+
* Shared by every provider that generates images through the OpenAI Images SDK
|
|
124
|
+
* (OpenAI, Grok). Token-billed image models (e.g. gpt-image-1) report an input
|
|
125
|
+
* breakdown of text vs image tokens, which is surfaced on `promptTokensDetails`.
|
|
126
|
+
* Models that don't return usage (e.g. DALL·E) yield `undefined` so callers can
|
|
127
|
+
* omit the field rather than emit zeroed totals.
|
|
128
|
+
*/
|
|
129
|
+
export function buildImagesUsage(
|
|
130
|
+
usage: OpenAI.Images.ImagesResponse['usage'] | undefined | null,
|
|
131
|
+
): TokenUsage | undefined {
|
|
132
|
+
if (!usage) return undefined
|
|
133
|
+
|
|
134
|
+
const result = buildBaseUsage({
|
|
135
|
+
promptTokens: usage.input_tokens || 0,
|
|
136
|
+
completionTokens: usage.output_tokens || 0,
|
|
137
|
+
totalTokens: usage.total_tokens || 0,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// The SDK types input_tokens_details (and its numeric fields) as required, but
|
|
141
|
+
// real responses — e.g. from DALL·E or other non-token-billed models — can
|
|
142
|
+
// omit them, so treat the breakdown as optional.
|
|
143
|
+
const inputDetails = usage.input_tokens_details as
|
|
144
|
+
| { text_tokens?: number; image_tokens?: number }
|
|
145
|
+
| undefined
|
|
146
|
+
const promptTokensDetails = {
|
|
147
|
+
...(inputDetails?.text_tokens
|
|
148
|
+
? { textTokens: inputDetails.text_tokens }
|
|
149
|
+
: {}),
|
|
150
|
+
...(inputDetails?.image_tokens
|
|
151
|
+
? { imageTokens: inputDetails.image_tokens }
|
|
152
|
+
: {}),
|
|
153
|
+
}
|
|
154
|
+
if (Object.keys(promptTokensDetails).length > 0) {
|
|
155
|
+
result.promptTokensDetails = promptTokensDetails
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result
|
|
159
|
+
}
|