@ai-sdk/anthropic 4.0.0-beta.4 → 4.0.0-beta.41

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 (53) hide show
  1. package/CHANGELOG.md +305 -4
  2. package/README.md +2 -0
  3. package/dist/index.d.ts +83 -58
  4. package/dist/index.js +2043 -1356
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts +85 -58
  7. package/dist/internal/index.js +1804 -1342
  8. package/dist/internal/index.js.map +1 -1
  9. package/docs/05-anthropic.mdx +116 -13
  10. package/package.json +14 -15
  11. package/src/{anthropic-messages-api.ts → anthropic-api.ts} +14 -6
  12. package/src/anthropic-error.ts +1 -1
  13. package/src/anthropic-files.ts +95 -0
  14. package/src/{anthropic-messages-language-model.ts → anthropic-language-model.ts} +263 -78
  15. package/src/anthropic-message-metadata.ts +1 -4
  16. package/src/{anthropic-messages-options.ts → anthropic-options.ts} +68 -11
  17. package/src/anthropic-prepare-tools.ts +14 -7
  18. package/src/anthropic-provider.ts +42 -13
  19. package/src/{convert-anthropic-messages-usage.ts → convert-anthropic-usage.ts} +4 -4
  20. package/src/{convert-to-anthropic-messages-prompt.ts → convert-to-anthropic-prompt.ts} +190 -149
  21. package/src/forward-anthropic-container-id-from-last-step.ts +2 -2
  22. package/src/get-cache-control.ts +5 -2
  23. package/src/index.ts +1 -1
  24. package/src/internal/index.ts +11 -2
  25. package/src/map-anthropic-stop-reason.ts +1 -1
  26. package/src/sanitize-json-schema.ts +203 -0
  27. package/src/skills/anthropic-skills-api.ts +44 -0
  28. package/src/skills/anthropic-skills.ts +132 -0
  29. package/src/tool/bash_20241022.ts +2 -2
  30. package/src/tool/bash_20250124.ts +2 -2
  31. package/src/tool/code-execution_20250522.ts +2 -2
  32. package/src/tool/code-execution_20250825.ts +2 -2
  33. package/src/tool/code-execution_20260120.ts +2 -2
  34. package/src/tool/computer_20241022.ts +2 -2
  35. package/src/tool/computer_20250124.ts +2 -2
  36. package/src/tool/computer_20251124.ts +2 -2
  37. package/src/tool/memory_20250818.ts +2 -2
  38. package/src/tool/text-editor_20241022.ts +2 -2
  39. package/src/tool/text-editor_20250124.ts +2 -2
  40. package/src/tool/text-editor_20250429.ts +2 -2
  41. package/src/tool/text-editor_20250728.ts +6 -3
  42. package/src/tool/tool-search-bm25_20251119.ts +2 -2
  43. package/src/tool/tool-search-regex_20251119.ts +2 -2
  44. package/src/tool/web-fetch-20250910.ts +2 -2
  45. package/src/tool/web-fetch-20260209.ts +2 -2
  46. package/src/tool/web-search_20250305.ts +2 -2
  47. package/src/tool/web-search_20260209.ts +2 -2
  48. package/dist/index.d.mts +0 -1090
  49. package/dist/index.mjs +0 -5244
  50. package/dist/index.mjs.map +0 -1
  51. package/dist/internal/index.d.mts +0 -969
  52. package/dist/internal/index.mjs +0 -5136
  53. package/dist/internal/index.mjs.map +0 -1
@@ -1,28 +1,30 @@
1
1
  import {
2
- SharedV4Warning,
3
- LanguageModelV4DataContent,
4
- LanguageModelV4Message,
5
- LanguageModelV4Prompt,
6
- SharedV4ProviderMetadata,
7
2
  UnsupportedFunctionalityError,
3
+ type SharedV4Warning,
4
+ type LanguageModelV4Message,
5
+ type LanguageModelV4Prompt,
6
+ type SharedV4ProviderMetadata,
8
7
  } from '@ai-sdk/provider';
