@tanstack/openai-base 0.10.1 → 0.10.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.
Files changed (29) hide show
  1. package/README.md +2 -2
  2. package/dist/esm/adapters/chat-completions-text.d.ts +9 -1
  3. package/dist/esm/adapters/chat-completions-text.js +16 -5
  4. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  5. package/dist/esm/adapters/chat-completions-tool-converter.test.d.ts +1 -0
  6. package/dist/esm/adapters/responses-text.d.ts +9 -1
  7. package/dist/esm/adapters/responses-text.js +41 -6
  8. package/dist/esm/adapters/responses-text.js.map +1 -1
  9. package/dist/esm/adapters/responses-tool-converter.test.d.ts +1 -0
  10. package/dist/esm/index.d.ts +1 -1
  11. package/dist/esm/index.js +2 -2
  12. package/dist/esm/tools/function-tool.test.d.ts +1 -0
  13. package/dist/esm/utils/schema-converter.d.ts +14 -0
  14. package/dist/esm/utils/schema-converter.js +106 -18
  15. package/dist/esm/utils/schema-converter.js.map +1 -1
  16. package/dist/esm/utils/tool-input-normalizer.d.ts +12 -0
  17. package/dist/esm/utils/tool-input-normalizer.js +36 -0
  18. package/dist/esm/utils/tool-input-normalizer.js.map +1 -0
  19. package/dist/esm/utils/tool-input-normalizer.test.d.ts +1 -0
  20. package/package.json +3 -3
  21. package/src/adapters/chat-completions-text.ts +32 -6
  22. package/src/adapters/chat-completions-tool-converter.test.ts +64 -0
  23. package/src/adapters/responses-text.ts +81 -8
  24. package/src/adapters/responses-tool-converter.test.ts +69 -0
  25. package/src/index.ts +4 -1
  26. package/src/tools/function-tool.test.ts +62 -0
  27. package/src/utils/schema-converter.ts +165 -20
  28. package/src/utils/tool-input-normalizer.test.ts +146 -0
  29. package/src/utils/tool-input-normalizer.ts +55 -0
