@ai-sdk/anthropic 4.0.92 → 4.0.94
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/CHANGELOG.md +19 -0
- package/dist/index.js +556 -237
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +17 -35
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +556 -237
- package/dist/internal/index.js.map +1 -1
- package/docs/05-anthropic.mdx +23 -8
- package/package.json +3 -3
- package/src/anthropic-language-model.ts +124 -300
- package/src/anthropic-model-catalog.ts +406 -0
- package/src/has-circular-schema-reference.ts +115 -0
- package/src/sanitize-json-schema.ts +27 -24
- package/src/supports-native-structured-output-schema.ts +138 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ModelCapabilities,
|
|
3
|
+
ModelCapability,
|
|
4
|
+
ModelEntry,
|
|
5
|
+
ProviderOptionDefinition,
|
|
6
|
+
ProviderOptionDefinitions,
|
|
7
|
+
} from '@ai-sdk/provider';
|
|
8
|
+
import type { AnthropicLanguageModelOptions } from './anthropic-language-model-options';
|
|
9
|
+
|
|
10
|
+
type AnthropicLanguageProviderOption = keyof AnthropicLanguageModelOptions;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options accepted under `providerOptions.anthropic`. The `satisfies` check
|
|
14
|
+
* keeps this list in sync with the option schema.
|
|
15
|
+
*/
|
|
16
|
+
export const anthropicProviderOptions = {
|
|
17
|
+
language: {
|
|
18
|
+
sendReasoning: { description: 'Whether to send reasoning to the model.' },
|
|
19
|
+
structuredOutputMode: {
|
|
20
|
+
description: 'How structured outputs are generated.',
|
|
21
|
+
},
|
|
22
|
+
thinking: { description: 'Extended thinking configuration.' },
|
|
23
|
+
disableParallelToolUse: {
|
|
24
|
+
description: 'Use at most one tool per response.',
|
|
25
|
+
},
|
|
26
|
+
cacheControl: { description: 'Prompt cache control settings.' },
|
|
27
|
+
metadata: { description: 'Request metadata, such as a user ID.' },
|
|
28
|
+
mcpServers: { description: 'MCP servers to use in the request.' },
|
|
29
|
+
container: { description: 'Container and Agent Skills configuration.' },
|
|
30
|
+
toolStreaming: {
|
|
31
|
+
description: 'Fine-grained streaming of tool inputs and outputs.',
|
|
32
|
+
},
|
|
33
|
+
effort: {
|
|
34
|
+
description: 'Effort spent on thinking, text, and tool calls.',
|
|
35
|
+
},
|
|
36
|
+
taskBudget: { description: 'Advisory token budget for an agentic task.' },
|
|
37
|
+
speed: { description: 'Fast mode for faster output token speeds.' },
|
|
38
|
+
serviceTier: { description: 'Service tier for the request.' },
|
|
39
|
+
inferenceGeo: { description: 'Where model inference runs.' },
|
|
40
|
+
fallbacks: { description: 'Server-side fallback models.' },
|
|
41
|
+
anthropicBeta: { description: 'Additional beta features to enable.' },
|
|
42
|
+
safeguards: { description: 'Server-side safeguards to run.' },
|
|
43
|
+
compaction: { description: 'On-demand conversation summary.' },
|
|
44
|
+
contextManagement: { description: 'Server-side context management.' },
|
|
45
|
+
} satisfies Record<AnthropicLanguageProviderOption, ProviderOptionDefinition>,
|
|
46
|
+
} as const satisfies ProviderOptionDefinitions;
|
|
47
|
+
|
|
48
|
+
type AnthropicLanguageCapabilities = NonNullable<
|
|
49
|
+
ModelCapabilities['language']
|
|
50
|
+
> & {
|
|
51
|
+
tools: NonNullable<ModelCapabilities['language']>['tools'] & {
|
|
52
|
+
forcedChoice: boolean;
|
|
53
|
+
computerUse: ModelCapability;
|
|
54
|
+
webSearch: ModelCapability;
|
|
55
|
+
toolSearch: ModelCapability;
|
|
56
|
+
};
|
|
57
|
+
reasoning: NonNullable<ModelCapabilities['language']>['reasoning'] & {
|
|
58
|
+
adaptive: boolean;
|
|
59
|
+
budget: boolean;
|
|
60
|
+
canDisable: boolean;
|
|
61
|
+
canDisableAboveHighEffort: boolean;
|
|
62
|
+
supportsXhighEffort: boolean;
|
|
63
|
+
betweenTools: boolean;
|
|
64
|
+
};
|
|
65
|
+
sampling: NonNullable<ModelCapabilities['language']>['sampling'] & {
|
|
66
|
+
topK: ModelCapability;
|
|
67
|
+
};
|
|
68
|
+
structuredOutput: {
|
|
69
|
+
native: boolean;
|
|
70
|
+
};
|
|
71
|
+
compaction: ModelCapability;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type AnthropicLanguageRequestConfig = {
|
|
75
|
+
maxOutputTokens: number;
|
|
76
|
+
knownModel: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type AnthropicLanguageModelEntry = ModelEntry<{
|
|
80
|
+
language: AnthropicLanguageRequestConfig;
|
|
81
|
+
}> & {
|
|
82
|
+
capabilities: { language: AnthropicLanguageCapabilities };
|
|
83
|
+
providerOptions: {
|
|
84
|
+
language: Record<AnthropicLanguageProviderOption, ModelCapability>;
|
|
85
|
+
};
|
|
86
|
+
requestConfig: { language: AnthropicLanguageRequestConfig };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type RuntimeDefinition = {
|
|
90
|
+
maxOutputTokens: number;
|
|
91
|
+
nativeStructuredOutput: boolean;
|
|
92
|
+
adaptiveThinking: boolean;
|
|
93
|
+
samplingParameters: boolean;
|
|
94
|
+
xhighEffort: boolean;
|
|
95
|
+
thinkingCanBeDisabledAboveHighEffort: boolean;
|
|
96
|
+
thinkingCanBeDisabled: boolean;
|
|
97
|
+
budgetThinking: boolean;
|
|
98
|
+
forcedToolUse: boolean;
|
|
99
|
+
betweenToolsThinking: boolean;
|
|
100
|
+
knownModel: boolean;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type DocumentedCapabilities = {
|
|
104
|
+
toolSearch: boolean;
|
|
105
|
+
compaction: boolean;
|
|
106
|
+
/** `effort` was introduced with claude-opus-4-5. */
|
|
107
|
+
effort: boolean;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const legacyClaude = defineRuntime({});
|
|
111
|
+
const currentClaude = defineRuntime({
|
|
112
|
+
maxOutputTokens: 128000,
|
|
113
|
+
nativeStructuredOutput: true,
|
|
114
|
+
adaptiveThinking: true,
|
|
115
|
+
});
|
|
116
|
+
const currentClaudeWithoutSampling = defineRuntime({
|
|
117
|
+
...currentClaude,
|
|
118
|
+
samplingParameters: false,
|
|
119
|
+
xhighEffort: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function defineRuntime(
|
|
123
|
+
overrides: Partial<RuntimeDefinition>,
|
|
124
|
+
): RuntimeDefinition {
|
|
125
|
+
return {
|
|
126
|
+
maxOutputTokens: 4096,
|
|
127
|
+
nativeStructuredOutput: false,
|
|
128
|
+
adaptiveThinking: false,
|
|
129
|
+
samplingParameters: true,
|
|
130
|
+
xhighEffort: false,
|
|
131
|
+
thinkingCanBeDisabledAboveHighEffort: true,
|
|
132
|
+
thinkingCanBeDisabled: true,
|
|
133
|
+
budgetThinking: true,
|
|
134
|
+
forcedToolUse: true,
|
|
135
|
+
betweenToolsThinking: false,
|
|
136
|
+
knownModel: true,
|
|
137
|
+
...overrides,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const runtimeRules: readonly {
|
|
142
|
+
matches: (modelId: string) => boolean;
|
|
143
|
+
definition: RuntimeDefinition;
|
|
144
|
+
}[] = [
|
|
145
|
+
{
|
|
146
|
+
matches: modelId => modelId.includes('claude-sonnet-5-5'),
|
|
147
|
+
definition: defineRuntime({
|
|
148
|
+
...currentClaudeWithoutSampling,
|
|
149
|
+
thinkingCanBeDisabledAboveHighEffort: false,
|
|
150
|
+
thinkingCanBeDisabled: false,
|
|
151
|
+
budgetThinking: false,
|
|
152
|
+
forcedToolUse: false,
|
|
153
|
+
betweenToolsThinking: true,
|
|
154
|
+
}),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
matches: modelId => modelId.includes('claude-opus-5-5'),
|
|
158
|
+
definition: defineRuntime({
|
|
159
|
+
...currentClaudeWithoutSampling,
|
|
160
|
+
thinkingCanBeDisabledAboveHighEffort: false,
|
|
161
|
+
thinkingCanBeDisabled: false,
|
|
162
|
+
budgetThinking: false,
|
|
163
|
+
forcedToolUse: false,
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
matches: modelId => modelId.includes('claude-haiku-5-5'),
|
|
168
|
+
definition: defineRuntime({
|
|
169
|
+
...currentClaudeWithoutSampling,
|
|
170
|
+
thinkingCanBeDisabledAboveHighEffort: false,
|
|
171
|
+
budgetThinking: false,
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
matches: modelId => modelId.includes('claude-opus-5'),
|
|
176
|
+
definition: defineRuntime({
|
|
177
|
+
...currentClaudeWithoutSampling,
|
|
178
|
+
thinkingCanBeDisabledAboveHighEffort: false,
|
|
179
|
+
}),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
matches: modelId => modelId.includes('claude-fable-5-1'),
|
|
183
|
+
definition: defineRuntime({
|
|
184
|
+
...currentClaudeWithoutSampling,
|
|
185
|
+
thinkingCanBeDisabled: false,
|
|
186
|
+
budgetThinking: false,
|
|
187
|
+
forcedToolUse: false,
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
matches: modelId => modelId.includes('claude-fable-5'),
|
|
192
|
+
definition: defineRuntime({
|
|
193
|
+
...currentClaudeWithoutSampling,
|
|
194
|
+
thinkingCanBeDisabled: false,
|
|
195
|
+
budgetThinking: false,
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
matches: modelId =>
|
|
200
|
+
modelId.includes('claude-opus-4-8') ||
|
|
201
|
+
modelId.includes('claude-opus-4-7') ||
|
|
202
|
+
modelId.includes('claude-sonnet-5'),
|
|
203
|
+
definition: currentClaudeWithoutSampling,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
matches: modelId =>
|
|
207
|
+
modelId.includes('claude-sonnet-4-6') ||
|
|
208
|
+
modelId.includes('claude-opus-4-6'),
|
|
209
|
+
definition: currentClaude,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
matches: modelId =>
|
|
213
|
+
modelId.includes('claude-sonnet-4-5') ||
|
|
214
|
+
modelId.includes('claude-opus-4-5') ||
|
|
215
|
+
modelId.includes('claude-haiku-4-5'),
|
|
216
|
+
definition: defineRuntime({
|
|
217
|
+
maxOutputTokens: 64000,
|
|
218
|
+
nativeStructuredOutput: true,
|
|
219
|
+
}),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
matches: modelId => modelId.includes('claude-opus-4-1'),
|
|
223
|
+
definition: defineRuntime({
|
|
224
|
+
maxOutputTokens: 32000,
|
|
225
|
+
nativeStructuredOutput: true,
|
|
226
|
+
}),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
matches: modelId => /claude-sonnet-4(?:-|@)/.test(modelId),
|
|
230
|
+
definition: defineRuntime({ maxOutputTokens: 64000 }),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
matches: modelId => /claude-opus-4(?:-|@)/.test(modelId),
|
|
234
|
+
definition: defineRuntime({ maxOutputTokens: 32000 }),
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
matches: modelId => modelId.includes('claude-3-haiku'),
|
|
238
|
+
definition: legacyClaude,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
matches: modelId =>
|
|
242
|
+
/claude-(?:instant(?:-|$)|v?2(?=$|[-.:])|3(?=$|[-.]))/.test(modelId),
|
|
243
|
+
definition: defineRuntime({ knownModel: false }),
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const documentedModels: Readonly<Record<string, DocumentedCapabilities>> = {
|
|
248
|
+
'claude-opus-5-5': { toolSearch: true, compaction: true, effort: true },
|
|
249
|
+
'claude-opus-5': { toolSearch: true, compaction: true, effort: true },
|
|
250
|
+
'claude-sonnet-5-5': { toolSearch: true, compaction: true, effort: true },
|
|
251
|
+
'claude-sonnet-5': { toolSearch: true, compaction: true, effort: true },
|
|
252
|
+
'claude-haiku-5-5': { toolSearch: true, compaction: true, effort: true },
|
|
253
|
+
'claude-fable-5-1': { toolSearch: true, compaction: true, effort: true },
|
|
254
|
+
'claude-fable-5': { toolSearch: true, compaction: true, effort: true },
|
|
255
|
+
'claude-opus-4-8': { toolSearch: true, compaction: true, effort: true },
|
|
256
|
+
'claude-opus-4-7': { toolSearch: true, compaction: true, effort: true },
|
|
257
|
+
'claude-opus-4-6': { toolSearch: true, compaction: true, effort: true },
|
|
258
|
+
'claude-sonnet-4-6': { toolSearch: true, compaction: true, effort: true },
|
|
259
|
+
'claude-opus-4-5': { toolSearch: true, compaction: false, effort: true },
|
|
260
|
+
'claude-haiku-4-5': { toolSearch: true, compaction: false, effort: false },
|
|
261
|
+
'claude-sonnet-4-5': { toolSearch: true, compaction: false, effort: false },
|
|
262
|
+
'claude-opus-4-1': { toolSearch: false, compaction: false, effort: false },
|
|
263
|
+
'claude-opus-4-0': { toolSearch: false, compaction: false, effort: false },
|
|
264
|
+
'claude-sonnet-4-0': { toolSearch: false, compaction: false, effort: false },
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export const anthropicModelCatalog: Readonly<
|
|
268
|
+
Record<string, AnthropicLanguageModelEntry>
|
|
269
|
+
> = Object.fromEntries(
|
|
270
|
+
Object.entries(documentedModels).map(([modelId, capabilities]) => [
|
|
271
|
+
modelId,
|
|
272
|
+
createAnthropicLanguageModelEntry(modelId, capabilities),
|
|
273
|
+
]),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
export function getAnthropicLanguageModelEntry(
|
|
277
|
+
modelId: string,
|
|
278
|
+
): AnthropicLanguageModelEntry {
|
|
279
|
+
return Object.prototype.hasOwnProperty.call(anthropicModelCatalog, modelId)
|
|
280
|
+
? anthropicModelCatalog[modelId]
|
|
281
|
+
: createAnthropicLanguageModelEntry(modelId);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function createAnthropicLanguageModelEntry(
|
|
285
|
+
modelId: string,
|
|
286
|
+
documented?: DocumentedCapabilities,
|
|
287
|
+
): AnthropicLanguageModelEntry {
|
|
288
|
+
const runtime = resolveRuntimeDefinition(modelId);
|
|
289
|
+
const declared = documented != null;
|
|
290
|
+
const samplingSupport = runtime.samplingParameters
|
|
291
|
+
? true
|
|
292
|
+
: { condition: 'Only supported when thinking is disabled.' };
|
|
293
|
+
const reasoningSupport = runtime.adaptiveThinking || runtime.budgetThinking;
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
capabilities: {
|
|
297
|
+
language: {
|
|
298
|
+
inputs: {
|
|
299
|
+
image: declared ? true : 'unknown',
|
|
300
|
+
audio: declared ? false : 'unknown',
|
|
301
|
+
},
|
|
302
|
+
objectGeneration: declared ? true : 'unknown',
|
|
303
|
+
tools: {
|
|
304
|
+
usage: declared ? true : 'unknown',
|
|
305
|
+
streaming: declared ? true : 'unknown',
|
|
306
|
+
forcedChoice: runtime.forcedToolUse,
|
|
307
|
+
computerUse: declared ? true : 'unknown',
|
|
308
|
+
webSearch: declared ? true : 'unknown',
|
|
309
|
+
toolSearch: documented?.toolSearch ?? 'unknown',
|
|
310
|
+
},
|
|
311
|
+
reasoning: {
|
|
312
|
+
support: reasoningSupport,
|
|
313
|
+
efforts: runtime.xhighEffort
|
|
314
|
+
? ['low', 'medium', 'high', 'xhigh', 'max']
|
|
315
|
+
: undefined,
|
|
316
|
+
adaptive: runtime.adaptiveThinking,
|
|
317
|
+
budget: runtime.budgetThinking,
|
|
318
|
+
canDisable: runtime.thinkingCanBeDisabled,
|
|
319
|
+
canDisableAboveHighEffort:
|
|
320
|
+
runtime.thinkingCanBeDisabledAboveHighEffort,
|
|
321
|
+
supportsXhighEffort: runtime.xhighEffort,
|
|
322
|
+
betweenTools: runtime.betweenToolsThinking,
|
|
323
|
+
},
|
|
324
|
+
sampling: {
|
|
325
|
+
temperature: samplingSupport,
|
|
326
|
+
topP: samplingSupport,
|
|
327
|
+
topK: samplingSupport,
|
|
328
|
+
logprobs: false,
|
|
329
|
+
},
|
|
330
|
+
structuredOutput: { native: runtime.nativeStructuredOutput },
|
|
331
|
+
compaction: documented?.compaction ?? 'unknown',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
providerOptions: {
|
|
335
|
+
language: declareProviderOptions(
|
|
336
|
+
anthropicProviderOptions.language,
|
|
337
|
+
declared ? true : 'unknown',
|
|
338
|
+
{
|
|
339
|
+
thinking: reasoningSupport,
|
|
340
|
+
effort: documented?.effort ?? 'unknown',
|
|
341
|
+
// Fast mode is documented for claude-opus-4-6 only.
|
|
342
|
+
speed: declared ? modelId === 'claude-opus-4-6' : 'unknown',
|
|
343
|
+
// Task budgets are documented for claude-opus-4-7; other models are
|
|
344
|
+
// not documented either way.
|
|
345
|
+
taskBudget: modelId === 'claude-opus-4-7' ? true : 'unknown',
|
|
346
|
+
},
|
|
347
|
+
),
|
|
348
|
+
},
|
|
349
|
+
requestConfig: {
|
|
350
|
+
language: {
|
|
351
|
+
maxOutputTokens: runtime.maxOutputTokens,
|
|
352
|
+
knownModel: runtime.knownModel,
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Applies one default support value to every option, except the listed ones. */
|
|
359
|
+
function declareProviderOptions<Option extends string>(
|
|
360
|
+
definitions: Readonly<Record<Option, ProviderOptionDefinition>>,
|
|
361
|
+
support: ModelCapability,
|
|
362
|
+
exceptions: Partial<Record<Option, ModelCapability>>,
|
|
363
|
+
): Record<Option, ModelCapability> {
|
|
364
|
+
return Object.fromEntries(
|
|
365
|
+
(Object.keys(definitions) as Option[]).map(option => [
|
|
366
|
+
option,
|
|
367
|
+
exceptions[option] ?? support,
|
|
368
|
+
]),
|
|
369
|
+
) as Record<Option, ModelCapability>;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function resolveRuntimeDefinition(modelId: string): RuntimeDefinition {
|
|
373
|
+
const rule = runtimeRules.find(rule => rule.matches(modelId));
|
|
374
|
+
if (rule != null) return rule.definition;
|
|
375
|
+
|
|
376
|
+
return modelId.includes('claude-')
|
|
377
|
+
? defineRuntime({
|
|
378
|
+
...currentClaudeWithoutSampling,
|
|
379
|
+
thinkingCanBeDisabledAboveHighEffort: false,
|
|
380
|
+
knownModel: false,
|
|
381
|
+
})
|
|
382
|
+
: defineRuntime({ knownModel: false });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Compatibility projection for the provider's existing request preparation. */
|
|
386
|
+
export function getAnthropicModelCapabilities(modelId: string) {
|
|
387
|
+
const {
|
|
388
|
+
capabilities: { language },
|
|
389
|
+
requestConfig: { language: requestConfig },
|
|
390
|
+
} = getAnthropicLanguageModelEntry(modelId);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
maxOutputTokens: requestConfig.maxOutputTokens,
|
|
394
|
+
supportsStructuredOutput: language.structuredOutput.native,
|
|
395
|
+
supportsAdaptiveThinking: language.reasoning.adaptive,
|
|
396
|
+
rejectsSamplingParameters: language.sampling.temperature !== true,
|
|
397
|
+
supportsXhighEffort: language.reasoning.supportsXhighEffort,
|
|
398
|
+
rejectsThinkingDisabledAboveHighEffort:
|
|
399
|
+
!language.reasoning.canDisableAboveHighEffort,
|
|
400
|
+
rejectsThinkingDisabled: !language.reasoning.canDisable,
|
|
401
|
+
rejectsBudgetThinking: !language.reasoning.budget,
|
|
402
|
+
rejectsForcedToolUse: !language.tools.forcedChoice,
|
|
403
|
+
supportsBetweenToolsThinking: language.reasoning.betweenTools,
|
|
404
|
+
isKnownModel: requestConfig.knownModel,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { JSONSchema7 } from '@ai-sdk/provider';
|
|
2
|
+
import { isRecord } from '@ai-sdk/provider-utils';
|
|
3
|
+
|
|
4
|
+
// Keywords whose values are maps of name -> schema rather than a schema.
|
|
5
|
+
const SCHEMA_MAP_KEYWORDS = new Set([
|
|
6
|
+
'dependentSchemas',
|
|
7
|
+
'patternProperties',
|
|
8
|
+
'properties',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
// Keywords holding instance data, which can contain a literal `$ref` key.
|
|
12
|
+
const DATA_KEYWORDS = new Set(['const', 'default', 'enum', 'examples']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether a JSON schema contains a cycle of local `$ref`s, e.g. a tree node
|
|
16
|
+
* whose children reference the node itself.
|
|
17
|
+
*
|
|
18
|
+
* Anthropic's native structured outputs reject such schemas, while the JSON
|
|
19
|
+
* response tool accepts them.
|
|
20
|
+
*/
|
|
21
|
+
export function hasCircularSchemaReference(schema: JSONSchema7): boolean {
|
|
22
|
+
const visiting = new Set<string>();
|
|
23
|
+
const visited = new Set<string>();
|
|
24
|
+
|
|
25
|
+
const visit = (pointer: string): boolean => {
|
|
26
|
+
if (visiting.has(pointer)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (visited.has(pointer)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const target = resolveSchemaPointer(schema, pointer);
|
|
34
|
+
if (target === undefined) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
visiting.add(pointer);
|
|
39
|
+
const isCircular = collectLocalRefs(target).some(visit);
|
|
40
|
+
visiting.delete(pointer);
|
|
41
|
+
visited.add(pointer);
|
|
42
|
+
|
|
43
|
+
return isCircular;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return visit('#');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectLocalRefs(node: unknown, refs: string[] = []): string[] {
|
|
50
|
+
if (Array.isArray(node)) {
|
|
51
|
+
for (const item of node) {
|
|
52
|
+
collectLocalRefs(item, refs);
|
|
53
|
+
}
|
|
54
|
+
return refs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isRecord(node)) {
|
|
58
|
+
return refs;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const [key, value] of Object.entries(node)) {
|
|
62
|
+
if (key === '$ref') {
|
|
63
|
+
if (typeof value === 'string' && value.startsWith('#')) {
|
|
64
|
+
refs.push(value);
|
|
65
|
+
}
|
|
66
|
+
} else if (SCHEMA_MAP_KEYWORDS.has(key) && isRecord(value)) {
|
|
67
|
+
for (const subschema of Object.values(value)) {
|
|
68
|
+
collectLocalRefs(subschema, refs);
|
|
69
|
+
}
|
|
70
|
+
} else if (
|
|
71
|
+
// definitions are only reached through a `$ref`:
|
|
72
|
+
key !== '$defs' &&
|
|
73
|
+
key !== 'definitions' &&
|
|
74
|
+
!DATA_KEYWORDS.has(key)
|
|
75
|
+
) {
|
|
76
|
+
collectLocalRefs(value, refs);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return refs;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveSchemaPointer(
|
|
84
|
+
schema: JSONSchema7,
|
|
85
|
+
pointer: string,
|
|
86
|
+
): unknown {
|
|
87
|
+
if (pointer === '#') {
|
|
88
|
+
return schema;
|
|
89
|
+
}
|
|
90
|
+
if (!pointer.startsWith('#/')) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let decodedPointer: string;
|
|
95
|
+
try {
|
|
96
|
+
decodedPointer = decodeURIComponent(pointer.slice(2));
|
|
97
|
+
} catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let current: unknown = schema;
|
|
102
|
+
for (const rawSegment of decodedPointer.split('/')) {
|
|
103
|
+
const segment = rawSegment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(current)) {
|
|
106
|
+
current = current[Number(segment)];
|
|
107
|
+
} else if (isRecord(current)) {
|
|
108
|
+
current = current[segment];
|
|
109
|
+
} else {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return current;
|
|
115
|
+
}
|
|
@@ -54,12 +54,11 @@ function sanitizeDefinition(
|
|
|
54
54
|
|
|
55
55
|
function sanitizeSchema(schema: JSONSchema7): JSONSchema7 {
|
|
56
56
|
const result: JSONSchema7 = {};
|
|
57
|
-
const schemaWithDefs = schema as JSONSchema7 & {
|
|
58
|
-
$defs?: Record<string, JSONSchema7Definition>;
|
|
59
|
-
};
|
|
60
57
|
|
|
58
|
+
// Keep the definitions next to a `$ref` so a top-level reference still
|
|
59
|
+
// resolves (e.g. `{ $ref: '#/definitions/Answer', definitions: {...} }`).
|
|
61
60
|
if (schema.$ref != null) {
|
|
62
|
-
return { $ref: schema.$ref };
|
|
61
|
+
return { $ref: schema.$ref, ...sanitizeSchemaDefinitions(schema) };
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
if (schema.$schema != null) {
|
|
@@ -104,26 +103,7 @@ function sanitizeSchema(schema: JSONSchema7): JSONSchema7 {
|
|
|
104
103
|
result.allOf = schema.allOf.map(sanitizeDefinition);
|
|
105
104
|
}
|
|
106
105
|
|
|
107
|
-
|
|
108
|
-
result.definitions = Object.fromEntries(
|
|
109
|
-
Object.entries(schema.definitions).map(([name, definition]) => [
|
|
110
|
-
name,
|
|
111
|
-
sanitizeDefinition(definition),
|
|
112
|
-
]),
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (schemaWithDefs.$defs != null) {
|
|
117
|
-
const resultWithDefs = result as JSONSchema7 & {
|
|
118
|
-
$defs?: Record<string, JSONSchema7Definition>;
|
|
119
|
-
};
|
|
120
|
-
resultWithDefs.$defs = Object.fromEntries(
|
|
121
|
-
Object.entries(schemaWithDefs.$defs).map(([name, definition]) => [
|
|
122
|
-
name,
|
|
123
|
-
sanitizeDefinition(definition),
|
|
124
|
-
]),
|
|
125
|
-
);
|
|
126
|
-
}
|
|
106
|
+
Object.assign(result, sanitizeSchemaDefinitions(schema));
|
|
127
107
|
|
|
128
108
|
if (schema.type === 'object' || schema.properties != null) {
|
|
129
109
|
if (schema.properties != null) {
|
|
@@ -166,6 +146,29 @@ function sanitizeSchema(schema: JSONSchema7): JSONSchema7 {
|
|
|
166
146
|
return result;
|
|
167
147
|
}
|
|
168
148
|
|
|
149
|
+
function sanitizeSchemaDefinitions(schema: JSONSchema7): Pick<
|
|
150
|
+
JSONSchema7,
|
|
151
|
+
'definitions'
|
|
152
|
+
> & {
|
|
153
|
+
$defs?: Record<string, JSONSchema7Definition>;
|
|
154
|
+
} {
|
|
155
|
+
const { $defs } = schema as { $defs?: Record<string, JSONSchema7Definition> };
|
|
156
|
+
const sanitizeAll = (definitions: Record<string, JSONSchema7Definition>) =>
|
|
157
|
+
Object.fromEntries(
|
|
158
|
+
Object.entries(definitions).map(([name, definition]) => [
|
|
159
|
+
name,
|
|
160
|
+
sanitizeDefinition(definition),
|
|
161
|
+
]),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...(schema.definitions != null && {
|
|
166
|
+
definitions: sanitizeAll(schema.definitions),
|
|
167
|
+
}),
|
|
168
|
+
...($defs != null && { $defs: sanitizeAll($defs) }),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
169
172
|
function getConstraintDescription(schema: JSONSchema7): string | undefined {
|
|
170
173
|
const descriptions = DESCRIPTION_CONSTRAINT_KEYS.flatMap(key => {
|
|
171
174
|
const value = schema[key];
|