@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.
- package/dist/esm/adapters/chat-completions-text.d.ts +4 -2
- package/dist/esm/adapters/chat-completions-text.js +29 -4
- package/dist/esm/adapters/chat-completions-text.js.map +1 -1
- package/dist/esm/adapters/responses-text.d.ts +18 -6
- package/dist/esm/adapters/responses-text.js +261 -16
- package/dist/esm/adapters/responses-text.js.map +1 -1
- package/dist/esm/adapters/responses-user-tools.d.ts +42 -0
- package/dist/esm/adapters/responses-user-tools.js +241 -0
- package/dist/esm/adapters/responses-user-tools.js.map +1 -0
- 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 +74 -5
- package/src/adapters/responses-text.ts +468 -37
- package/src/adapters/responses-user-tools.ts +355 -0
- package/src/index.ts +2 -0
- package/src/usage.ts +16 -5
- package/src/utils/schema-converter.ts +72 -12
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import type { ResponseInputItem } from 'openai/resources/responses/responses'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenAI Responses tools the app must run.
|
|
5
|
+
* A container `shell` runs on the provider. `apply_patch`, `local_shell`,
|
|
6
|
+
* and `shell` with `environment.type: "local"` (or no environment) do not.
|
|
7
|
+
*/
|
|
8
|
+
export type OpenAIUserToolName = 'shell' | 'apply_patch' | 'local_shell'
|
|
9
|
+
|
|
10
|
+
export interface OpenAIUserExecutedCall {
|
|
11
|
+
name: OpenAIUserToolName
|
|
12
|
+
callId: string
|
|
13
|
+
itemId?: string
|
|
14
|
+
input: Record<string, unknown>
|
|
15
|
+
maxOutputLength?: number | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ToolCallLike {
|
|
19
|
+
id: string
|
|
20
|
+
function: { name: string; arguments: string }
|
|
21
|
+
metadata?: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stringList(value: unknown): Array<string> | null {
|
|
29
|
+
if (
|
|
30
|
+
!Array.isArray(value) ||
|
|
31
|
+
!value.every((entry) => typeof entry === 'string')
|
|
32
|
+
) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stringEnv(value: unknown): Record<string, string> {
|
|
39
|
+
if (!isRecord(value)) return {}
|
|
40
|
+
const env: Record<string, string> = {}
|
|
41
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
42
|
+
if (typeof entry === 'string') env[key] = entry
|
|
43
|
+
}
|
|
44
|
+
return env
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function nullableNumber(value: unknown): number | null {
|
|
48
|
+
return typeof value === 'number' || value === null ? value : null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hosted shell calls already include a `shell_call_output` in the same
|
|
53
|
+
* response. Those call ids must not pause the app for another run.
|
|
54
|
+
*/
|
|
55
|
+
export function hostedShellCallIds(
|
|
56
|
+
output: ReadonlyArray<unknown>,
|
|
57
|
+
): Set<string> {
|
|
58
|
+
const ids = new Set<string>()
|
|
59
|
+
for (const item of output) {
|
|
60
|
+
if (
|
|
61
|
+
isRecord(item) &&
|
|
62
|
+
item.type === 'shell_call_output' &&
|
|
63
|
+
typeof item.call_id === 'string'
|
|
64
|
+
) {
|
|
65
|
+
ids.add(item.call_id)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return ids
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function readUserToolName(metadata: unknown): OpenAIUserToolName | null {
|
|
72
|
+
if (!isRecord(metadata)) return null
|
|
73
|
+
const name = metadata.openaiUserTool
|
|
74
|
+
if (name === 'shell' || name === 'apply_patch' || name === 'local_shell') {
|
|
75
|
+
return name
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readItemId(metadata: unknown): string | undefined {
|
|
81
|
+
if (!isRecord(metadata)) return undefined
|
|
82
|
+
return typeof metadata.itemId === 'string' && metadata.itemId.length > 0
|
|
83
|
+
? metadata.itemId
|
|
84
|
+
: undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readMaxOutputLength(metadata: unknown): number | null | undefined {
|
|
88
|
+
if (!isRecord(metadata)) return undefined
|
|
89
|
+
const value = metadata.maxOutputLength
|
|
90
|
+
return typeof value === 'number' || value === null ? value : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function applyPatchOperation(
|
|
94
|
+
value: unknown,
|
|
95
|
+
):
|
|
96
|
+
| { type: 'create_file'; path: string; diff: string }
|
|
97
|
+
| { type: 'update_file'; path: string; diff: string }
|
|
98
|
+
| { type: 'delete_file'; path: string }
|
|
99
|
+
| null {
|
|
100
|
+
if (!isRecord(value) || typeof value.path !== 'string') return null
|
|
101
|
+
if (value.type === 'delete_file') {
|
|
102
|
+
return { type: 'delete_file', path: value.path }
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
(value.type === 'create_file' || value.type === 'update_file') &&
|
|
106
|
+
typeof value.diff === 'string'
|
|
107
|
+
) {
|
|
108
|
+
return { type: value.type, path: value.path, diff: value.diff }
|
|
109
|
+
}
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Read a user-run Responses output item.
|
|
115
|
+
* `bareShell` is true only once the full response is known. A shell call
|
|
116
|
+
* with no environment waits for that pass, so a hosted call that later
|
|
117
|
+
* carries `shell_call_output` is not asked of the app.
|
|
118
|
+
*/
|
|
119
|
+
export function readUserExecutedCall(
|
|
120
|
+
item: unknown,
|
|
121
|
+
options: { bareShell: boolean },
|
|
122
|
+
): OpenAIUserExecutedCall | null {
|
|
123
|
+
if (!isRecord(item) || typeof item.type !== 'string') return null
|
|
124
|
+
const callId = typeof item.call_id === 'string' ? item.call_id : ''
|
|
125
|
+
const itemId = typeof item.id === 'string' ? item.id : undefined
|
|
126
|
+
|
|
127
|
+
if (item.type === 'apply_patch_call') {
|
|
128
|
+
const operation = applyPatchOperation(item.operation)
|
|
129
|
+
if (!callId || !operation) return null
|
|
130
|
+
return {
|
|
131
|
+
name: 'apply_patch',
|
|
132
|
+
callId,
|
|
133
|
+
...(itemId ? { itemId } : {}),
|
|
134
|
+
input: { operation },
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (item.type === 'local_shell_call') {
|
|
139
|
+
if (!isRecord(item.action)) return null
|
|
140
|
+
const command = stringList(item.action.command)
|
|
141
|
+
if (!callId || !command || command.length === 0) return null
|
|
142
|
+
return {
|
|
143
|
+
name: 'local_shell',
|
|
144
|
+
callId,
|
|
145
|
+
...(itemId ? { itemId } : {}),
|
|
146
|
+
input: item.action,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (item.type !== 'shell_call') return null
|
|
151
|
+
const environment = item.environment
|
|
152
|
+
if (isRecord(environment)) {
|
|
153
|
+
if (environment.type !== 'local') return null
|
|
154
|
+
} else if (!options.bareShell) {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
if (!callId || !isRecord(item.action)) return null
|
|
158
|
+
const commands = stringList(item.action.commands)
|
|
159
|
+
if (!commands || commands.length === 0) return null
|
|
160
|
+
const maxOutputLength = nullableNumber(item.action.max_output_length)
|
|
161
|
+
return {
|
|
162
|
+
name: 'shell',
|
|
163
|
+
callId,
|
|
164
|
+
...(itemId ? { itemId } : {}),
|
|
165
|
+
input: {
|
|
166
|
+
commands,
|
|
167
|
+
max_output_length: maxOutputLength,
|
|
168
|
+
timeout_ms: nullableNumber(item.action.timeout_ms),
|
|
169
|
+
},
|
|
170
|
+
maxOutputLength,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseArguments(argumentsString: string): Record<string, unknown> {
|
|
175
|
+
try {
|
|
176
|
+
const parsed: unknown = JSON.parse(argumentsString)
|
|
177
|
+
return isRecord(parsed) ? parsed : {}
|
|
178
|
+
} catch {
|
|
179
|
+
return {}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseContent(content: unknown): unknown {
|
|
184
|
+
if (typeof content !== 'string') return content
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(content) as unknown
|
|
187
|
+
} catch {
|
|
188
|
+
return content
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Replay a user-run tool call as the Responses input item OpenAI expects. */
|
|
193
|
+
export function userToolRequestItem(
|
|
194
|
+
toolCall: ToolCallLike,
|
|
195
|
+
): ResponseInputItem | null {
|
|
196
|
+
const name = readUserToolName(toolCall.metadata)
|
|
197
|
+
if (!name) return null
|
|
198
|
+
const args = parseArguments(toolCall.function.arguments)
|
|
199
|
+
const itemId = readItemId(toolCall.metadata)
|
|
200
|
+
|
|
201
|
+
if (name === 'apply_patch') {
|
|
202
|
+
const operation = applyPatchOperation(args.operation)
|
|
203
|
+
if (!operation) return null
|
|
204
|
+
return {
|
|
205
|
+
type: 'apply_patch_call',
|
|
206
|
+
call_id: toolCall.id,
|
|
207
|
+
status: 'completed',
|
|
208
|
+
operation,
|
|
209
|
+
...(itemId ? { id: itemId } : {}),
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (name === 'local_shell') {
|
|
214
|
+
const command = stringList(args.command)
|
|
215
|
+
if (!command) return null
|
|
216
|
+
return {
|
|
217
|
+
type: 'local_shell_call',
|
|
218
|
+
id: itemId ?? toolCall.id,
|
|
219
|
+
call_id: toolCall.id,
|
|
220
|
+
status: 'completed',
|
|
221
|
+
action: {
|
|
222
|
+
type: 'exec',
|
|
223
|
+
command,
|
|
224
|
+
env: stringEnv(args.env),
|
|
225
|
+
timeout_ms: nullableNumber(args.timeout_ms),
|
|
226
|
+
...(typeof args.user === 'string' || args.user === null
|
|
227
|
+
? { user: args.user }
|
|
228
|
+
: {}),
|
|
229
|
+
...(typeof args.working_directory === 'string' ||
|
|
230
|
+
args.working_directory === null
|
|
231
|
+
? { working_directory: args.working_directory }
|
|
232
|
+
: {}),
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const commands = stringList(args.commands)
|
|
238
|
+
if (!commands) return null
|
|
239
|
+
return {
|
|
240
|
+
type: 'shell_call',
|
|
241
|
+
call_id: toolCall.id,
|
|
242
|
+
status: 'completed',
|
|
243
|
+
action: {
|
|
244
|
+
commands,
|
|
245
|
+
max_output_length: nullableNumber(args.max_output_length),
|
|
246
|
+
timeout_ms: nullableNumber(args.timeout_ms),
|
|
247
|
+
},
|
|
248
|
+
...(itemId ? { id: itemId } : {}),
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
type ShellOutcome = { type: 'exit'; exit_code: number } | { type: 'timeout' }
|
|
253
|
+
|
|
254
|
+
function shellOutcome(value: unknown): ShellOutcome {
|
|
255
|
+
if (isRecord(value) && value.type === 'timeout') return { type: 'timeout' }
|
|
256
|
+
const exitCode =
|
|
257
|
+
isRecord(value) && typeof value.exit_code === 'number' ? value.exit_code : 0
|
|
258
|
+
return { type: 'exit', exit_code: exitCode }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function shellEntry(value: unknown): {
|
|
262
|
+
stdout: string
|
|
263
|
+
stderr: string
|
|
264
|
+
outcome: ShellOutcome
|
|
265
|
+
} {
|
|
266
|
+
if (typeof value === 'string') {
|
|
267
|
+
return {
|
|
268
|
+
stdout: value,
|
|
269
|
+
stderr: '',
|
|
270
|
+
outcome: { type: 'exit', exit_code: 0 },
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const record = isRecord(value) ? value : {}
|
|
274
|
+
return {
|
|
275
|
+
stdout: typeof record.stdout === 'string' ? record.stdout : '',
|
|
276
|
+
stderr: typeof record.stderr === 'string' ? record.stderr : '',
|
|
277
|
+
outcome: shellOutcome(record.outcome),
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function shellOutputList(content: unknown): Array<{
|
|
282
|
+
stdout: string
|
|
283
|
+
stderr: string
|
|
284
|
+
outcome: ShellOutcome
|
|
285
|
+
}> {
|
|
286
|
+
const parsed = parseContent(content)
|
|
287
|
+
if (isRecord(parsed) && Array.isArray(parsed.output)) {
|
|
288
|
+
return parsed.output.map((entry) => shellEntry(entry))
|
|
289
|
+
}
|
|
290
|
+
if (isRecord(parsed) && ('stdout' in parsed || 'outcome' in parsed)) {
|
|
291
|
+
return [shellEntry(parsed)]
|
|
292
|
+
}
|
|
293
|
+
if (typeof parsed === 'string') return [shellEntry(parsed)]
|
|
294
|
+
return [shellEntry(JSON.stringify(parsed ?? ''))]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Replay the app's tool result as the matching Responses output item. */
|
|
298
|
+
export function userToolResultItem(
|
|
299
|
+
toolCall: ToolCallLike,
|
|
300
|
+
content: unknown,
|
|
301
|
+
): ResponseInputItem | null {
|
|
302
|
+
const name = readUserToolName(toolCall.metadata)
|
|
303
|
+
if (!name) return null
|
|
304
|
+
const parsed = parseContent(content)
|
|
305
|
+
|
|
306
|
+
if (name === 'apply_patch') {
|
|
307
|
+
const record = isRecord(parsed) ? parsed : {}
|
|
308
|
+
const failed =
|
|
309
|
+
record.status === 'failed' ||
|
|
310
|
+
(record.status !== 'completed' && typeof record.error === 'string')
|
|
311
|
+
const output =
|
|
312
|
+
typeof record.output === 'string'
|
|
313
|
+
? record.output
|
|
314
|
+
: typeof record.error === 'string'
|
|
315
|
+
? record.error
|
|
316
|
+
: typeof parsed === 'string'
|
|
317
|
+
? parsed
|
|
318
|
+
: undefined
|
|
319
|
+
return {
|
|
320
|
+
type: 'apply_patch_call_output',
|
|
321
|
+
call_id: toolCall.id,
|
|
322
|
+
status: failed ? 'failed' : 'completed',
|
|
323
|
+
...(output !== undefined ? { output } : {}),
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (name === 'local_shell') {
|
|
328
|
+
const output =
|
|
329
|
+
typeof parsed === 'string'
|
|
330
|
+
? parsed
|
|
331
|
+
: isRecord(parsed) && typeof parsed.output === 'string'
|
|
332
|
+
? parsed.output
|
|
333
|
+
: JSON.stringify(parsed ?? '')
|
|
334
|
+
return {
|
|
335
|
+
type: 'local_shell_call_output',
|
|
336
|
+
id: toolCall.id,
|
|
337
|
+
output,
|
|
338
|
+
status: 'completed',
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const record = isRecord(parsed) ? parsed : {}
|
|
343
|
+
const fromResult = nullableNumber(record.max_output_length)
|
|
344
|
+
const fromCall = readMaxOutputLength(toolCall.metadata)
|
|
345
|
+
const maxOutputLength =
|
|
346
|
+
record.max_output_length !== undefined ? fromResult : fromCall
|
|
347
|
+
return {
|
|
348
|
+
type: 'shell_call_output',
|
|
349
|
+
call_id: toolCall.id,
|
|
350
|
+
output: shellOutputList(content),
|
|
351
|
+
...(maxOutputLength !== undefined
|
|
352
|
+
? { max_output_length: maxOutputLength }
|
|
353
|
+
: {}),
|
|
354
|
+
}
|
|
355
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export {
|
|
2
2
|
makeStructuredOutputCompatible,
|
|
3
3
|
makeStructuredOutputCompatibleWithMap,
|
|
4
|
+
warnStrictFallback,
|
|
4
5
|
} from './utils/schema-converter'
|
|
6
|
+
export type { OpenAIBaseTextAdapterOptions } from './utils/schema-converter'
|
|
5
7
|
export {
|
|
6
8
|
buildChatCompletionsUsage,
|
|
7
9
|
buildResponsesUsage,
|
package/src/usage.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type OpenAI from 'openai'
|
|
|
8
8
|
*
|
|
9
9
|
* Shared by every provider that routes through
|
|
10
10
|
* {@link OpenAIBaseChatCompletionsTextAdapter} (OpenAI Chat Completions, Grok,
|
|
11
|
-
* Groq). Surfaces
|
|
12
|
-
* the provider reports them. Returns `undefined` when the provider reported no
|
|
11
|
+
* Groq). Surfaces cache read/write prompt tokens and reasoning/audio detail
|
|
12
|
+
* tokens when the provider reports them. Returns `undefined` when the provider reported no
|
|
13
13
|
* usage object, so callers omit the field rather than fabricating zeroed totals.
|
|
14
14
|
*/
|
|
15
15
|
export function buildChatCompletionsUsage(
|
|
@@ -33,10 +33,21 @@ export function buildChatCompletionsUsage(
|
|
|
33
33
|
: {}),
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// Moonshot (Kimi) also reports `cache_write_tokens` under
|
|
37
|
+
// `prompt_tokens_details`, and `cached_tokens` at the root of `usage`.
|
|
38
|
+
// The OpenAI SDK types have neither field.
|
|
39
|
+
const promptDetails = usage.prompt_tokens_details as
|
|
40
|
+
| (OpenAI.Completions.CompletionUsage.PromptTokensDetails & {
|
|
41
|
+
cache_write_tokens?: number
|
|
42
|
+
})
|
|
43
|
+
| undefined
|
|
44
|
+
const cachedTokens =
|
|
45
|
+
promptDetails?.cached_tokens ||
|
|
46
|
+
(usage as { cached_tokens?: number }).cached_tokens
|
|
37
47
|
const promptTokensDetails = {
|
|
38
|
-
...(
|
|
39
|
-
|
|
48
|
+
...(cachedTokens ? { cachedTokens } : {}),
|
|
49
|
+
...(promptDetails?.cache_write_tokens
|
|
50
|
+
? { cacheWriteTokens: promptDetails.cache_write_tokens }
|
|
40
51
|
: {}),
|
|
41
52
|
...(promptDetails?.audio_tokens
|
|
42
53
|
? { audioTokens: promptDetails.audio_tokens }
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { NullWideningMap } from '@tanstack/ai-utils'
|
|
2
|
+
import type { Tool } from '@tanstack/ai'
|
|
3
|
+
import type { InternalLogger } from '@tanstack/ai/adapter-internals'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* String `format` values accepted by OpenAI's strict Structured Outputs subset.
|
|
@@ -161,12 +163,65 @@ const TYPE_INDICATOR_KEYWORDS: ReadonlyArray<string> = [
|
|
|
161
163
|
* verdict that 400s the whole request.
|
|
162
164
|
*/
|
|
163
165
|
export function isStrictModeCompatible(schema: unknown): boolean {
|
|
164
|
-
return (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
return strictModeFallbackReason(schema) === undefined
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Why `schema` must be sent with `strict: false`, or `undefined` when it can be
|
|
171
|
+
* strict. Runs the same checks as `isStrictModeCompatible`, in the same order.
|
|
172
|
+
*/
|
|
173
|
+
export function strictModeFallbackReason(schema: unknown): string | undefined {
|
|
174
|
+
const keyword = findStrictUnsupportedKeyword(schema)
|
|
175
|
+
if (keyword !== undefined) {
|
|
176
|
+
return `schema uses ${keyword}, which strict mode does not support`
|
|
177
|
+
}
|
|
178
|
+
if (containsTypelessSchema(schema)) {
|
|
179
|
+
return 'schema has a node with no type (for example z.any() or z.unknown())'
|
|
180
|
+
}
|
|
181
|
+
if (containsOpenObject(schema)) {
|
|
182
|
+
return 'schema has an open object (for example z.record())'
|
|
183
|
+
}
|
|
184
|
+
if (containsUntrackableAnyOfWidening(schema)) {
|
|
185
|
+
return 'schema has an optional field inside an anyOf variant'
|
|
186
|
+
}
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Options that every `openai-base` text adapter accepts in its config. */
|
|
191
|
+
export interface OpenAIBaseTextAdapterOptions {
|
|
192
|
+
/**
|
|
193
|
+
* In development, warn once per tool that is sent with `strict: false`
|
|
194
|
+
* because its schema cannot be strict. Set to `false` to turn the warning
|
|
195
|
+
* off. It never runs when `NODE_ENV` is `production`. Default: `true`.
|
|
196
|
+
*/
|
|
197
|
+
strictFallbackWarning?: boolean
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ponytail: keyed on the Tool object, so a tool defined once warns once per
|
|
201
|
+
// process. Tools rebuilt per request (e.g. from MCP) warn once per request.
|
|
202
|
+
const warnedStrictFallback = new WeakSet<Tool>()
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Warn once per tool that is sent with `strict: false` because its schema
|
|
206
|
+
* cannot be strict. The tool still works, but the model is not held to the
|
|
207
|
+
* schema, so the developer must know (#1213).
|
|
208
|
+
*/
|
|
209
|
+
export function warnStrictFallback(
|
|
210
|
+
tools: Array<Tool> | undefined,
|
|
211
|
+
logger: InternalLogger,
|
|
212
|
+
): void {
|
|
213
|
+
// Development only. `process` is absent on some runtimes (e.g. Workers).
|
|
214
|
+
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'production')
|
|
215
|
+
return
|
|
216
|
+
for (const tool of tools ?? []) {
|
|
217
|
+
if (!tool.inputSchema || warnedStrictFallback.has(tool)) continue
|
|
218
|
+
const reason = strictModeFallbackReason(tool.inputSchema)
|
|
219
|
+
if (reason === undefined) continue
|
|
220
|
+
warnedStrictFallback.add(tool)
|
|
221
|
+
logger.warn(`tool "${tool.name}" sent with strict: false: ${reason}`, {
|
|
222
|
+
tool: tool.name,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
170
225
|
}
|
|
171
226
|
|
|
172
227
|
/**
|
|
@@ -219,16 +274,21 @@ function containsOpenObject(node: unknown): boolean {
|
|
|
219
274
|
return Object.values(schema).some(containsOpenObject)
|
|
220
275
|
}
|
|
221
276
|
|
|
222
|
-
function
|
|
277
|
+
function findStrictUnsupportedKeyword(node: unknown): string | undefined {
|
|
223
278
|
if (Array.isArray(node)) {
|
|
224
|
-
|
|
279
|
+
for (const item of node) {
|
|
280
|
+
const found = findStrictUnsupportedKeyword(item)
|
|
281
|
+
if (found !== undefined) return found
|
|
282
|
+
}
|
|
283
|
+
return undefined
|
|
225
284
|
}
|
|
226
|
-
if (node === null || typeof node !== 'object') return
|
|
285
|
+
if (node === null || typeof node !== 'object') return undefined
|
|
227
286
|
for (const [key, value] of Object.entries(node)) {
|
|
228
|
-
if (STRICT_UNSUPPORTED_KEYWORDS.includes(key)) return
|
|
229
|
-
|
|
287
|
+
if (STRICT_UNSUPPORTED_KEYWORDS.includes(key)) return key
|
|
288
|
+
const found = findStrictUnsupportedKeyword(value)
|
|
289
|
+
if (found !== undefined) return found
|
|
230
290
|
}
|
|
231
|
-
return
|
|
291
|
+
return undefined
|
|
232
292
|
}
|
|
233
293
|
|
|
234
294
|
/** A schema-position node that declares no type and so 400s strict mode. */
|