@tanstack/openai-base 0.10.16 → 0.12.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.
@@ -0,0 +1,42 @@
1
+ import { ResponseInputItem } from 'openai/resources/responses/responses';
2
+ /**
3
+ * OpenAI Responses tools the app must run.
4
+ * A container `shell` runs on the provider. `apply_patch`, `local_shell`,
5
+ * and `shell` with `environment.type: "local"` (or no environment) do not.
6
+ */
7
+ export type OpenAIUserToolName = 'shell' | 'apply_patch' | 'local_shell';
8
+ export interface OpenAIUserExecutedCall {
9
+ name: OpenAIUserToolName;
10
+ callId: string;
11
+ itemId?: string;
12
+ input: Record<string, unknown>;
13
+ maxOutputLength?: number | null;
14
+ }
15
+ interface ToolCallLike {
16
+ id: string;
17
+ function: {
18
+ name: string;
19
+ arguments: string;
20
+ };
21
+ metadata?: unknown;
22
+ }
23
+ /**
24
+ * Hosted shell calls already include a `shell_call_output` in the same
25
+ * response. Those call ids must not pause the app for another run.
26
+ */
27
+ export declare function hostedShellCallIds(output: ReadonlyArray<unknown>): Set<string>;
28
+ export declare function readUserToolName(metadata: unknown): OpenAIUserToolName | null;
29
+ /**
30
+ * Read a user-run Responses output item.
31
+ * `bareShell` is true only once the full response is known. A shell call
32
+ * with no environment waits for that pass, so a hosted call that later
33
+ * carries `shell_call_output` is not asked of the app.
34
+ */
35
+ export declare function readUserExecutedCall(item: unknown, options: {
36
+ bareShell: boolean;
37
+ }): OpenAIUserExecutedCall | null;
38
+ /** Replay a user-run tool call as the Responses input item OpenAI expects. */
39
+ export declare function userToolRequestItem(toolCall: ToolCallLike): ResponseInputItem | null;
40
+ /** Replay the app's tool result as the matching Responses output item. */
41
+ export declare function userToolResultItem(toolCall: ToolCallLike, content: unknown): ResponseInputItem | null;
42
+ export {};
@@ -0,0 +1,241 @@
1
+ //#region src/adapters/responses-user-tools.ts
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function stringList(value) {
6
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) return null;
7
+ return value;
8
+ }
9
+ function stringEnv(value) {
10
+ if (!isRecord(value)) return {};
11
+ const env = {};
12
+ for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") env[key] = entry;
13
+ return env;
14
+ }
15
+ function nullableNumber(value) {
16
+ return typeof value === "number" || value === null ? value : null;
17
+ }
18
+ /**
19
+ * Hosted shell calls already include a `shell_call_output` in the same
20
+ * response. Those call ids must not pause the app for another run.
21
+ */
22
+ function hostedShellCallIds(output) {
23
+ const ids = /* @__PURE__ */ new Set();
24
+ for (const item of output) if (isRecord(item) && item.type === "shell_call_output" && typeof item.call_id === "string") ids.add(item.call_id);
25
+ return ids;
26
+ }
27
+ function readUserToolName(metadata) {
28
+ if (!isRecord(metadata)) return null;
29
+ const name = metadata.openaiUserTool;
30
+ if (name === "shell" || name === "apply_patch" || name === "local_shell") return name;
31
+ return null;
32
+ }
33
+ function readItemId(metadata) {
34
+ if (!isRecord(metadata)) return void 0;
35
+ return typeof metadata.itemId === "string" && metadata.itemId.length > 0 ? metadata.itemId : void 0;
36
+ }
37
+ function readMaxOutputLength(metadata) {
38
+ if (!isRecord(metadata)) return void 0;
39
+ const value = metadata.maxOutputLength;
40
+ return typeof value === "number" || value === null ? value : void 0;
41
+ }
42
+ function applyPatchOperation(value) {
43
+ if (!isRecord(value) || typeof value.path !== "string") return null;
44
+ if (value.type === "delete_file") return {
45
+ type: "delete_file",
46
+ path: value.path
47
+ };
48
+ if ((value.type === "create_file" || value.type === "update_file") && typeof value.diff === "string") return {
49
+ type: value.type,
50
+ path: value.path,
51
+ diff: value.diff
52
+ };
53
+ return null;
54
+ }
55
+ /**
56
+ * Read a user-run Responses output item.
57
+ * `bareShell` is true only once the full response is known. A shell call
58
+ * with no environment waits for that pass, so a hosted call that later
59
+ * carries `shell_call_output` is not asked of the app.
60
+ */
61
+ function readUserExecutedCall(item, options) {
62
+ if (!isRecord(item) || typeof item.type !== "string") return null;
63
+ const callId = typeof item.call_id === "string" ? item.call_id : "";
64
+ const itemId = typeof item.id === "string" ? item.id : void 0;
65
+ if (item.type === "apply_patch_call") {
66
+ const operation = applyPatchOperation(item.operation);
67
+ if (!callId || !operation) return null;
68
+ return {
69
+ name: "apply_patch",
70
+ callId,
71
+ ...itemId ? { itemId } : {},
72
+ input: { operation }
73
+ };
74
+ }
75
+ if (item.type === "local_shell_call") {
76
+ if (!isRecord(item.action)) return null;
77
+ const command = stringList(item.action.command);
78
+ if (!callId || !command || command.length === 0) return null;
79
+ return {
80
+ name: "local_shell",
81
+ callId,
82
+ ...itemId ? { itemId } : {},
83
+ input: item.action
84
+ };
85
+ }
86
+ if (item.type !== "shell_call") return null;
87
+ const environment = item.environment;
88
+ if (isRecord(environment)) {
89
+ if (environment.type !== "local") return null;
90
+ } else if (!options.bareShell) return null;
91
+ if (!callId || !isRecord(item.action)) return null;
92
+ const commands = stringList(item.action.commands);
93
+ if (!commands || commands.length === 0) return null;
94
+ const maxOutputLength = nullableNumber(item.action.max_output_length);
95
+ return {
96
+ name: "shell",
97
+ callId,
98
+ ...itemId ? { itemId } : {},
99
+ input: {
100
+ commands,
101
+ max_output_length: maxOutputLength,
102
+ timeout_ms: nullableNumber(item.action.timeout_ms)
103
+ },
104
+ maxOutputLength
105
+ };
106
+ }
107
+ function parseArguments(argumentsString) {
108
+ try {
109
+ const parsed = JSON.parse(argumentsString);
110
+ return isRecord(parsed) ? parsed : {};
111
+ } catch {
112
+ return {};
113
+ }
114
+ }
115
+ function parseContent(content) {
116
+ if (typeof content !== "string") return content;
117
+ try {
118
+ return JSON.parse(content);
119
+ } catch {
120
+ return content;
121
+ }
122
+ }
123
+ /** Replay a user-run tool call as the Responses input item OpenAI expects. */
124
+ function userToolRequestItem(toolCall) {
125
+ const name = readUserToolName(toolCall.metadata);
126
+ if (!name) return null;
127
+ const args = parseArguments(toolCall.function.arguments);
128
+ const itemId = readItemId(toolCall.metadata);
129
+ if (name === "apply_patch") {
130
+ const operation = applyPatchOperation(args.operation);
131
+ if (!operation) return null;
132
+ return {
133
+ type: "apply_patch_call",
134
+ call_id: toolCall.id,
135
+ status: "completed",
136
+ operation,
137
+ ...itemId ? { id: itemId } : {}
138
+ };
139
+ }
140
+ if (name === "local_shell") {
141
+ const command = stringList(args.command);
142
+ if (!command) return null;
143
+ return {
144
+ type: "local_shell_call",
145
+ id: itemId ?? toolCall.id,
146
+ call_id: toolCall.id,
147
+ status: "completed",
148
+ action: {
149
+ type: "exec",
150
+ command,
151
+ env: stringEnv(args.env),
152
+ timeout_ms: nullableNumber(args.timeout_ms),
153
+ ...typeof args.user === "string" || args.user === null ? { user: args.user } : {},
154
+ ...typeof args.working_directory === "string" || args.working_directory === null ? { working_directory: args.working_directory } : {}
155
+ }
156
+ };
157
+ }
158
+ const commands = stringList(args.commands);
159
+ if (!commands) return null;
160
+ return {
161
+ type: "shell_call",
162
+ call_id: toolCall.id,
163
+ status: "completed",
164
+ action: {
165
+ commands,
166
+ max_output_length: nullableNumber(args.max_output_length),
167
+ timeout_ms: nullableNumber(args.timeout_ms)
168
+ },
169
+ ...itemId ? { id: itemId } : {}
170
+ };
171
+ }
172
+ function shellOutcome(value) {
173
+ if (isRecord(value) && value.type === "timeout") return { type: "timeout" };
174
+ return {
175
+ type: "exit",
176
+ exit_code: isRecord(value) && typeof value.exit_code === "number" ? value.exit_code : 0
177
+ };
178
+ }
179
+ function shellEntry(value) {
180
+ if (typeof value === "string") return {
181
+ stdout: value,
182
+ stderr: "",
183
+ outcome: {
184
+ type: "exit",
185
+ exit_code: 0
186
+ }
187
+ };
188
+ const record = isRecord(value) ? value : {};
189
+ return {
190
+ stdout: typeof record.stdout === "string" ? record.stdout : "",
191
+ stderr: typeof record.stderr === "string" ? record.stderr : "",
192
+ outcome: shellOutcome(record.outcome)
193
+ };
194
+ }
195
+ function shellOutputList(content) {
196
+ const parsed = parseContent(content);
197
+ if (isRecord(parsed) && Array.isArray(parsed.output)) return parsed.output.map((entry) => shellEntry(entry));
198
+ if (isRecord(parsed) && ("stdout" in parsed || "outcome" in parsed)) return [shellEntry(parsed)];
199
+ if (typeof parsed === "string") return [shellEntry(parsed)];
200
+ return [shellEntry(JSON.stringify(parsed ?? ""))];
201
+ }
202
+ /** Replay the app's tool result as the matching Responses output item. */
203
+ function userToolResultItem(toolCall, content) {
204
+ const name = readUserToolName(toolCall.metadata);
205
+ if (!name) return null;
206
+ const parsed = parseContent(content);
207
+ if (name === "apply_patch") {
208
+ const record = isRecord(parsed) ? parsed : {};
209
+ const failed = record.status === "failed" || record.status !== "completed" && typeof record.error === "string";
210
+ const output = typeof record.output === "string" ? record.output : typeof record.error === "string" ? record.error : typeof parsed === "string" ? parsed : void 0;
211
+ return {
212
+ type: "apply_patch_call_output",
213
+ call_id: toolCall.id,
214
+ status: failed ? "failed" : "completed",
215
+ ...output !== void 0 ? { output } : {}
216
+ };
217
+ }
218
+ if (name === "local_shell") {
219
+ const output = typeof parsed === "string" ? parsed : isRecord(parsed) && typeof parsed.output === "string" ? parsed.output : JSON.stringify(parsed ?? "");
220
+ return {
221
+ type: "local_shell_call_output",
222
+ id: toolCall.id,
223
+ output,
224
+ status: "completed"
225
+ };
226
+ }
227
+ const record = isRecord(parsed) ? parsed : {};
228
+ const fromResult = nullableNumber(record.max_output_length);
229
+ const fromCall = readMaxOutputLength(toolCall.metadata);
230
+ const maxOutputLength = record.max_output_length !== void 0 ? fromResult : fromCall;
231
+ return {
232
+ type: "shell_call_output",
233
+ call_id: toolCall.id,
234
+ output: shellOutputList(content),
235
+ ...maxOutputLength !== void 0 ? { max_output_length: maxOutputLength } : {}
236
+ };
237
+ }
238
+ //#endregion
239
+ export { hostedShellCallIds, readUserExecutedCall, readUserToolName, userToolRequestItem, userToolResultItem };
240
+
241
+ //# sourceMappingURL=responses-user-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responses-user-tools.js","names":[],"sources":["../../../src/adapters/responses-user-tools.ts"],"sourcesContent":["import type { ResponseInputItem } from 'openai/resources/responses/responses'\n\n/**\n * OpenAI Responses tools the app must run.\n * A container `shell` runs on the provider. `apply_patch`, `local_shell`,\n * and `shell` with `environment.type: \"local\"` (or no environment) do not.\n */\nexport type OpenAIUserToolName = 'shell' | 'apply_patch' | 'local_shell'\n\nexport interface OpenAIUserExecutedCall {\n name: OpenAIUserToolName\n callId: string\n itemId?: string\n input: Record<string, unknown>\n maxOutputLength?: number | null\n}\n\ninterface ToolCallLike {\n id: string\n function: { name: string; arguments: string }\n metadata?: unknown\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction stringList(value: unknown): Array<string> | null {\n if (\n !Array.isArray(value) ||\n !value.every((entry) => typeof entry === 'string')\n ) {\n return null\n }\n return value\n}\n\nfunction stringEnv(value: unknown): Record<string, string> {\n if (!isRecord(value)) return {}\n const env: Record<string, string> = {}\n for (const [key, entry] of Object.entries(value)) {\n if (typeof entry === 'string') env[key] = entry\n }\n return env\n}\n\nfunction nullableNumber(value: unknown): number | null {\n return typeof value === 'number' || value === null ? value : null\n}\n\n/**\n * Hosted shell calls already include a `shell_call_output` in the same\n * response. Those call ids must not pause the app for another run.\n */\nexport function hostedShellCallIds(\n output: ReadonlyArray<unknown>,\n): Set<string> {\n const ids = new Set<string>()\n for (const item of output) {\n if (\n isRecord(item) &&\n item.type === 'shell_call_output' &&\n typeof item.call_id === 'string'\n ) {\n ids.add(item.call_id)\n }\n }\n return ids\n}\n\nexport function readUserToolName(metadata: unknown): OpenAIUserToolName | null {\n if (!isRecord(metadata)) return null\n const name = metadata.openaiUserTool\n if (name === 'shell' || name === 'apply_patch' || name === 'local_shell') {\n return name\n }\n return null\n}\n\nfunction readItemId(metadata: unknown): string | undefined {\n if (!isRecord(metadata)) return undefined\n return typeof metadata.itemId === 'string' && metadata.itemId.length > 0\n ? metadata.itemId\n : undefined\n}\n\nfunction readMaxOutputLength(metadata: unknown): number | null | undefined {\n if (!isRecord(metadata)) return undefined\n const value = metadata.maxOutputLength\n return typeof value === 'number' || value === null ? value : undefined\n}\n\nfunction applyPatchOperation(\n value: unknown,\n):\n | { type: 'create_file'; path: string; diff: string }\n | { type: 'update_file'; path: string; diff: string }\n | { type: 'delete_file'; path: string }\n | null {\n if (!isRecord(value) || typeof value.path !== 'string') return null\n if (value.type === 'delete_file') {\n return { type: 'delete_file', path: value.path }\n }\n if (\n (value.type === 'create_file' || value.type === 'update_file') &&\n typeof value.diff === 'string'\n ) {\n return { type: value.type, path: value.path, diff: value.diff }\n }\n return null\n}\n\n/**\n * Read a user-run Responses output item.\n * `bareShell` is true only once the full response is known. A shell call\n * with no environment waits for that pass, so a hosted call that later\n * carries `shell_call_output` is not asked of the app.\n */\nexport function readUserExecutedCall(\n item: unknown,\n options: { bareShell: boolean },\n): OpenAIUserExecutedCall | null {\n if (!isRecord(item) || typeof item.type !== 'string') return null\n const callId = typeof item.call_id === 'string' ? item.call_id : ''\n const itemId = typeof item.id === 'string' ? item.id : undefined\n\n if (item.type === 'apply_patch_call') {\n const operation = applyPatchOperation(item.operation)\n if (!callId || !operation) return null\n return {\n name: 'apply_patch',\n callId,\n ...(itemId ? { itemId } : {}),\n input: { operation },\n }\n }\n\n if (item.type === 'local_shell_call') {\n if (!isRecord(item.action)) return null\n const command = stringList(item.action.command)\n if (!callId || !command || command.length === 0) return null\n return {\n name: 'local_shell',\n callId,\n ...(itemId ? { itemId } : {}),\n input: item.action,\n }\n }\n\n if (item.type !== 'shell_call') return null\n const environment = item.environment\n if (isRecord(environment)) {\n if (environment.type !== 'local') return null\n } else if (!options.bareShell) {\n return null\n }\n if (!callId || !isRecord(item.action)) return null\n const commands = stringList(item.action.commands)\n if (!commands || commands.length === 0) return null\n const maxOutputLength = nullableNumber(item.action.max_output_length)\n return {\n name: 'shell',\n callId,\n ...(itemId ? { itemId } : {}),\n input: {\n commands,\n max_output_length: maxOutputLength,\n timeout_ms: nullableNumber(item.action.timeout_ms),\n },\n maxOutputLength,\n }\n}\n\nfunction parseArguments(argumentsString: string): Record<string, unknown> {\n try {\n const parsed: unknown = JSON.parse(argumentsString)\n return isRecord(parsed) ? parsed : {}\n } catch {\n return {}\n }\n}\n\nfunction parseContent(content: unknown): unknown {\n if (typeof content !== 'string') return content\n try {\n return JSON.parse(content) as unknown\n } catch {\n return content\n }\n}\n\n/** Replay a user-run tool call as the Responses input item OpenAI expects. */\nexport function userToolRequestItem(\n toolCall: ToolCallLike,\n): ResponseInputItem | null {\n const name = readUserToolName(toolCall.metadata)\n if (!name) return null\n const args = parseArguments(toolCall.function.arguments)\n const itemId = readItemId(toolCall.metadata)\n\n if (name === 'apply_patch') {\n const operation = applyPatchOperation(args.operation)\n if (!operation) return null\n return {\n type: 'apply_patch_call',\n call_id: toolCall.id,\n status: 'completed',\n operation,\n ...(itemId ? { id: itemId } : {}),\n }\n }\n\n if (name === 'local_shell') {\n const command = stringList(args.command)\n if (!command) return null\n return {\n type: 'local_shell_call',\n id: itemId ?? toolCall.id,\n call_id: toolCall.id,\n status: 'completed',\n action: {\n type: 'exec',\n command,\n env: stringEnv(args.env),\n timeout_ms: nullableNumber(args.timeout_ms),\n ...(typeof args.user === 'string' || args.user === null\n ? { user: args.user }\n : {}),\n ...(typeof args.working_directory === 'string' ||\n args.working_directory === null\n ? { working_directory: args.working_directory }\n : {}),\n },\n }\n }\n\n const commands = stringList(args.commands)\n if (!commands) return null\n return {\n type: 'shell_call',\n call_id: toolCall.id,\n status: 'completed',\n action: {\n commands,\n max_output_length: nullableNumber(args.max_output_length),\n timeout_ms: nullableNumber(args.timeout_ms),\n },\n ...(itemId ? { id: itemId } : {}),\n }\n}\n\ntype ShellOutcome = { type: 'exit'; exit_code: number } | { type: 'timeout' }\n\nfunction shellOutcome(value: unknown): ShellOutcome {\n if (isRecord(value) && value.type === 'timeout') return { type: 'timeout' }\n const exitCode =\n isRecord(value) && typeof value.exit_code === 'number' ? value.exit_code : 0\n return { type: 'exit', exit_code: exitCode }\n}\n\nfunction shellEntry(value: unknown): {\n stdout: string\n stderr: string\n outcome: ShellOutcome\n} {\n if (typeof value === 'string') {\n return {\n stdout: value,\n stderr: '',\n outcome: { type: 'exit', exit_code: 0 },\n }\n }\n const record = isRecord(value) ? value : {}\n return {\n stdout: typeof record.stdout === 'string' ? record.stdout : '',\n stderr: typeof record.stderr === 'string' ? record.stderr : '',\n outcome: shellOutcome(record.outcome),\n }\n}\n\nfunction shellOutputList(content: unknown): Array<{\n stdout: string\n stderr: string\n outcome: ShellOutcome\n}> {\n const parsed = parseContent(content)\n if (isRecord(parsed) && Array.isArray(parsed.output)) {\n return parsed.output.map((entry) => shellEntry(entry))\n }\n if (isRecord(parsed) && ('stdout' in parsed || 'outcome' in parsed)) {\n return [shellEntry(parsed)]\n }\n if (typeof parsed === 'string') return [shellEntry(parsed)]\n return [shellEntry(JSON.stringify(parsed ?? ''))]\n}\n\n/** Replay the app's tool result as the matching Responses output item. */\nexport function userToolResultItem(\n toolCall: ToolCallLike,\n content: unknown,\n): ResponseInputItem | null {\n const name = readUserToolName(toolCall.metadata)\n if (!name) return null\n const parsed = parseContent(content)\n\n if (name === 'apply_patch') {\n const record = isRecord(parsed) ? parsed : {}\n const failed =\n record.status === 'failed' ||\n (record.status !== 'completed' && typeof record.error === 'string')\n const output =\n typeof record.output === 'string'\n ? record.output\n : typeof record.error === 'string'\n ? record.error\n : typeof parsed === 'string'\n ? parsed\n : undefined\n return {\n type: 'apply_patch_call_output',\n call_id: toolCall.id,\n status: failed ? 'failed' : 'completed',\n ...(output !== undefined ? { output } : {}),\n }\n }\n\n if (name === 'local_shell') {\n const output =\n typeof parsed === 'string'\n ? parsed\n : isRecord(parsed) && typeof parsed.output === 'string'\n ? parsed.output\n : JSON.stringify(parsed ?? '')\n return {\n type: 'local_shell_call_output',\n id: toolCall.id,\n output,\n status: 'completed',\n }\n }\n\n const record = isRecord(parsed) ? parsed : {}\n const fromResult = nullableNumber(record.max_output_length)\n const fromCall = readMaxOutputLength(toolCall.metadata)\n const maxOutputLength =\n record.max_output_length !== undefined ? fromResult : fromCall\n return {\n type: 'shell_call_output',\n call_id: toolCall.id,\n output: shellOutputList(content),\n ...(maxOutputLength !== undefined\n ? { max_output_length: maxOutputLength }\n : {}),\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- export { makeStructuredOutputCompatible, makeStructuredOutputCompatibleWithMap, } from './utils/schema-converter.js';
1
+ export { makeStructuredOutputCompatible, makeStructuredOutputCompatibleWithMap, warnStrictFallback, } from './utils/schema-converter.js';
2
+ export type { OpenAIBaseTextAdapterOptions } from './utils/schema-converter.js';
2
3
  export { buildChatCompletionsUsage, buildResponsesUsage, buildImagesUsage, } from './usage.js';
