@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.
@@ -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
- if (schema.definitions != null) {
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];