9
8
  import {
10
9
  convertBase64ToUint8Array,
11
10
  convertToBase64,
11
+ getTopLevelMediaType,
12
12
  parseProviderOptions,
13
+ resolveFullMediaType,
14
+ resolveProviderReference,
13
15
  validateTypes,
14
16
  isNonNullable,
15
- ToolNameMapping,
17
+ type ToolNameMapping,
16
18
  } from '@ai-sdk/provider-utils';
17
19
  import {
18
- AnthropicAssistantMessage,
19
- AnthropicMessagesPrompt,
20
20
  anthropicReasoningMetadataSchema,
21
- AnthropicToolResultContent,
22
- AnthropicUserMessage,
23
- AnthropicWebFetchToolResultContent,
24
- } from './anthropic-messages-api';
25
- import { anthropicFilePartProviderOptions } from './anthropic-messages-options';
21
+ type AnthropicAssistantMessage,
22
+ type AnthropicPrompt,
23
+ type AnthropicToolResultContent,
24
+ type AnthropicUserMessage,
25
+ type AnthropicWebFetchToolResultContent,
26
+ } from './anthropic-api';
27
+ import { anthropicFilePartProviderOptions } from './anthropic-options';
26
28
  import { CacheControlValidator } from './get-cache-control';
27
29
  import { codeExecution_20250522OutputSchema } from './tool/code-execution_20250522';
28
30
  import { codeExecution_20250825OutputSchema } from './tool/code-execution_20250825';
@@ -31,44 +33,14 @@ import { toolSearchRegex_20251119OutputSchema as toolSearchOutputSchema } from '
31
33
  import { webFetch_20250910OutputSchema } from './tool/web-fetch-20250910';
32
34
  import { webSearch_20250305OutputSchema } from './tool/web-search_20250305';
33
35
 