3
4
  export * from './tools/index.js';
4
5
  export { OpenAIBaseChatCompletionsTextAdapter } from './adapters/chat-completions-text.js';
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { makeStructuredOutputCompatible, makeStructuredOutputCompatibleWithMap } from "./utils/schema-converter.js";
1
+ import { makeStructuredOutputCompatible, makeStructuredOutputCompatibleWithMap, warnStrictFallback } from "./utils/schema-converter.js";
2
2
  import { buildChatCompletionsUsage, buildImagesUsage, buildResponsesUsage } from "./usage.js";
3
3
  import { applyPatchTool, convertApplyPatchToolToAdapterFormat } from "./tools/apply-patch-tool.js";
4
4
  import { codeInterpreterTool, convertCodeInterpreterToolToAdapterFormat } from "./tools/code-interpreter-tool.js";
@@ -18,4 +18,4 @@ import { convertFunctionToolToChatCompletionsFormat, convertToolsToChatCompletio
18
18
  import { OpenAIBaseChatCompletionsTextAdapter } from "./adapters/chat-completions-text.js";
19
19
  import { convertFunctionToolToResponsesFormat, convertToolsToResponsesFormat } from "./adapters/responses-tool-converter.js";
20
20
  import { OpenAIBaseResponsesTextAdapter } from "./adapters/responses-text.js";
21
- export { OpenAIBaseChatCompletionsTextAdapter, OpenAIBaseResponsesTextAdapter, applyPatchTool, buildChatCompletionsUsage, buildImagesUsage, buildResponsesUsage, codeInterpreterTool, computerUseTool, convertApplyPatchToolToAdapterFormat, convertCodeInterpreterToolToAdapterFormat, convertComputerUseToolToAdapterFormat, convertCustomToolToAdapterFormat, convertFileSearchToolToAdapterFormat, convertFunctionToolToAdapterFormat, convertFunctionToolToChatCompletionsFormat, convertFunctionToolToResponsesFormat, convertImageGenerationToolToAdapterFormat, convertLocalShellToolToAdapterFormat, convertMCPToolToAdapterFormat, convertShellToolToAdapterFormat, convertToolsToChatCompletionsFormat, convertToolsToProviderFormat, convertToolsToResponsesFormat, convertWebSearchPreviewToolToAdapterFormat, convertWebSearchToolToAdapterFormat, customTool, fileSearchTool, imageGenerationTool, localShellTool, makeStructuredOutputCompatible, makeStructuredOutputCompatibleWithMap, mcpTool, shellTool, validateMCPtool, validateMaxNumResults, validatePartialImages, webSearchPreviewTool, webSearchTool };
21
+ export { OpenAIBaseChatCompletionsTextAdapter, OpenAIBaseResponsesTextAdapter, applyPatchTool, buildChatCompletionsUsage, buildImagesUsage, buildResponsesUsage, codeInterpreterTool, computerUseTool, convertApplyPatchToolToAdapterFormat, convertCodeInterpreterToolToAdapterFormat, convertComputerUseToolToAdapterFormat, convertCustomToolToAdapterFormat, convertFileSearchToolToAdapterFormat, convertFunctionToolToAdapterFormat, convertFunctionToolToChatCompletionsFormat, convertFunctionToolToResponsesFormat, convertImageGenerationToolToAdapterFormat, convertLocalShellToolToAdapterFormat, convertMCPToolToAdapterFormat, convertShellToolToAdapterFormat, convertToolsToChatCompletionsFormat, convertToolsToProviderFormat, convertToolsToResponsesFormat, convertWebSearchPreviewToolToAdapterFormat, convertWebSearchToolToAdapterFormat, customTool, fileSearchTool, imageGenerationTool, localShellTool, makeStructuredOutputCompatible, makeStructuredOutputCompatibleWithMap, mcpTool, shellTool, validateMCPtool, validateMaxNumResults, validatePartialImages, warnStrictFallback, webSearchPreviewTool, webSearchTool };
@@ -6,8 +6,8 @@ import { default as OpenAI } from 'openai';
6
6
  *
7
7
  * Shared by every provider that routes through
8
8
  * {@link OpenAIBaseChatCompletionsTextAdapter} (OpenAI Chat Completions, Grok,
9
- * Groq). Surfaces cached prompt tokens and reasoning/audio detail tokens when
10
- * the provider reports them. Returns `undefined` when the provider reported no
9
+ * Groq). Surfaces cache read/write prompt tokens and reasoning/audio detail
10
+ * tokens when the provider reports them. Returns `undefined` when the provider reported no
11
11
  * usage object, so callers omit the field rather than fabricating zeroed totals.
