@tanstack/openai-base 0.11.1 → 0.12.2
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.d.ts +4 -2
- package/dist/esm/adapters/chat-completions-text.js +6 -2
- package/dist/esm/adapters/chat-completions-text.js.map +1 -1
- package/dist/esm/adapters/responses-text.d.ts +12 -5
- package/dist/esm/adapters/responses-text.js +155 -15
- package/dist/esm/adapters/responses-text.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/usage.d.ts +2 -2
- package/dist/esm/usage.js +5 -3
- package/dist/esm/usage.js.map +1 -1
- package/dist/esm/utils/schema-converter.d.ts +22 -0
- package/dist/esm/utils/schema-converter.js +41 -8
- package/dist/esm/utils/schema-converter.js.map +1 -1
- package/package.json +4 -3
- package/src/adapters/chat-completions-text.ts +21 -3
- package/src/adapters/responses-text.ts +276 -31
- package/src/index.ts +2 -0
- package/src/usage.ts +16 -5
- package/src/utils/schema-converter.ts +72 -12
package/dist/esm/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/esm/usage.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
...
|
|
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;
|
package/dist/esm/usage.js.map
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
161
|
-
if (Array.isArray(node))
|
|
162
|
-
|
|
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
|
|
165
|
-
|
|
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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-converter.js","names":[],"sources":["../../../src/utils/schema-converter.ts"],"sourcesContent":["import type { NullWideningMap } from '@tanstack/ai-utils'\n\n/**\n * String `format` values accepted by OpenAI's strict Structured Outputs subset.\n * Any other format (e.g. \"uri\", \"uri-reference\", \"regex\") causes the API to\n * reject the whole request with `400 ... '<format>' is not a valid format`.\n * MCP servers and hand-written tools routinely declare such formats, so we strip\n * the unsupported ones before sending. See:\n * https://platform.openai.com/docs/guides/structured-outputs#supported-properties\n */\nconst SUPPORTED_STRING_FORMATS = new Set([\n 'date-time',\n 'time',\n 'date',\n 'duration',\n 'email',\n 'hostname',\n 'ipv4',\n 'ipv6',\n 'uuid',\n])\n\n/**\n * Recursively drop JSON-Schema `format` keywords whose value isn't in OpenAI's\n * strict-mode allowlist. Pure — returns a fresh tree and never mutates `node`,\n * so the caller's original tool definition is left intact.\n *\n * A property *named* `format` always has a schema (object/boolean) value, never\n * a bare string, so it is preserved and recursed into; only the `format`\n * *keyword* (whose value is a string) is subject to removal.\n */\nexport function stripUnsupportedFormats(node: any): any {\n if (Array.isArray(node)) return node.map(stripUnsupportedFormats)\n if (node === null || typeof node !== 'object') return node\n\n const out: Record<string, any> = {}\n for (const [key, value] of Object.entries(node)) {\n if (\n key === 'format' &&\n typeof value === 'string' &&\n !SUPPORTED_STRING_FORMATS.has(value)\n ) {\n continue\n }\n out[key] = stripUnsupportedFormats(value)\n }\n return out\n}\n\n/**\n * Transform a JSON schema to be compatible with OpenAI's structured output requirements.\n * OpenAI requires:\n * - All properties must be in the `required` array\n * - Optional fields should have null added to their type union\n * - additionalProperties must be false for objects\n * - String `format` keywords must be from a fixed allowlist (others are stripped)\n *\n * @param schema - JSON schema to transform\n * @param originalRequired - Original required array (to know which fields were optional)\n * @returns Transformed schema compatible with OpenAI structured output\n */\nexport function makeStructuredOutputCompatible(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n): Record<string, any> {\n return makeStructuredOutputCompatibleWithMap(schema, originalRequired).schema\n}\n\nexport interface StructuredOutputCompatibility {\n schema: Record<string, any>\n nullWideningMap: NullWideningMap | undefined\n}\n\ninterface CoercedStrictSchema extends StructuredOutputCompatibility {\n hasUntrackableAnyOfWidening: boolean\n}\n\n/**\n * Strict-schema conversion plus an exact map of the nullability introduced by\n * that conversion. Consumers can pass provider output through\n * `undoNullWidening` before validating it against the original schema.\n */\nexport function makeStructuredOutputCompatibleWithMap(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n): StructuredOutputCompatibility {\n const { schema: strictSchema, nullWideningMap } = coerceStrictSchema(\n schema,\n originalRequired,\n )\n return {\n schema: stripUnsupportedFormats(strictSchema),\n nullWideningMap,\n }\n}\n\n/**\n * JSON-Schema keywords outside OpenAI's strict Structured Outputs subset. A\n * schema using any of these can't be coerced into a strict-valid shape, and\n * sending it with `strict: true` makes the API reject the ENTIRE request\n * (e.g. `400 Invalid schema ... 'additionalProperties' is required to be ...`).\n * Tools with such schemas are emitted with `strict: false` instead (see the\n * tool converters) so they remain callable. MCP servers (e.g. Notion) routinely\n * emit these.\n *\n * - `oneOf` / `allOf` / `not` — combinator keywords strict mode rejects\n * - `prefixItems` — 2020-12 tuple keyword. openai-node's strict transform\n * rejects it, so we send those tools with `strict: false` instead\n * - `$ref` / `$defs` / `definitions` — references and definition pools whose\n * object subschemas escape the `additionalProperties: false` normalization\n * strict mode requires\n */\nconst STRICT_UNSUPPORTED_KEYWORDS: ReadonlyArray<string> = [\n 'oneOf',\n 'allOf',\n 'not',\n 'prefixItems',\n '$ref',\n '$defs',\n 'definitions',\n]\n\n/**\n * Keys that give a schema node a resolvable type under OpenAI's strict subset.\n * A schema-position node carrying none of these is *typeless* (e.g. the empty\n * `{}` that `z.any()` / `z.unknown()` emit). Strict mode requires every schema\n * to declare a type, so a typeless node 400s the whole request — such tools\n * must be sent with `strict: false` instead. (`oneOf`/`allOf`/`$ref` count as\n * type indicators here even though they're independently strict-unsupported;\n * the keyword check below already rejects them.)\n */\nconst TYPE_INDICATOR_KEYWORDS: ReadonlyArray<string> = [\n 'type',\n 'enum',\n 'const',\n 'anyOf',\n 'oneOf',\n 'allOf',\n '$ref',\n]\n\n/**\n * Returns `false` when `schema` cannot be made strict-compatible and must be\n * sent with `strict: false`. Two ways that happens:\n *\n * 1. It uses a JSON-Schema keyword outside OpenAI's strict subset anywhere in\n * the tree (`oneOf`/`allOf`/`not`/`prefixItems`/`$ref`/`$defs`).\n * 2. It contains a *typeless* schema node — a property/items/anyOf entry with\n * no `type` (nor `enum`/`const`/combinator), e.g. the `{}` that `z.any()`\n * produces. Strict mode rejects typeless schemas.\n * 3. It contains an open object schema. OpenAI strict mode requires objects to\n * set `additionalProperties: false`, which would change the semantics of a\n * free-form map rather than merely normalizing it.\n * 4. An `anyOf` variant itself needs null widening. The inverse map is\n * intentionally schema-blind, so it cannot select a variant without risking\n * removal of a genuine nullable value accepted by another variant.\n *\n * Conservative by design: for (1) keywords are matched as object keys, so a\n * property literally named e.g. `oneOf` also trips it. That only costs that one\n * tool its strict mode, which is strictly safer than a false \"compatible\"\n * verdict that 400s the whole request.\n */\nexport function isStrictModeCompatible(schema: unknown): boolean {\n return (\n !containsStrictUnsupportedKeyword(schema) &&\n !containsTypelessSchema(schema) &&\n !containsOpenObject(schema) &&\n !containsUntrackableAnyOfWidening(schema)\n )\n}\n\n/**\n * Reports strict conversions whose synthesized nulls cannot be represented by\n * the schema-blind inverse map. Optional `anyOf` wrappers remain supported:\n * only widening introduced inside one of their variants triggers fallback.\n */\nfunction containsUntrackableAnyOfWidening(schema: unknown): boolean {\n if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {\n return false\n }\n return coerceStrictSchema(schema as Record<string, any>)\n .hasUntrackableAnyOfWidening\n}\n\n/**\n * Reports object schemas that cannot be closed without changing their input\n * semantics. Objects with `properties` and no explicit\n * `additionalProperties` are safe because `coerceStrictSchema` closes them.\n */\nfunction containsOpenObject(node: unknown): boolean {\n if (Array.isArray(node)) {\n return node.some(containsOpenObject)\n }\n if (node === null || typeof node !== 'object') return false\n\n const schema = node as Record<string, unknown>\n const type = schema['type']\n const isObjectSchema =\n type === 'object' || (Array.isArray(type) && type.includes('object'))\n\n if (isObjectSchema) {\n if (\n 'additionalProperties' in schema &&\n schema['additionalProperties'] !== false\n ) {\n return true\n }\n\n const properties = schema['properties']\n const hasProperties =\n properties !== null &&\n typeof properties === 'object' &&\n !Array.isArray(properties)\n if (!hasProperties && schema['additionalProperties'] !== false) {\n return true\n }\n }\n\n return Object.values(schema).some(containsOpenObject)\n}\n\nfunction containsStrictUnsupportedKeyword(node: unknown): boolean {\n if (Array.isArray(node)) {\n return node.some(containsStrictUnsupportedKeyword)\n }\n if (node === null || typeof node !== 'object') return false\n for (const [key, value] of Object.entries(node)) {\n if (STRICT_UNSUPPORTED_KEYWORDS.includes(key)) return true\n if (containsStrictUnsupportedKeyword(value)) return true\n }\n return false\n}\n\n/** A schema-position node that declares no type and so 400s strict mode. */\nfunction isTypelessSchema(node: unknown): boolean {\n if (node === null || typeof node !== 'object' || Array.isArray(node)) {\n // JSON Schema permits bare boolean nodes; malformed inputs may contain\n // other primitives. OpenAI's strict subset requires a declared type, so\n // preserve the containing tool by sending it in non-strict mode.\n return true\n }\n return !TYPE_INDICATOR_KEYWORDS.some((key) => key in node)\n}\n\n/**\n * Walks the genuine schema positions (property values, `items`, `anyOf`\n * variants) and reports whether any is typeless. Unlike the keyword walk this\n * must respect structure: an empty `{}` is only a problem at a schema position,\n * not e.g. an empty `properties` map.\n */\nfunction containsTypelessSchema(node: unknown): boolean {\n if (node === null || typeof node !== 'object' || Array.isArray(node)) {\n return false\n }\n const schema = node as Record<string, any>\n\n const children: Array<unknown> = []\n if (schema.properties && typeof schema.properties === 'object') {\n children.push(...Object.values(schema.properties))\n }\n if (schema.items !== undefined) {\n children.push(\n ...(Array.isArray(schema.items) ? schema.items : [schema.items]),\n )\n }\n if (Array.isArray(schema.anyOf)) {\n children.push(...schema.anyOf)\n }\n\n return children.some(\n (child) => isTypelessSchema(child) || containsTypelessSchema(child),\n )\n}\n\n/**\n * Strict-mode structural rewrite (required widening, nullability,\n * additionalProperties). Kept private so the public entry point can apply the\n * format-stripping pass exactly once over the fully-rewritten tree.\n */\nfunction pruneMap(map: NullWideningMap): NullWideningMap | undefined {\n return Object.keys(map).length > 0 ? map : undefined\n}\n\nfunction isSchemaObject(schema: unknown): schema is Record<string, any> {\n return typeof schema === 'object' && schema !== null && !Array.isArray(schema)\n}\n\n/** Whether every active JSON Schema constraint at this node admits null. */\nfunction acceptsNull(schema: unknown): boolean {\n if (schema === true) return true\n if (!isSchemaObject(schema)) return false\n\n if ('const' in schema && schema.const !== null) return false\n if (Array.isArray(schema.enum) && !schema.enum.includes(null)) return false\n\n if (typeof schema.type === 'string' && schema.type !== 'null') return false\n if (Array.isArray(schema.type) && !schema.type.includes('null')) return false\n\n if (\n Array.isArray(schema.anyOf) &&\n !schema.anyOf.some((variant: unknown) => acceptsNull(variant))\n ) {\n return false\n }\n\n return true\n}\n\nfunction coerceStrictSchema(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n): CoercedStrictSchema {\n const result = { ...schema }\n const nullWideningMap: NullWideningMap = {}\n let hasUntrackableAnyOfWidening = false\n const required =\n originalRequired ??\n (Array.isArray(result['required']) ? result['required'] : [])\n\n if (result.type === 'object' && result.properties) {\n const properties = { ...result.properties }\n const allPropertyNames = Object.keys(properties)\n const propertyMaps: Record<string, NullWideningMap> = {}\n\n for (const propName of allPropertyNames) {\n let prop = properties[propName]\n const wasOptional = !required.includes(propName)\n let childMap: NullWideningMap | undefined\n let widenedHere = false\n\n // Step 1: Recurse into nested structures\n if (isSchemaObject(prop) && prop.type === 'object' && prop.properties) {\n const nested = coerceStrictSchema(prop, prop.required || [])\n prop = nested.schema\n childMap = nested.nullWideningMap\n hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening\n } else if (isSchemaObject(prop) && prop.type === 'array') {\n const nested = coerceStrictSchema(prop, [])\n prop = nested.schema\n childMap = nested.nullWideningMap\n hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening\n } else if (isSchemaObject(prop) && prop.anyOf) {\n const nested = coerceStrictSchema(prop, prop.required || [])\n prop = nested.schema\n childMap = nested.nullWideningMap\n hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening\n } else if (isSchemaObject(prop) && prop.oneOf) {\n throw new Error(\n 'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',\n )\n }\n\n // Step 2: Apply null-widening for optional properties (after recursion)\n if (wasOptional) {\n const originallyAcceptedNull = acceptsNull(prop)\n\n // `type: [..., 'null']` alone does not make null valid when an enum or\n // const still excludes it; strict decoding would be forced to emit the\n // original literal instead of the synthetic omission marker.\n if (isSchemaObject(prop) && 'const' in prop && prop.const !== null) {\n const { const: constValue, ...withoutConst } = prop\n prop = { ...withoutConst, enum: [constValue, null] }\n } else if (\n isSchemaObject(prop) &&\n Array.isArray(prop.enum) &&\n !prop.enum.includes(null)\n ) {\n prop = { ...prop, enum: [...prop.enum, null] }\n }\n\n if (isSchemaObject(prop) && prop.anyOf) {\n // A genuine null branch can use type, enum, or const. Only add a\n // provider omission marker when the original union rejected null.\n if (!acceptsNull(prop)) {\n prop = { ...prop, anyOf: [...prop.anyOf, { type: 'null' }] }\n }\n } else if (\n isSchemaObject(prop) &&\n prop.type &&\n !Array.isArray(prop.type)\n ) {\n prop = { ...prop, type: [prop.type, 'null'] }\n } else if (\n isSchemaObject(prop) &&\n Array.isArray(prop.type) &&\n !prop.type.includes('null')\n ) {\n prop = { ...prop, type: [...prop.type, 'null'] }\n }\n\n widenedHere = !originallyAcceptedNull && acceptsNull(prop)\n }\n\n properties[propName] = prop\n if (childMap || widenedHere) {\n propertyMaps[propName] = {\n ...(childMap ?? {}),\n ...(widenedHere ? { widened: true } : {}),\n }\n }\n }\n\n result.properties = properties\n result.required = allPropertyNames\n result.additionalProperties = false\n if (Object.keys(propertyMaps).length > 0) {\n nullWideningMap.properties = propertyMaps\n }\n }\n\n if (result.type === 'array' && result.items) {\n if (Array.isArray(result.items)) {\n const itemMaps: Array<NullWideningMap> = []\n result.items = result.items.map((item) => {\n if (!isSchemaObject(item)) {\n itemMaps.push({})\n return item\n }\n const nested = coerceStrictSchema(item, item.required || [])\n itemMaps.push(nested.nullWideningMap ?? {})\n hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening\n return nested.schema\n })\n if (itemMaps.some((map) => Object.keys(map).length > 0)) {\n nullWideningMap.items = itemMaps\n }\n } else {\n const nested = coerceStrictSchema(\n result.items,\n result.items.required || [],\n )\n result.items = nested.schema\n if (nested.nullWideningMap) {\n nullWideningMap.items = nested.nullWideningMap\n }\n hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening\n }\n }\n\n if (result.anyOf && Array.isArray(result.anyOf)) {\n const variants = result.anyOf.map((variant) =>\n coerceStrictSchema(variant, variant.required || []),\n )\n result.anyOf = variants.map((variant) => variant.schema)\n hasUntrackableAnyOfWidening ||= variants.some(\n (variant) =>\n variant.nullWideningMap !== undefined ||\n variant.hasUntrackableAnyOfWidening,\n )\n }\n\n if (result.oneOf) {\n throw new Error(\n 'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',\n )\n }\n\n return {\n schema: result,\n nullWideningMap: pruneMap(nullWideningMap),\n hasUntrackableAnyOfWidening,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema-converter.js","names":[],"sources":["../../../src/utils/schema-converter.ts"],"sourcesContent":["import type { NullWideningMap } from '@tanstack/ai-utils'\nimport type { Tool } from '@tanstack/ai'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\n\n/**\n * String `format` values accepted by OpenAI's strict Structured Outputs subset.\n * Any other format (e.g. \"uri\", \"uri-reference\", \"regex\") causes the API to\n * reject the whole request with `400 ... '<format>' is not a valid format`.\n * MCP servers and hand-written tools routinely declare such formats, so we strip\n * the unsupported ones before sending. See:\n * https://platform.openai.com/docs/guides/structured-outputs#supported-properties\n */\nconst SUPPORTED_STRING_FORMATS = new Set([\n 'date-time',\n 'time',\n 'date',\n 'duration',\n 'email',\n 'hostname',\n 'ipv4',\n 'ipv6',\n 'uuid',\n])\n\n/**\n * Recursively drop JSON-Schema `format` keywords whose value isn't in OpenAI's\n * strict-mode allowlist. Pure — returns a fresh tree and never mutates `node`,\n * so the caller's original tool definition is left intact.\n *\n * A property *named* `format` always has a schema (object/boolean) value, never\n * a bare string, so it is preserved and recursed into; only the `format`\n * *keyword* (whose value is a string) is subject to removal.\n */\nexport function stripUnsupportedFormats(node: any): any {\n if (Array.isArray(node)) return node.map(stripUnsupportedFormats)\n if (node === null || typeof node !== 'object') return node\n\n const out: Record<string, any> = {}\n for (const [key, value] of Object.entries(node)) {\n if (\n key === 'format' &&\n typeof value === 'string' &&\n !SUPPORTED_STRING_FORMATS.has(value)\n ) {\n continue\n }\n out[key] = stripUnsupportedFormats(value)\n }\n return out\n}\n\n/**\n * Transform a JSON schema to be compatible with OpenAI's structured output requirements.\n * OpenAI requires:\n * - All properties must be in the `required` array\n * - Optional fields should have null added to their type union\n * - additionalProperties must be false for objects\n * - String `format` keywords must be from a fixed allowlist (others are stripped)\n *\n * @param schema - JSON schema to transform\n * @param originalRequired - Original required array (to know which fields were optional)\n * @returns Transformed schema compatible with OpenAI structured output\n */\nexport function makeStructuredOutputCompatible(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n): Record<string, any> {\n return makeStructuredOutputCompatibleWithMap(schema, originalRequired).schema\n}\n\nexport interface StructuredOutputCompatibility {\n schema: Record<string, any>\n nullWideningMap: NullWideningMap | undefined\n}\n\ninterface CoercedStrictSchema extends StructuredOutputCompatibility {\n hasUntrackableAnyOfWidening: boolean\n}\n\n/**\n * Strict-schema conversion plus an exact map of the nullability introduced by\n * that conversion. Consumers can pass provider output through\n * `undoNullWidening` before validating it against the original schema.\n */\nexport function makeStructuredOutputCompatibleWithMap(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n): StructuredOutputCompatibility {\n const { schema: strictSchema, nullWideningMap } = coerceStrictSchema(\n schema,\n originalRequired,\n )\n return {\n schema: stripUnsupportedFormats(strictSchema),\n nullWideningMap,\n }\n}\n\n/**\n * JSON-Schema keywords outside OpenAI's strict Structured Outputs subset. A\n * schema using any of these can't be coerced into a strict-valid shape, and\n * sending it with `strict: true` makes the API reject the ENTIRE request\n * (e.g. `400 Invalid schema ... 'additionalProperties' is required to be ...`).\n * Tools with such schemas are emitted with `strict: false` instead (see the\n * tool converters) so they remain callable. MCP servers (e.g. Notion) routinely\n * emit these.\n *\n * - `oneOf` / `allOf` / `not` — combinator keywords strict mode rejects\n * - `prefixItems` — 2020-12 tuple keyword. openai-node's strict transform\n * rejects it, so we send those tools with `strict: false` instead\n * - `$ref` / `$defs` / `definitions` — references and definition pools whose\n * object subschemas escape the `additionalProperties: false` normalization\n * strict mode requires\n */\nconst STRICT_UNSUPPORTED_KEYWORDS: ReadonlyArray<string> = [\n 'oneOf',\n 'allOf',\n 'not',\n 'prefixItems',\n '$ref',\n '$defs',\n 'definitions',\n]\n\n/**\n * Keys that give a schema node a resolvable type under OpenAI's strict subset.\n * A schema-position node carrying none of these is *typeless* (e.g. the empty\n * `{}` that `z.any()` / `z.unknown()` emit). Strict mode requires every schema\n * to declare a type, so a typeless node 400s the whole request — such tools\n * must be sent with `strict: false` instead. (`oneOf`/`allOf`/`$ref` count as\n * type indicators here even though they're independently strict-unsupported;\n * the keyword check below already rejects them.)\n */\nconst TYPE_INDICATOR_KEYWORDS: ReadonlyArray<string> = [\n 'type',\n 'enum',\n 'const',\n 'anyOf',\n 'oneOf',\n 'allOf',\n '$ref',\n]\n\n/**\n * Returns `false` when `schema` cannot be made strict-compatible and must be\n * sent with `strict: false`. Two ways that happens:\n *\n * 1. It uses a JSON-Schema keyword outside OpenAI's strict subset anywhere in\n * the tree (`oneOf`/`allOf`/`not`/`prefixItems`/`$ref`/`$defs`).\n * 2. It contains a *typeless* schema node — a property/items/anyOf entry with\n * no `type` (nor `enum`/`const`/combinator), e.g. the `{}` that `z.any()`\n * produces. Strict mode rejects typeless schemas.\n * 3. It contains an open object schema. OpenAI strict mode requires objects to\n * set `additionalProperties: false`, which would change the semantics of a\n * free-form map rather than merely normalizing it.\n * 4. An `anyOf` variant itself needs null widening. The inverse map is\n * intentionally schema-blind, so it cannot select a variant without risking\n * removal of a genuine nullable value accepted by another variant.\n *\n * Conservative by design: for (1) keywords are matched as object keys, so a\n * property literally named e.g. `oneOf` also trips it. That only costs that one\n * tool its strict mode, which is strictly safer than a false \"compatible\"\n * verdict that 400s the whole request.\n */\nexport function isStrictModeCompatible(schema: unknown): boolean {\n return strictModeFallbackReason(schema) === undefined\n}\n\n/**\n * Why `schema` must be sent with `strict: false`, or `undefined` when it can be\n * strict. Runs the same checks as `isStrictModeCompatible`, in the same order.\n */\nexport function strictModeFallbackReason(schema: unknown): string | undefined {\n const keyword = findStrictUnsupportedKeyword(schema)\n if (keyword !== undefined) {\n return `schema uses ${keyword}, which strict mode does not support`\n }\n if (containsTypelessSchema(schema)) {\n return 'schema has a node with no type (for example z.any() or z.unknown())'\n }\n if (containsOpenObject(schema)) {\n return 'schema has an open object (for example z.record())'\n }\n if (containsUntrackableAnyOfWidening(schema)) {\n return 'schema has an optional field inside an anyOf variant'\n }\n return undefined\n}\n\n/** Options that every `openai-base` text adapter accepts in its config. */\nexport interface OpenAIBaseTextAdapterOptions {\n /**\n * In development, warn once per tool that is sent with `strict: false`\n * because its schema cannot be strict. Set to `false` to turn the warning\n * off. It never runs when `NODE_ENV` is `production`. Default: `true`.\n */\n strictFallbackWarning?: boolean\n}\n\n// ponytail: keyed on the Tool object, so a tool defined once warns once per\n// process. Tools rebuilt per request (e.g. from MCP) warn once per request.\nconst warnedStrictFallback = new WeakSet<Tool>()\n\n/**\n * Warn once per tool that is sent with `strict: false` because its schema\n * cannot be strict. The tool still works, but the model is not held to the\n * schema, so the developer must know (#1213).\n */\nexport function warnStrictFallback(\n tools: Array<Tool> | undefined,\n logger: InternalLogger,\n): void {\n // Development only. `process` is absent on some runtimes (e.g. Workers).\n if (typeof process !== 'undefined' && process.env.NODE_ENV === 'production')\n return\n for (const tool of tools ?? []) {\n if (!tool.inputSchema || warnedStrictFallback.has(tool)) continue\n const reason = strictModeFallbackReason(tool.inputSchema)\n if (reason === undefined) continue\n warnedStrictFallback.add(tool)\n logger.warn(`tool \"${tool.name}\" sent with strict: false: ${reason}`, {\n tool: tool.name,\n })\n }\n}\n\n/**\n * Reports strict conversions whose synthesized nulls cannot be represented by\n * the schema-blind inverse map. Optional `anyOf` wrappers remain supported:\n * only widening introduced inside one of their variants triggers fallback.\n */\nfunction containsUntrackableAnyOfWidening(schema: unknown): boolean {\n if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {\n return false\n }\n return coerceStrictSchema(schema as Record<string, any>)\n .hasUntrackableAnyOfWidening\n}\n\n/**\n * Reports object schemas that cannot be closed without changing their input\n * semantics. Objects with `properties` and no explicit\n * `additionalProperties` are safe because `coerceStrictSchema` closes them.\n */\nfunction containsOpenObject(node: unknown): boolean {\n if (Array.isArray(node)) {\n return node.some(containsOpenObject)\n }\n if (node === null || typeof node !== 'object') return false\n\n const schema = node as Record<string, unknown>\n const type = schema['type']\n const isObjectSchema =\n type === 'object' || (Array.isArray(type) && type.includes('object'))\n\n if (isObjectSchema) {\n if (\n 'additionalProperties' in schema &&\n schema['additionalProperties'] !== false\n ) {\n return true\n }\n\n const properties = schema['properties']\n const hasProperties =\n properties !== null &&\n typeof properties === 'object' &&\n !Array.isArray(properties)\n if (!hasProperties && schema['additionalProperties'] !== false) {\n return true\n }\n }\n\n return Object.values(schema).some(containsOpenObject)\n}\n\nfunction findStrictUnsupportedKeyword(node: unknown): string | undefined {\n if (Array.isArray(node)) {\n for (const item of node) {\n const found = findStrictUnsupportedKeyword(item)\n if (found !== undefined) return found\n }\n return undefined\n }\n if (node === null || typeof node !== 'object') return undefined\n for (const [key, value] of Object.entries(node)) {\n if (STRICT_UNSUPPORTED_KEYWORDS.includes(key)) return key\n const found = findStrictUnsupportedKeyword(value)\n if (found !== undefined) return found\n }\n return undefined\n}\n\n/** A schema-position node that declares no type and so 400s strict mode. */\nfunction isTypelessSchema(node: unknown): boolean {\n if (node === null || typeof node !== 'object' || Array.isArray(node)) {\n // JSON Schema permits bare boolean nodes; malformed inputs may contain\n // other primitives. OpenAI's strict subset requires a declared type, so\n // preserve the containing tool by sending it in non-strict mode.\n return true\n }\n return !TYPE_INDICATOR_KEYWORDS.some((key) => key in node)\n}\n\n/**\n * Walks the genuine schema positions (property values, `items`, `anyOf`\n * variants) and reports whether any is typeless. Unlike the keyword walk this\n * must respect structure: an empty `{}` is only a problem at a schema position,\n * not e.g. an empty `properties` map.\n */\nfunction containsTypelessSchema(node: unknown): boolean {\n if (node === null || typeof node !== 'object' || Array.isArray(node)) {\n return false\n }\n const schema = node as Record<string, any>\n\n const children: Array<unknown> = []\n if (schema.properties && typeof schema.properties === 'object') {\n children.push(...Object.values(schema.properties))\n }\n if (schema.items !== undefined) {\n children.push(\n ...(Array.isArray(schema.items) ? schema.items : [schema.items]),\n )\n }\n if (Array.isArray(schema.anyOf)) {\n children.push(...schema.anyOf)\n }\n\n return children.some(\n (child) => isTypelessSchema(child) || containsTypelessSchema(child),\n )\n}\n\n/**\n * Strict-mode structural rewrite (required widening, nullability,\n * additionalProperties). Kept private so the public entry point can apply the\n * format-stripping pass exactly once over the fully-rewritten tree.\n */\nfunction pruneMap(map: NullWideningMap): NullWideningMap | undefined {\n return Object.keys(map).length > 0 ? map : undefined\n}\n\nfunction isSchemaObject(schema: unknown): schema is Record<string, any> {\n return typeof schema === 'object' && schema !== null && !Array.isArray(schema)\n}\n\n/** Whether every active JSON Schema constraint at this node admits null. */\nfunction acceptsNull(schema: unknown): boolean {\n if (schema === true) return true\n if (!isSchemaObject(schema)) return false\n\n if ('const' in schema && schema.const !== null) return false\n if (Array.isArray(schema.enum) && !schema.enum.includes(null)) return false\n\n if (typeof schema.type === 'string' && schema.type !== 'null') return false\n if (Array.isArray(schema.type) && !schema.type.includes('null')) return false\n\n if (\n Array.isArray(schema.anyOf) &&\n !schema.anyOf.some((variant: unknown) => acceptsNull(variant))\n ) {\n return false\n }\n\n return true\n}\n\nfunction coerceStrictSchema(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n): CoercedStrictSchema {\n const result = { ...schema }\n const nullWideningMap: NullWideningMap = {}\n let hasUntrackableAnyOfWidening = false\n const required =\n originalRequired ??\n (Array.isArray(result['required']) ? result['required'] : [])\n\n if (result.type === 'object' && result.properties) {\n const properties = { ...result.properties }\n const allPropertyNames = Object.keys(properties)\n const propertyMaps: Record<string, NullWideningMap> = {}\n\n for (const propName of allPropertyNames) {\n let prop = properties[propName]\n const wasOptional = !required.includes(propName)\n let childMap: NullWideningMap | undefined\n let widenedHere = false\n\n // Step 1: Recurse into nested structures\n if (isSchemaObject(prop) && prop.type === 'object' && prop.properties) {\n const nested = coerceStrictSchema(prop, prop.required || [])\n prop = nested.schema\n childMap = nested.nullWideningMap\n hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening\n } else if (isSchemaObject(prop) && prop.type === 'array') {\n const nested = coerceStrictSchema(prop, [])\n prop = nested.schema\n childMap = nested.nullWideningMap\n hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening\n } else if (isSchemaObject(prop) && prop.anyOf) {\n const nested = coerceStrictSchema(prop, prop.required || [])\n prop = nested.schema\n childMap = nested.nullWideningMap\n hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening\n } else if (isSchemaObject(prop) && prop.oneOf) {\n throw new Error(\n 'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',\n )\n }\n\n // Step 2: Apply null-widening for optional properties (after recursion)\n if (wasOptional) {\n const originallyAcceptedNull = acceptsNull(prop)\n\n // `type: [..., 'null']` alone does not make null valid when an enum or\n // const still excludes it; strict decoding would be forced to emit the\n // original literal instead of the synthetic omission marker.\n if (isSchemaObject(prop) && 'const' in prop && prop.const !== null) {\n const { const: constValue, ...withoutConst } = prop\n prop = { ...withoutConst, enum: [constValue, null] }\n } else if (\n isSchemaObject(prop) &&\n Array.isArray(prop.enum) &&\n !prop.enum.includes(null)\n ) {\n prop = { ...prop, enum: [...prop.enum, null] }\n }\n\n if (isSchemaObject(prop) && prop.anyOf) {\n // A genuine null branch can use type, enum, or const. Only add a\n // provider omission marker when the original union rejected null.\n if (!acceptsNull(prop)) {\n prop = { ...prop, anyOf: [...prop.anyOf, { type: 'null' }] }\n }\n } else if (\n isSchemaObject(prop) &&\n prop.type &&\n !Array.isArray(prop.type)\n ) {\n prop = { ...prop, type: [prop.type, 'null'] }\n } else if (\n isSchemaObject(prop) &&\n Array.isArray(prop.type) &&\n !prop.type.includes('null')\n ) {\n prop = { ...prop, type: [...prop.type, 'null'] }\n }\n\n widenedHere = !originallyAcceptedNull && acceptsNull(prop)\n }\n\n properties[propName] = prop\n if (childMap || widenedHere) {\n propertyMaps[propName] = {\n ...(childMap ?? {}),\n ...(widenedHere ? { widened: true } : {}),\n }\n }\n }\n\n result.properties = properties\n result.required = allPropertyNames\n result.additionalProperties = false\n if (Object.keys(propertyMaps).length > 0) {\n nullWideningMap.properties = propertyMaps\n }\n }\n\n if (result.type === 'array' && result.items) {\n if (Array.isArray(result.items)) {\n const itemMaps: Array<NullWideningMap> = []\n result.items = result.items.map((item) => {\n if (!isSchemaObject(item)) {\n itemMaps.push({})\n return item\n }\n const nested = coerceStrictSchema(item, item.required || [])\n itemMaps.push(nested.nullWideningMap ?? {})\n hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening\n return nested.schema\n })\n if (itemMaps.some((map) => Object.keys(map).length > 0)) {\n nullWideningMap.items = itemMaps\n }\n } else {\n const nested = coerceStrictSchema(\n result.items,\n result.items.required || [],\n )\n result.items = nested.schema\n if (nested.nullWideningMap) {\n nullWideningMap.items = nested.nullWideningMap\n }\n hasUntrackableAnyOfWidening ||= nested.hasUntrackableAnyOfWidening\n }\n }\n\n if (result.anyOf && Array.isArray(result.anyOf)) {\n const variants = result.anyOf.map((variant) =>\n coerceStrictSchema(variant, variant.required || []),\n )\n result.anyOf = variants.map((variant) => variant.schema)\n hasUntrackableAnyOfWidening ||= variants.some(\n (variant) =>\n variant.nullWideningMap !== undefined ||\n variant.hasUntrackableAnyOfWidening,\n )\n }\n\n if (result.oneOf) {\n throw new Error(\n 'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',\n )\n }\n\n return {\n schema: result,\n nullWideningMap: pruneMap(nullWideningMap),\n hasUntrackableAnyOfWidening,\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/openai-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Shared OpenAI SDK base adapters for TanStack AI providers using Chat Completions and Responses APIs.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"@tanstack/ai-utils": "^0.4.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@tanstack/ai": "^0.
|
|
50
|
+
"@tanstack/ai": "^0.64.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@vitest/coverage-v8": "4.1.10",
|
|
54
54
|
"vite": "^8.2.1",
|
|
55
55
|
"zod": "^4.2.0",
|
|
56
|
-
"@tanstack/ai": "0.
|
|
56
|
+
"@tanstack/ai": "0.64.0"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "vite build",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"lint:fix": "oxlint src --type-aware --fix",
|
|
62
62
|
"test:build": "publint --strict",
|
|
63
63
|
"test:oxlint": "oxlint src --type-aware",
|
|
64
|
+
"test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary",
|
|
64
65
|
"test:lib": "vitest run",
|
|
65
66
|
"test:lib:dev": "pnpm test:lib --watch",
|
|
66
67
|
"test:types": "tsc"
|
|
@@ -11,9 +11,15 @@ import {
|
|
|
11
11
|
} from '@tanstack/ai/adapter-internals'
|
|
12
12
|
import { generateId } from '@tanstack/ai-utils'
|
|
13
13
|
import { extractRequestOptions } from '../utils/request-options'
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
makeStructuredOutputCompatibleWithMap,
|
|
16
|
+
warnStrictFallback,
|
|
17
|
+
} from '../utils/schema-converter'
|
|
15
18
|
import { createToolInputNormalizer } from '../utils/tool-input-normalizer'
|
|
16
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
OpenAIBaseTextAdapterOptions,
|
|
21
|
+
StructuredOutputCompatibility,
|
|
22
|
+
} from '../utils/schema-converter'
|
|
17
23
|
import { buildChatCompletionsUsage } from '../usage'
|
|
18
24
|
import { convertToolsToChatCompletionsFormat } from './chat-completions-tool-converter'
|
|
19
25
|
import type OpenAI from 'openai'
|
|
@@ -67,10 +73,19 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
67
73
|
readonly name: string
|
|
68
74
|
protected client: OpenAI
|
|
69
75
|
|
|
70
|
-
|
|
76
|
+
/** See {@link OpenAIBaseTextAdapterOptions.strictFallbackWarning}. */
|
|
77
|
+
protected readonly strictFallbackWarning: boolean
|
|
78
|
+
|
|
79
|
+
constructor(
|
|
80
|
+
model: TModel,
|
|
81
|
+
name: string,
|
|
82
|
+
client: OpenAI,
|
|
83
|
+
options: OpenAIBaseTextAdapterOptions = {},
|
|
84
|
+
) {
|
|
71
85
|
super({}, model)
|
|
72
86
|
this.name = name
|
|
73
87
|
this.client = client
|
|
88
|
+
this.strictFallbackWarning = options.strictFallbackWarning ?? true
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
async *chatStream(
|
|
@@ -1220,6 +1235,9 @@ export abstract class OpenAIBaseChatCompletionsTextAdapter<
|
|
|
1220
1235
|
protected mapOptionsToRequest(
|
|
1221
1236
|
options: TextOptions,
|
|
1222
1237
|
): ChatCompletionCreateParamsStreaming {
|
|
1238
|
+
if (this.strictFallbackWarning) {
|
|
1239
|
+
warnStrictFallback(options.tools, options.logger)
|
|
1240
|
+
}
|
|
1223
1241
|
const tools = options.tools
|
|
1224
1242
|
? convertToolsToChatCompletionsFormat(
|
|
1225
1243
|
options.tools,
|