@tanstack/openai-base 0.5.0 → 0.6.0

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.
@@ -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.5.0",
3
+ "version": "0.6.0",
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.24.0"
42
+ "@tanstack/ai": "^0.25.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.24.0"
48
+ "@tanstack/ai": "0.25.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
- promptTokens: chunk.response.usage?.input_tokens || 0,
1516
- completionTokens: chunk.response.usage?.output_tokens || 0,
1517
- totalTokens: chunk.response.usage?.total_tokens || 0,
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
+ }