@@ -0,0 +1,146 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createToolInputNormalizer } from './tool-input-normalizer'
3
+ import type { Tool } from '@tanstack/ai'
4
+
5
+ describe('createToolInputNormalizer', () => {
6
+ it('removes a null synthesized for an optional enum', () => {
7
+ const tool: Tool = {
8
+ name: 'optional_enum',
9
+ description: 'Uses an optional literal value',
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {
13
+ value: { type: 'string', enum: ['canary'] },
14
+ },
15
+ required: [],
16
+ },
17
+ }
18
+ const normalize = createToolInputNormalizer([tool])
19
+
20
+ expect(normalize(tool.name, { value: null })).toEqual({})
21
+ })
22
+
23
+ it('leaves non-strict tool inputs unchanged', () => {
24
+ const tool: Tool = {
25
+ name: 'non_strict',
26
+ description: 'Uses a schema outside the strict subset',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: { optional: { type: 'string' } },
30
+ oneOf: [{ required: ['optional'] }, { required: [] }],
31
+ },
32
+ }
33
+ const normalize = createToolInputNormalizer([tool])
34
+ const input = { optional: null }
35
+
36
+ expect(normalize(tool.name, input)).toBe(input)
37
+ })
38
+
39
+ it('does not guess when public tool names are ambiguous', () => {
40
+ const tools: Array<Tool> = [
41
+ {
42
+ name: 'duplicate',
43
+ description: 'First tool',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: { firstOptional: { type: 'string' } },
47
+ },
48
+ },
49
+ {
50
+ name: 'duplicate',
51
+ description: 'Second tool',
52
+ inputSchema: {
53
+ type: 'object',
54
+ properties: { secondOptional: { type: 'string' } },
55
+ },
56
+ },
57
+ ]
58
+ const normalize = createToolInputNormalizer(tools)
59
+ const input = { firstOptional: null, secondOptional: null }
60
+
61
+ expect(normalize('duplicate', input)).toBe(input)
62
+ })
63
+
64
+ it('leaves anyOf inputs unchanged when variant widening is ambiguous', () => {
65
+ const tool: Tool = {
66
+ name: 'union',
67
+ description: 'Uses variant-specific nullability',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {
71
+ value: {
72
+ anyOf: [
73
+ {
74
+ type: 'object',
75
+ properties: {
76
+ kind: { const: 'optional' },
77
+ note: { type: 'string' },
78
+ },
79
+ required: ['kind'],
80
+ },
81
+ {
82
+ type: 'object',
83
+ properties: {
84
+ kind: { const: 'nullable' },
85
+ note: { type: ['string', 'null'] },
86
+ },
87
+ required: ['kind', 'note'],
88
+ },
89
+ ],
90
+ },
91
+ },
92
+ required: ['value'],
93
+ },
94
+ }
95
+ const normalize = createToolInputNormalizer([tool])
96
+ const input = { value: { kind: 'nullable', note: null } }
97
+
98
+ expect(normalize(tool.name, input)).toBe(input)
99
+ })
100
+
101
+ it.each([
102
+ ['const', { anyOf: [{ type: 'string' }, { const: null }] }],
103
+ ['enum', { anyOf: [{ type: 'string' }, { enum: [null] }] }],
104
+ ])('preserves genuine null accepted by an anyOf %s branch', (_, value) => {
105
+ const tool: Tool = {
106
+ name: 'nullable_union',
107
+ description: 'Uses an optional union that genuinely accepts null',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: { value },
111
+ required: [],
112
+ },
113
+ }
114
+ const normalize = createToolInputNormalizer([tool])
115
+ const input = { value: null }
116
+
117
+ expect(normalize(tool.name, input)).toBe(input)
118
+ })
119
+
120
+ it('builds the inverse map from the supplied converter', () => {
121
+ const tool: Tool = {
122
+ name: 'ask',
123
+ description: 'Ask',
124
+ inputSchema: {
125
+ type: 'object',
126
+ properties: {
127
+ note: { type: 'string' },
128
+ },
129
+ required: ['note'],
130
+ },
131
+ }
132
+
133
+ const defaultNormalize = createToolInputNormalizer([tool])
134
+ expect(defaultNormalize(tool.name, { note: null })).toEqual({ note: null })
135
+
136
+ const normalize = createToolInputNormalizer([tool], () => ({
137
+ schema: {
138
+ type: 'object',
139
+ properties: { note: { type: ['string', 'null'] } },
140
+ required: ['note'],
141
+ },
142
+ nullWideningMap: { properties: { note: { widened: true } } },
143
+ }))
144
+ expect(normalize(tool.name, { note: null })).toEqual({})
145
+ })
146
+ })
@@ -0,0 +1,55 @@
1
+ import { undoNullWidening } from '@tanstack/ai-utils'
2
+ import {
3
+ isStrictModeCompatible,
4
+ makeStructuredOutputCompatibleWithMap,
5
+ } from './schema-converter'
6
+ import type { JSONSchema, Tool } from '@tanstack/ai'
7
+ import type { NullWideningMap } from '@tanstack/ai-utils'
8
+ import type { StructuredOutputCompatibility } from './schema-converter'
9
+
10
+ type ToolInputNormalizer = (toolName: string, input: unknown) => unknown
11
+
12
+ type SchemaConverterWithMap = (
13
+ schema: Record<string, any>,
14
+ originalRequired?: Array<string>,
15
+ ) => StructuredOutputCompatibility
16
+
17
+ /**
18
+ * Build the inverse transform for the strict tool schemas sent in one request.
19
+ * Pass the same converter the request used so subclass schema tweaks stay
20
+ * aligned with undo. Non-strict tools are excluded because they were not
21
+ * null-widened on the wire.
22
+ */
23
+ export function createToolInputNormalizer(
24
+ tools: Array<Tool> | undefined,
25
+ convertSchema: SchemaConverterWithMap = makeStructuredOutputCompatibleWithMap,
26
+ ): ToolInputNormalizer {
27
+ const maps = new Map<string, NullWideningMap>()
28
+ const seenNames = new Set<string>()
29
+ const ambiguousNames = new Set<string>()
30
+
31
+ for (const tool of tools ?? []) {
32
+ if (ambiguousNames.has(tool.name)) continue
33
+ if (seenNames.has(tool.name)) {
34
+ maps.delete(tool.name)
35
+ ambiguousNames.add(tool.name)
36
+ continue
37
+ }
38
+ seenNames.add(tool.name)
39
+
40
+ const inputSchema = (tool.inputSchema ?? {
41
+ type: 'object',
42
+ properties: {},
43
+ required: [],
44
+ }) as JSONSchema
45
+ if (!isStrictModeCompatible(inputSchema)) continue
46
+
47
+ const { nullWideningMap } = convertSchema(
48
+ inputSchema,
49
+ inputSchema.required || [],
50
+ )
51
+ if (nullWideningMap) maps.set(tool.name, nullWideningMap)
52
+ }
53
+
54
+ return (toolName, input) => undoNullWidening(input, maps.get(toolName))
55
+ }