12
12
  */
13
13
  export declare function buildChatCompletionsUsage(usage: OpenAI.Chat.Completions.ChatCompletion['usage'] | undefined | null): TokenUsage | undefined;
package/dist/esm/usage.js CHANGED
@@ -6,8 +6,8 @@ import { buildBaseUsage } from "@tanstack/ai";
6
6
  *
7
7
  * Shared by every provider that routes through
8
8
  * {@link OpenAIBaseChatCompletionsTextAdapter} (OpenAI Chat Completions, Grok,
9
- * Groq). Surfaces cached prompt tokens and reasoning/audio detail tokens when
10
- * the provider reports them. Returns `undefined` when the provider reported no
9
+ * Groq). Surfaces cache read/write prompt tokens and reasoning/audio detail
10
+ * tokens when the provider reports them. Returns `undefined` when the provider reported no
11
11
  * usage object, so callers omit the field rather than fabricating zeroed totals.
12
12
  */
13
13
  function buildChatCompletionsUsage(usage) {
@@ -23,8 +23,10 @@ function buildChatCompletionsUsage(usage) {
23
23
  ...completionDetails?.audio_tokens ? { audioTokens: completionDetails.audio_tokens } : {}
24
24
  };
25
25
  const promptDetails = usage.prompt_tokens_details;
26
+ const cachedTokens = promptDetails?.cached_tokens || usage.cached_tokens;
26
27
  const promptTokensDetails = {
27
- ...promptDetails?.cached_tokens ? { cachedTokens: promptDetails.cached_tokens } : {},
28
+ ...cachedTokens ? { cachedTokens } : {},
29
+ ...promptDetails?.cache_write_tokens ? { cacheWriteTokens: promptDetails.cache_write_tokens } : {},
28
30
  ...promptDetails?.audio_tokens ? { audioTokens: promptDetails.audio_tokens } : {}
29
31
  };
30
32
  if (Object.keys(completionTokensDetails).length > 0) result.completionTokensDetails = completionTokensDetails;
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","names":[],"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"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,0BACd,OACwB;CACxB,IAAI,CAAC,OAAO,OAAO,KAAA;CAEnB,MAAM,SAAS,eAAe;EAC5B,cAAc,MAAM,iBAAiB;EACrC,kBAAkB,MAAM,qBAAqB;EAC7C,aAAa,MAAM,gBAAgB;CACrC,CAAC;CAED,MAAM,oBAAoB,MAAM;CAChC,MAAM,0BAA0B;EAC9B,GAAI,mBAAmB,mBACnB,EAAE,iBAAiB,kBAAkB,iBAAiB,IACtD,CAAC;EACL,GAAI,mBAAmB,eACnB,EAAE,aAAa,kBAAkB,aAAa,IAC9C,CAAC;CACP;CAEA,MAAM,gBAAgB,MAAM;CAC5B,MAAM,sBAAsB;EAC1B,GAAI,eAAe,gBACf,EAAE,cAAc,cAAc,cAAc,IAC5C,CAAC;EACL,GAAI,eAAe,eACf,EAAE,aAAa,cAAc,aAAa,IAC1C,CAAC;CACP;CAEA,IAAI,OAAO,KAAK,uBAAuB,CAAC,CAAC,SAAS,GAChD,OAAO,0BAA0B;CAEnC,IAAI,OAAO,KAAK,mBAAmB,CAAC,CAAC,SAAS,GAC5C,OAAO,sBAAsB;CAM/B,MAAM,uBAAuB;EAC3B,GAAI,mBAAmB,6BACnB,EACE,0BACE,kBAAkB,2BACtB,IACA,CAAC;EACL,GAAI,mBAAmB,6BACnB,EACE,0BACE,kBAAkB,2BACtB,IACA,CAAC;CACP;CACA,IAAI,OAAO,KAAK,oBAAoB,CAAC,CAAC,SAAS,GAC7C,OAAO,uBAAuB;CAGhC,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,oBACd,OACwB;CACxB,IAAI,CAAC,OAAO,OAAO,KAAA;CAEnB,MAAM,SAAS,eAAe;EAC5B,cAAc,MAAM,gBAAgB;EACpC,kBAAkB,MAAM,iBAAiB;EACzC,aAAa,MAAM,gBAAgB;CACrC,CAAC;CAID,MAAM,eAAe,MAAM,sBAAsB;CACjD,IAAI,gBAAgB,eAAe,GACjC,OAAO,sBAAsB;EAC3B,GAAG,OAAO;EACV;CACF;CAIF,MAAM,kBAAkB,MAAM,uBAAuB;CACrD,IAAI,mBAAmB,kBAAkB,GACvC,OAAO,0BAA0B;EAC/B,GAAG,OAAO;EACV;CACF;CAGF,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,iBACd,OACwB;CACxB,IAAI,CAAC,OAAO,OAAO,KAAA;CAEnB,MAAM,SAAS,eAAe;EAC5B,cAAc,MAAM,gBAAgB;EACpC,kBAAkB,MAAM,iBAAiB;EACzC,aAAa,MAAM,gBAAgB;CACrC,CAAC;CAKD,MAAM,eAAe,MAAM;CAG3B,MAAM,sBAAsB;EAC1B,GAAI,cAAc,cACd,EAAE,YAAY,aAAa,YAAY,IACvC,CAAC;EACL,GAAI,cAAc,eACd,EAAE,aAAa,aAAa,aAAa,IACzC,CAAC;CACP;CACA,IAAI,OAAO,KAAK,mBAAmB,CAAC,CAAC,SAAS,GAC5C,OAAO,sBAAsB;CAG/B,OAAO;AACT"}
1
+ {"version":3,"file":"usage.js","names":[],"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 cache read/write prompt tokens and reasoning/audio detail\n * tokens when 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 // Moonshot (Kimi) also reports `cache_write_tokens` under\n // `prompt_tokens_details`, and `cached_tokens` at the root of `usage`.\n // The OpenAI SDK types have neither field.\n const promptDetails = usage.prompt_tokens_details as\n | (OpenAI.Completions.CompletionUsage.PromptTokensDetails & {\n cache_write_tokens?: number\n })\n | undefined\n const cachedTokens =\n promptDetails?.cached_tokens ||\n (usage as { cached_tokens?: number }).cached_tokens\n const promptTokensDetails = {\n ...(cachedTokens ? { cachedTokens } : {}),\n ...(promptDetails?.cache_write_tokens\n ? { cacheWriteTokens: promptDetails.cache_write_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"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,0BACd,OACwB;CACxB,IAAI,CAAC,OAAO,OAAO,KAAA;CAEnB,MAAM,SAAS,eAAe;EAC5B,cAAc,MAAM,iBAAiB;EACrC,kBAAkB,MAAM,qBAAqB;EAC7C,aAAa,MAAM,gBAAgB;CACrC,CAAC;CAED,MAAM,oBAAoB,MAAM;CAChC,MAAM,0BAA0B;EAC9B,GAAI,mBAAmB,mBACnB,EAAE,iBAAiB,kBAAkB,iBAAiB,IACtD,CAAC;EACL,GAAI,mBAAmB,eACnB,EAAE,aAAa,kBAAkB,aAAa,IAC9C,CAAC;CACP;CAKA,MAAM,gBAAgB,MAAM;CAK5B,MAAM,eACJ,eAAe,iBACd,MAAqC;CACxC,MAAM,sBAAsB;EAC1B,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;EACvC,GAAI,eAAe,qBACf,EAAE,kBAAkB,cAAc,mBAAmB,IACrD,CAAC;EACL,GAAI,eAAe,eACf,EAAE,aAAa,cAAc,aAAa,IAC1C,CAAC;CACP;CAEA,IAAI,OAAO,KAAK,uBAAuB,CAAC,CAAC,SAAS,GAChD,OAAO,0BAA0B;CAEnC,IAAI,OAAO,KAAK,mBAAmB,CAAC,CAAC,SAAS,GAC5C,OAAO,sBAAsB;CAM/B,MAAM,uBAAuB;EAC3B,GAAI,mBAAmB,6BACnB,EACE,0BACE,kBAAkB,2BACtB,IACA,CAAC;EACL,GAAI,mBAAmB,6BACnB,EACE,0BACE,kBAAkB,2BACtB,IACA,CAAC;CACP;CACA,IAAI,OAAO,KAAK,oBAAoB,CAAC,CAAC,SAAS,GAC7C,OAAO,uBAAuB;CAGhC,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,oBACd,OACwB;CACxB,IAAI,CAAC,OAAO,OAAO,KAAA;CAEnB,MAAM,SAAS,eAAe;EAC5B,cAAc,MAAM,gBAAgB;EACpC,kBAAkB,MAAM,iBAAiB;EACzC,aAAa,MAAM,gBAAgB;CACrC,CAAC;CAID,MAAM,eAAe,MAAM,sBAAsB;CACjD,IAAI,gBAAgB,eAAe,GACjC,OAAO,sBAAsB;EAC3B,GAAG,OAAO;EACV;CACF;CAIF,MAAM,kBAAkB,MAAM,uBAAuB;CACrD,IAAI,mBAAmB,kBAAkB,GACvC,OAAO,0BAA0B;EAC/B,GAAG,OAAO;EACV;CACF;CAGF,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,iBACd,OACwB;CACxB,IAAI,CAAC,OAAO,OAAO,KAAA;CAEnB,MAAM,SAAS,eAAe;EAC5B,cAAc,MAAM,gBAAgB;EACpC,kBAAkB,MAAM,iBAAiB;EACzC,aAAa,MAAM,gBAAgB;CACrC,CAAC;CAKD,MAAM,eAAe,MAAM;CAG3B,MAAM,sBAAsB;EAC1B,GAAI,cAAc,cACd,EAAE,YAAY,aAAa,YAAY,IACvC,CAAC;EACL,GAAI,cAAc,eACd,EAAE,aAAa,aAAa,aAAa,IACzC,CAAC;CACP;CACA,IAAI,OAAO,KAAK,mBAAmB,CAAC,CAAC,SAAS,GAC5C,OAAO,sBAAsB;CAG/B,OAAO;AACT"}
@@ -1,4 +1,6 @@
1
1
  import { NullWideningMap } from '@tanstack/ai-utils';
2
+ import { Tool } from '@tanstack/ai';
3
+ import { InternalLogger } from '@tanstack/ai/adapter-internals';
2
4
  /**
3
5
  * Recursively drop JSON-Schema `format` keywords whose value isn't in OpenAI's
4
6
  * strict-mode allowlist. Pure — returns a fresh tree and never mutates `node`,
@@ -54,3 +56,23 @@ export declare function makeStructuredOutputCompatibleWithMap(schema: Record<str
54
56
  * verdict that 400s the whole request.
55
57
  */
56
58
  export declare function isStrictModeCompatible(schema: unknown): boolean;
59
+ /**
60
+ * Why `schema` must be sent with `strict: false`, or `undefined` when it can be
61
+ * strict. Runs the same checks as `isStrictModeCompatible`, in the same order.
62
+ */
63
+ export declare function strictModeFallbackReason(schema: unknown): string | undefined;
64
+ /** Options that every `openai-base` text adapter accepts in its config. */
65
+ export interface OpenAIBaseTextAdapterOptions {
66
+ /**
67
+ * In development, warn once per tool that is sent with `strict: false`
68
+ * because its schema cannot be strict. Set to `false` to turn the warning
69
+ * off. It never runs when `NODE_ENV` is `production`. Default: `true`.
70
+ */
71
+ strictFallbackWarning?: boolean;
72
+ }
73
+ /**
74
+ * Warn once per tool that is sent with `strict: false` because its schema
75
+ * cannot be strict. The tool still works, but the model is not held to the
76
+ * schema, so the developer must know (#1213).
77
+ */
78
+ export declare function warnStrictFallback(tools: Array<Tool> | undefined, logger: InternalLogger): void;
@@ -129,7 +129,34 @@ var TYPE_INDICATOR_KEYWORDS = [
129
129
  * verdict that 400s the whole request.
130
130
  */
131
131
  function isStrictModeCompatible(schema) {
132
- return !containsStrictUnsupportedKeyword(schema) && !containsTypelessSchema(schema) && !containsOpenObject(schema) && !containsUntrackableAnyOfWidening(schema);
132
+ return strictModeFallbackReason(schema) === void 0;
133
+ }
134
+ /**
135
+ * Why `schema` must be sent with `strict: false`, or `undefined` when it can be
136
+ * strict. Runs the same checks as `isStrictModeCompatible`, in the same order.
137
+ */
138
+ function strictModeFallbackReason(schema) {
139
+ const keyword = findStrictUnsupportedKeyword(schema);
140
+ if (keyword !== void 0) return `schema uses ${keyword}, which strict mode does not support`;
141
+ if (containsTypelessSchema(schema)) return "schema has a node with no type (for example z.any() or z.unknown())";
142
+ if (containsOpenObject(schema)) return "schema has an open object (for example z.record())";
143
+ if (containsUntrackableAnyOfWidening(schema)) return "schema has an optional field inside an anyOf variant";
144
+ }
145
+ var warnedStrictFallback = /* @__PURE__ */ new WeakSet();
146
+ /**
147
+ * Warn once per tool that is sent with `strict: false` because its schema
148
+ * cannot be strict. The tool still works, but the model is not held to the
149
+ * schema, so the developer must know (#1213).
150
+ */
151
+ function warnStrictFallback(tools, logger) {
152
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "production") return;
153
+ for (const tool of tools ?? []) {
154
+ if (!tool.inputSchema || warnedStrictFallback.has(tool)) continue;
155
+ const reason = strictModeFallbackReason(tool.inputSchema);
156
+ if (reason === void 0) continue;
157
+ warnedStrictFallback.add(tool);
158
+ logger.warn(`tool "${tool.name}" sent with strict: false: ${reason}`, { tool: tool.name });
159
+ }
133
160
  }
134
161
  /**
135
162
  * Reports strict conversions whose synthesized nulls cannot be represented by
@@ -157,14 +184,20 @@ function containsOpenObject(node) {
157
184
  }
158
185
  return Object.values(schema).some(containsOpenObject);
159
186
  }
160
- function containsStrictUnsupportedKeyword(node) {
161
- if (Array.isArray(node)) return node.some(containsStrictUnsupportedKeyword);
162
- if (node === null || typeof node !== "object") return false;
187
+ function findStrictUnsupportedKeyword(node) {
188
+ if (Array.isArray(node)) {
189
+ for (const item of node) {
190
+ const found = findStrictUnsupportedKeyword(item);
191
+ if (found !== void 0) return found;
192
+ }
193
+ return;
194
+ }
195
+ if (node === null || typeof node !== "object") return void 0;
163
196
  for (const [key, value] of Object.entries(node)) {
164
- if (STRICT_UNSUPPORTED_KEYWORDS.includes(key)) return true;
165
- if (containsStrictUnsupportedKeyword(value)) return true;
197
+ if (STRICT_UNSUPPORTED_KEYWORDS.includes(key)) return key;
198
+ const found = findStrictUnsupportedKeyword(value);
199
+ if (found !== void 0) return found;
166
200
  }
167
- return false;
168
201
  }
169
202
  /** A schema-position node that declares no type and so 400s strict mode. */
170
203
  function isTypelessSchema(node) {
@@ -310,6 +343,6 @@ function coerceStrictSchema(schema, originalRequired) {
310
343
  };
311
344
  }
312
345
  //#endregion
313
- export { isStrictModeCompatible, makeStructuredOutputCompatible, makeStructuredOutputCompatibleWithMap, stripUnsupportedFormats };
346
+ export { isStrictModeCompatible, makeStructuredOutputCompatible, makeStructuredOutputCompatibleWithMap, strictModeFallbackReason, stripUnsupportedFormats, warnStrictFallback };
314
347
 
315
348
  //# sourceMappingURL=schema-converter.js.map