34
- function convertToString(data: LanguageModelV4DataContent): string {
36
+ function convertBytesDataToString(data: Uint8Array | string): string {
35
37
  if (typeof data === 'string') {
36
38
  return new TextDecoder().decode(convertBase64ToUint8Array(data));
37
39
  }
38
-
39
- if (data instanceof Uint8Array) {
40
- return new TextDecoder().decode(data);
41
- }
42
-
43
- if (data instanceof URL) {
44
- throw new UnsupportedFunctionalityError({
45
- functionality: 'URL-based text documents are not supported for citations',
46
- });
47
- }
48
-
49
- throw new UnsupportedFunctionalityError({
50
- functionality: `unsupported data type for text documents: ${typeof data}`,
51
- });
52
- }
53
-
54
- /**
55
- * Checks if data is a URL (either a URL object or a URL string).
56
- */
57
- function isUrlData(
58
- data: LanguageModelV4DataContent,
59
- ): data is URL | (string & { __brand: 'url-string' }) {
60
- return data instanceof URL || isUrlString(data);
40
+ return new TextDecoder().decode(data);
61
41
  }
62
42
 
63
- function isUrlString(data: LanguageModelV4DataContent): boolean {
64
- return typeof data === 'string' && /^https?:\/\//i.test(data);
65
- }
66
-
67
- function getUrlString(data: LanguageModelV4DataContent): string {
68
- return data instanceof URL ? data.toString() : (data as string);
69
- }
70
-
71
- export async function convertToAnthropicMessagesPrompt({
43
+ export async function convertToAnthropicPrompt({
72
44
  prompt,
73
45
  sendReasoning,
74
46
  warnings,
@@ -81,15 +53,15 @@ export async function convertToAnthropicMessagesPrompt({
81
53
  cacheControlValidator?: CacheControlValidator;
82
54
  toolNameMapping: ToolNameMapping;
83
55
  }): Promise<{
84
- prompt: AnthropicMessagesPrompt;
56
+ prompt: AnthropicPrompt;
85
57
  betas: Set<string>;
86
58
  }> {
87
59
  const betas = new Set<string>();
88
60
  const blocks = groupIntoBlocks(prompt);
89
61
  const validator = cacheControlValidator || new CacheControlValidator();
90
62
 
91
- let system: AnthropicMessagesPrompt['system'] = undefined;
92
- const messages: AnthropicMessagesPrompt['messages'] = [];
63
+ let system: AnthropicPrompt['system'] = undefined;
64
+ const messages: AnthropicPrompt['messages'] = [];
93
65
 
94
66
  async function shouldEnableCitations(
95
67
  providerMetadata: SharedV4ProviderMetadata | undefined,
@@ -183,86 +155,154 @@ export async function convertToAnthropicMessagesPrompt({
183
155
  }
184
156
 
185
157
  case 'file': {
186
- if (part.mediaType.startsWith('image/')) {
187
- anthropicContent.push({
188
- type: 'image',
189
- source: isUrlData(part.data)
190
- ? {
191
- type: 'url',
192
- url: getUrlString(part.data),
193
- }
194
- : {
195
- type: 'base64',
196
- media_type:
197
- part.mediaType === 'image/*'
198
- ? 'image/jpeg'
199
- : part.mediaType,
200
- data: convertToBase64(part.data),
201
- },
202
- cache_control: cacheControl,
203
- });
204
- } else if (part.mediaType === 'application/pdf') {
205
- betas.add('pdfs-2024-09-25');
206
-
207
- const enableCitations = await shouldEnableCitations(
208
- part.providerOptions,
209
- );
210
-
211
- const metadata = await getDocumentMetadata(
212
- part.providerOptions,
213
- );
214
-
215
- anthropicContent.push({
216
- type: 'document',
217
- source: isUrlData(part.data)
218
- ? {
219
- type: 'url',
220
- url: getUrlString(part.data),
221
- }
222
- : {
223
- type: 'base64',
224
- media_type: 'application/pdf',
225
- data: convertToBase64(part.data),
226
- },
227
- title: metadata.title ?? part.filename,
228
- ...(metadata.context && { context: metadata.context }),
229
- ...(enableCitations && {
230
- citations: { enabled: true },
231
- }),
232
- cache_control: cacheControl,
233
- });
234
- } else if (part.mediaType === 'text/plain') {
235
- const enableCitations = await shouldEnableCitations(
236
- part.providerOptions,
237
- );
238
-
239
- const metadata = await getDocumentMetadata(
240
- part.providerOptions,
241
- );
242
-
243
- anthropicContent.push({
244
- type: 'document',
245
- source: isUrlData(part.data)
246
- ? {
247
- type: 'url',
248
- url: getUrlString(part.data),
249
- }
250
- : {
251
- type: 'text',
252
- media_type: 'text/plain',
253
- data: convertToString(part.data),
254
- },
255
- title: metadata.title ?? part.filename,
256
- ...(metadata.context && { context: metadata.context }),
257
- ...(enableCitations && {
258
- citations: { enabled: true },
259
- }),
260
- cache_control: cacheControl,
261
- });
262
- } else {
263
- throw new UnsupportedFunctionalityError({
264
- functionality: `media type: ${part.mediaType}`,
265
- });
158
+ switch (part.data.type) {
159
+ case 'reference': {
160
+ const fileId = resolveProviderReference({
161
+ reference: part.data.reference,
162
+ provider: 'anthropic',
163
+ });
164
+ betas.add('files-api-2025-04-14');
165
+
166
+ if (getTopLevelMediaType(part.mediaType) === 'image') {
167
+ anthropicContent.push({
168
+ type: 'image',
169
+ source: { type: 'file', file_id: fileId },
170
+ cache_control: cacheControl,
171
+ });
172
+ } else {
173
+ anthropicContent.push({
174
+ type: 'document',
175
+ source: { type: 'file', file_id: fileId },
176
+ cache_control: cacheControl,
177
+ });
178
+ }
179
+ break;
180
+ }
181
+ case 'text': {
182
+ const enableCitations = await shouldEnableCitations(
183
+ part.providerOptions,
184
+ );
185
+
186
+ const metadata = await getDocumentMetadata(
187
+ part.providerOptions,
188
+ );
189
+
190
+ anthropicContent.push({
191
+ type: 'document',
192
+ source: {
193
+ type: 'text',
194
+ media_type: 'text/plain',
195
+ data: part.data.text,
196
+ },
197
+ title: metadata.title ?? part.filename,
198
+ ...(metadata.context && {
199
+ context: metadata.context,
200
+ }),
201
+ ...(enableCitations && {
202
+ citations: { enabled: true },
203
+ }),
204
+ cache_control: cacheControl,
205
+ });
206
+ break;
207
+ }
208
+ case 'url':
209
+ case 'data': {
210
+ const topLevel = getTopLevelMediaType(part.mediaType);
211
+ if (topLevel === 'image') {
212
+ anthropicContent.push({
213
+ type: 'image',
214
+ source:
215
+ part.data.type === 'url'
216
+ ? {
217
+ type: 'url',
218
+ url: part.data.url.toString(),
219
+ }
220
+ : {
221
+ type: 'base64',
222
+ media_type: resolveFullMediaType({ part }),
223
+ data: convertToBase64(part.data.data),
224
+ },
225
+ cache_control: cacheControl,
226
+ });
227
+ } else if (
228
+ topLevel === 'application' &&
229
+ (part.data.type === 'url'
230
+ ? part.mediaType === 'application/pdf'
231
+ : resolveFullMediaType({ part }) ===
232
+ 'application/pdf')
233
+ ) {
234
+ betas.add('pdfs-2024-09-25');
235
+
236
+ const enableCitations = await shouldEnableCitations(
237
+ part.providerOptions,
238
+ );
239
+
240
+ const metadata = await getDocumentMetadata(
241
+ part.providerOptions,
242
+ );
243
+
244
+ anthropicContent.push({
245
+ type: 'document',
246
+ source:
247
+ part.data.type === 'url'
248
+ ? {
249
+ type: 'url',
250
+ url: part.data.url.toString(),
251
+ }
252
+ : {
253
+ type: 'base64',
254
+ media_type: 'application/pdf',
255
+ data: convertToBase64(part.data.data),
256
+ },
257
+ title: metadata.title ?? part.filename,
258
+ ...(metadata.context && {
259
+ context: metadata.context,
260
+ }),
261
+ ...(enableCitations && {
262
+ citations: { enabled: true },
263
+ }),
264
+ cache_control: cacheControl,
265
+ });
266
+ } else if (part.mediaType === 'text/plain') {
267
+ const enableCitations = await shouldEnableCitations(
268
+ part.providerOptions,
269
+ );
270
+
271
+ const metadata = await getDocumentMetadata(
272
+ part.providerOptions,
273
+ );
274
+
275
+ anthropicContent.push({
276
+ type: 'document',
277
+ source:
278
+ part.data.type === 'url'
279
+ ? {
280
+ type: 'url',
281
+ url: part.data.url.toString(),
282
+ }
283
+ : {
284
+ type: 'text',
285
+ media_type: 'text/plain',
286
+ data: convertBytesDataToString(
287
+ part.data.data,
288
+ ),
289
+ },
290
+ title: metadata.title ?? part.filename,
291
+ ...(metadata.context && {
292
+ context: metadata.context,
293
+ }),
294
+ ...(enableCitations && {
295
+ citations: { enabled: true },
296
+ }),
297
+ cache_control: cacheControl,
298
+ });
299
+ } else {
300
+ throw new UnsupportedFunctionalityError({
301
+ functionality: `media type: ${part.mediaType}`,
302
+ });
303
+ }
304
+ break;
305
+ }
266
306
  }
267
307
 
268
308
  break;
@@ -309,26 +349,16 @@ export async function convertToAnthropicMessagesPrompt({
309
349
  type: 'text' as const,
310
350
  text: contentPart.text,
311
351
  };
312
- case 'image-data': {
313
- return {
314
- type: 'image' as const,
315
- source: {
316
- type: 'base64' as const,
317
- media_type: contentPart.mediaType,
318
- data: contentPart.data,
319
- },
320
- };
321
- }
322
- case 'image-url': {
323
- return {
324
- type: 'image' as const,
325
- source: {
326
- type: 'url' as const,
327
- url: contentPart.url,
328
- },
329
- };
330
- }
331
352
  case 'file-url': {
353
+ if (contentPart.mediaType.startsWith('image/')) {
354
+ return {
355
+ type: 'image' as const,
356
+ source: {
357
+ type: 'url' as const,
358
+ url: contentPart.url,
359
+ },
360
+ };
361
+ }
332
362
  return {
333
363
  type: 'document' as const,
334
364
  source: {
@@ -338,6 +368,16 @@ export async function convertToAnthropicMessagesPrompt({
338
368
  };
339
369
  }
340
370
  case 'file-data': {
371
+ if (contentPart.mediaType.startsWith('image/')) {
372
+ return {
373
+ type: 'image' as const,
374
+ source: {
375
+ type: 'base64' as const,
376
+ media_type: contentPart.mediaType,
377
+ data: contentPart.data,
378
+ },
379
+ };
380
+ }
341
381
  if (contentPart.mediaType === 'application/pdf') {
342
382
  betas.add('pdfs-2024-09-25');
343
383
  return {
@@ -391,7 +431,8 @@ export async function convertToAnthropicMessagesPrompt({
391
431
  contentValue = output.value;
392
432
  break;
393
433
  case 'execution-denied':
394
- contentValue = output.reason ?? 'Tool execution denied.';
434
+ contentValue =
435
+ output.reason ?? 'Tool call execution denied.';
395
436
  break;
396
437
  case 'json':
397
438
  case 'error-json':
@@ -1,5 +1,5 @@
1
- import { JSONObject } from '@ai-sdk/provider';
2
- import { AnthropicMessageMetadata } from './anthropic-message-metadata';
1
+ import type { JSONObject } from '@ai-sdk/provider';
2
+ import type { AnthropicMessageMetadata } from './anthropic-message-metadata';
3
3
 
4
4
  /**
5
5
  * Sets the Anthropic container ID in the provider options based on
@@ -1,5 +1,8 @@
1
- import { SharedV4Warning, SharedV4ProviderMetadata } from '@ai-sdk/provider';
2
- import { AnthropicCacheControl } from './anthropic-messages-api';
1
+ import type {
2
+ SharedV4Warning,
3
+ SharedV4ProviderMetadata,
4
+ } from '@ai-sdk/provider';
5
+ import type { AnthropicCacheControl } from './anthropic-api';
3
6
 
4
7
  // Anthropic allows a maximum of 4 cache breakpoints per request
5
8
  const MAX_CACHE_BREAKPOINTS = 4;
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@ export type {
6
6
  AnthropicLanguageModelOptions,
7
7
  /** @deprecated Use `AnthropicLanguageModelOptions` instead. */
8
8
  AnthropicLanguageModelOptions as AnthropicProviderOptions,
9
- } from './anthropic-messages-options';
9
+ } from './anthropic-options';
10
10
  export type { AnthropicToolOptions } from './anthropic-prepare-tools';
11
11
  export { anthropic, createAnthropic } from './anthropic-provider';
12
12
  export type {
@@ -1,4 +1,13 @@
1
- export { AnthropicMessagesLanguageModel } from '../anthropic-messages-language-model';
1
+ export {
2
+ AnthropicLanguageModel,
3
+ /** @deprecated Use `AnthropicLanguageModel` instead. */
4
+ AnthropicLanguageModel as AnthropicMessagesLanguageModel,
5
+ getModelCapabilities,
6
+ } from '../anthropic-language-model';
2
7
  export { anthropicTools } from '../anthropic-tools';
3
- export type { AnthropicMessagesModelId } from '../anthropic-messages-options';
8
+ export type {
9
+ AnthropicModelId,
10
+ /** @deprecated Use `AnthropicModelId` instead. */
11
+ AnthropicModelId as AnthropicMessagesModelId,
12
+ } from '../anthropic-options';
4
13
  export { prepareTools } from '../anthropic-prepare-tools';
@@ -1,4 +1,4 @@
1
- import { LanguageModelV4FinishReason } from '@ai-sdk/provider';
1
+ import type { LanguageModelV4FinishReason } from '@ai-sdk/provider';
2
2
 
3
3
  /**
4
4
  * @see https://docs.anthropic.com/en/api/messages#response-stop-reason
@@ -0,0 +1,203 @@
1
+ import type { JSONSchema7, JSONSchema7Definition } from '@ai-sdk/provider';
2
+
3
+ const SUPPORTED_STRING_FORMATS = new Set([
4
+ 'date-time',
5
+ 'time',
6
+ 'date',
7
+ 'duration',
8
+ 'email',
9
+ 'hostname',
10
+ 'uri',
11
+ 'ipv4',
12
+ 'ipv6',
13
+ 'uuid',
14
+ ]);
15
+
16
+ const DESCRIPTION_CONSTRAINT_KEYS = [
17
+ 'minimum',
18
+ 'maximum',
19
+ 'exclusiveMinimum',
20
+ 'exclusiveMaximum',
21
+ 'multipleOf',
22
+ 'minLength',
23
+ 'maxLength',
24
+ 'pattern',
25
+ 'minItems',
26
+ 'maxItems',
27
+ 'uniqueItems',
28
+ 'minProperties',
29
+ 'maxProperties',
30
+ 'not',
31
+ ] satisfies Array<keyof JSONSchema7>;
32
+
33
+ /**
34
+ * Removes JSON Schema keywords that Anthropic rejects in
35
+ * `output_config.format.schema`.
36
+ *
37
+ * The full original schema is still used by AI SDK result validation. This
38
+ * only relaxes the schema sent to Anthropic's constrained decoder.
39
+ */
40
+ export function sanitizeJsonSchema(schema: JSONSchema7): JSONSchema7 {
41
+ return sanitizeSchema(schema) as JSONSchema7;
42
+ }
43
+
44
+ function sanitizeDefinition(
45
+ definition: JSONSchema7Definition,
46
+ ): JSONSchema7Definition {
47
+ if (typeof definition === 'boolean' || !isPlainObject(definition)) {
48
+ return definition;
49
+ }
50
+
51
+ return sanitizeSchema(definition as JSONSchema7);
52
+ }
53
+
54
+ function sanitizeSchema(schema: JSONSchema7): JSONSchema7 {
55
+ const result: JSONSchema7 = {};
56
+ const schemaWithDefs = schema as JSONSchema7 & {
57
+ $defs?: Record<string, JSONSchema7Definition>;
58
+ };
59
+
60
+ if (schema.$ref != null) {
61
+ return { $ref: schema.$ref };
62
+ }
63
+
64
+ if (schema.$schema != null) {
65
+ result.$schema = schema.$schema;
66
+ }
67
+
68
+ if (schema.$id != null) {
69
+ result.$id = schema.$id;
70
+ }
71
+
72
+ if (schema.title != null) {
73
+ result.title = schema.title;
74
+ }
75
+
76
+ if (schema.description != null) {
77
+ result.description = schema.description;
78
+ }
79
+
80
+ if (schema.default !== undefined) {
81
+ result.default = schema.default;
82
+ }
83
+
84
+ if (schema.const !== undefined) {
85
+ result.const = schema.const;
86
+ }
87
+
88
+ if (schema.enum != null) {
89
+ result.enum = schema.enum;
90
+ }
91
+
92
+ if (schema.type != null) {
93
+ result.type = schema.type;
94
+ }
95
+
96
+ if (schema.anyOf != null) {
97
+ result.anyOf = schema.anyOf.map(sanitizeDefinition);
98
+ } else if (schema.oneOf != null) {
99
+ result.anyOf = schema.oneOf.map(sanitizeDefinition);
100
+ }
101
+
102
+ if (schema.allOf != null) {
103
+ result.allOf = schema.allOf.map(sanitizeDefinition);
104
+ }
105
+
106
+ if (schema.definitions != null) {
107
+ result.definitions = Object.fromEntries(
108
+ Object.entries(schema.definitions).map(([name, definition]) => [
109
+ name,
110
+ sanitizeDefinition(definition),
111
+ ]),
112
+ );
113
+ }
114
+
115
+ if (schemaWithDefs.$defs != null) {
116
+ const resultWithDefs = result as JSONSchema7 & {
117
+ $defs?: Record<string, JSONSchema7Definition>;
118
+ };
119
+ resultWithDefs.$defs = Object.fromEntries(
120
+ Object.entries(schemaWithDefs.$defs).map(([name, definition]) => [
121
+ name,
122
+ sanitizeDefinition(definition),
123
+ ]),
124
+ );
125
+ }
126
+
127
+ if (schema.type === 'object' || schema.properties != null) {
128
+ if (schema.properties != null) {
129
+ result.properties = Object.fromEntries(
130
+ Object.entries(schema.properties).map(([name, definition]) => [
131
+ name,
132
+ sanitizeDefinition(definition),
133
+ ]),
134
+ );
135
+ }
136
+
137
+ result.additionalProperties = false;
138
+
139
+ if (schema.required != null) {
140
+ result.required = schema.required;
141
+ }
142
+ }
143
+
144
+ if (schema.items != null) {
145
+ result.items = Array.isArray(schema.items)
146
+ ? schema.items.map(sanitizeDefinition)
147
+ : sanitizeDefinition(schema.items);
148
+ }
149
+
150
+ if (
151
+ typeof schema.format === 'string' &&
152
+ SUPPORTED_STRING_FORMATS.has(schema.format)
153
+ ) {
154
+ result.format = schema.format;
155
+ }
156
+
157
+ const constraintDescription = getConstraintDescription(schema);
158
+ if (constraintDescription != null) {
159
+ result.description =
160
+ result.description == null
161
+ ? constraintDescription
162
+ : `${result.description}\n${constraintDescription}`;
163
+ }
164
+
165
+ return result;
166
+ }
167
+
168
+ function getConstraintDescription(schema: JSONSchema7): string | undefined {
169
+ const descriptions = DESCRIPTION_CONSTRAINT_KEYS.flatMap(key => {
170
+ const value = schema[key];
171
+
172
+ if (value == null || value === false) {
173
+ return [];
174
+ }
175
+
176
+ return `${formatConstraintName(key)}: ${formatConstraintValue(value)}`;
177
+ });
178
+
179
+ if (
180
+ typeof schema.format === 'string' &&
181
+ !SUPPORTED_STRING_FORMATS.has(schema.format)
182
+ ) {
183
+ descriptions.push(`format: ${schema.format}`);
184
+ }
185
+
186
+ return descriptions.length === 0 ? undefined : `${descriptions.join('; ')}.`;
187
+ }
188
+
189
+ function formatConstraintName(key: string): string {
190
+ return key.replace(/[A-Z]/g, match => ` ${match.toLowerCase()}`);
191
+ }
192
+
193
+ function formatConstraintValue(value: unknown): string {
194
+ if (typeof value === 'string') {
195
+ return value;
196
+ }
197
+
198
+ return JSON.stringify(value);
199
+ }
200
+
201
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
202
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
203
+ }