genai-lite 0.13.0 → 0.14.0
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/llm/clients/AnthropicClientAdapter.d.ts +0 -8
- package/dist/llm/clients/AnthropicClientAdapter.js +43 -88
- package/dist/llm/clients/OpenAIClientAdapter.d.ts +0 -8
- package/dist/llm/clients/OpenAIClientAdapter.js +4 -33
- package/dist/llm/config.js +34 -0
- package/dist/shared/adapters/schemaUtils.d.ts +32 -0
- package/dist/shared/adapters/schemaUtils.js +87 -0
- package/package.json +1 -1
|
@@ -53,14 +53,6 @@ export declare class AnthropicClientAdapter implements ILLMClientAdapter {
|
|
|
53
53
|
private mapAnthropicUsage;
|
|
54
54
|
private mergeAnthropicUsage;
|
|
55
55
|
private createSyntheticMessage;
|
|
56
|
-
/**
|
|
57
|
-
* Recursively adds additionalProperties: false to all object schemas
|
|
58
|
-
* Required by Anthropic's strict mode for structured outputs
|
|
59
|
-
*
|
|
60
|
-
* @param schema - The JSON schema to process
|
|
61
|
-
* @returns A new schema with additionalProperties: false on all objects
|
|
62
|
-
*/
|
|
63
|
-
private addAdditionalPropertiesFalse;
|
|
64
56
|
/**
|
|
65
57
|
* Formats messages for Anthropic API with proper system message handling
|
|
66
58
|
*
|
|
@@ -9,6 +9,7 @@ exports.AnthropicClientAdapter = void 0;
|
|
|
9
9
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
10
10
|
const types_1 = require("./types");
|
|
11
11
|
const errorUtils_1 = require("../../shared/adapters/errorUtils");
|
|
12
|
+
const schemaUtils_1 = require("../../shared/adapters/schemaUtils");
|
|
12
13
|
const systemMessageUtils_1 = require("../../shared/adapters/systemMessageUtils");
|
|
13
14
|
const defaultLogger_1 = require("../../logging/defaultLogger");
|
|
14
15
|
/**
|
|
@@ -54,26 +55,15 @@ class AnthropicClientAdapter {
|
|
|
54
55
|
hasStopSequences: !!messageParams.stop_sequences,
|
|
55
56
|
useStructuredOutput,
|
|
56
57
|
});
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
headers: {
|
|
63
|
-
'anthropic-beta': 'structured-outputs-2025-11-13',
|
|
64
|
-
},
|
|
65
|
-
...requestTransportOptions,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
completion =
|
|
70
|
-
Object.keys(requestTransportOptions).length > 0
|
|
71
|
-
? await anthropic.messages.create(messageParams, requestTransportOptions)
|
|
72
|
-
: await anthropic.messages.create(messageParams);
|
|
73
|
-
}
|
|
58
|
+
// Structured and unstructured requests take the same endpoint - structured
|
|
59
|
+
// output is GA, so there is no beta header or beta endpoint involved.
|
|
60
|
+
const completion = Object.keys(requestTransportOptions).length > 0
|
|
61
|
+
? await anthropic.messages.create(messageParams, requestTransportOptions)
|
|
62
|
+
: await anthropic.messages.create(messageParams);
|
|
74
63
|
this.logger.info(`Anthropic API call successful, response ID: ${completion.id}`);
|
|
75
|
-
// Convert to standardized response format
|
|
76
|
-
//
|
|
64
|
+
// Convert to standardized response format. `create` is typed against the
|
|
65
|
+
// streaming/non-streaming param union; we never set `stream`, so this is
|
|
66
|
+
// always a Message.
|
|
77
67
|
return this.createSuccessResponse(completion, request);
|
|
78
68
|
}
|
|
79
69
|
catch (error) {
|
|
@@ -94,17 +84,12 @@ class AnthropicClientAdapter {
|
|
|
94
84
|
let started = false;
|
|
95
85
|
try {
|
|
96
86
|
const { anthropic, messageParams, requestTransportOptions, useStructuredOutput, } = this.prepareMessageRequest(request, apiKey, options);
|
|
97
|
-
this.logger.info(`Making Anthropic streaming API call for model: ${request.modelId}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
...requestTransportOptions,
|
|
104
|
-
}
|
|
105
|
-
: requestTransportOptions;
|
|
106
|
-
const stream = Object.keys(streamOptions).length > 0
|
|
107
|
-
? anthropic.messages.stream(messageParams, streamOptions)
|
|
87
|
+
this.logger.info(`Making Anthropic streaming API call for model: ${request.modelId}`, {
|
|
88
|
+
useStructuredOutput,
|
|
89
|
+
});
|
|
90
|
+
// No beta header for structured output - output_config.format is GA.
|
|
91
|
+
const stream = Object.keys(requestTransportOptions).length > 0
|
|
92
|
+
? anthropic.messages.stream(messageParams, requestTransportOptions)
|
|
108
93
|
: anthropic.messages.stream(messageParams);
|
|
109
94
|
for await (const event of stream) {
|
|
110
95
|
sawEvent = true;
|
|
@@ -254,20 +239,22 @@ class AnthropicClientAdapter {
|
|
|
254
239
|
stop_sequences: request.settings.stopSequences,
|
|
255
240
|
}),
|
|
256
241
|
};
|
|
257
|
-
// Handle structured output configuration for Anthropic
|
|
258
|
-
//
|
|
242
|
+
// Handle structured output configuration for Anthropic.
|
|
243
|
+
// Structured outputs are generally available: the stable request field is
|
|
244
|
+
// output_config.format, with no beta header. The format object carries only
|
|
245
|
+
// `type` and `schema` - the generic StructuredOutputSettings `name` and
|
|
246
|
+
// `strict` fields exist for other providers and are not serialized here.
|
|
259
247
|
if (useStructuredOutput) {
|
|
260
248
|
const so = request.settings.structuredOutput;
|
|
261
249
|
// Anthropic requires additionalProperties: false on all object schemas
|
|
262
250
|
const processedSchema = so.strict !== false
|
|
263
|
-
?
|
|
264
|
-
: so.schema;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
strict: so.strict !== false,
|
|
251
|
+
? (0, schemaUtils_1.applyStrictSchemaConstraints)({ ...so.schema })
|
|
252
|
+
: { ...so.schema };
|
|
253
|
+
messageParams.output_config = {
|
|
254
|
+
format: {
|
|
255
|
+
type: "json_schema",
|
|
256
|
+
schema: processedSchema,
|
|
257
|
+
},
|
|
271
258
|
};
|
|
272
259
|
}
|
|
273
260
|
// Handle reasoning/thinking configuration for Claude models
|
|
@@ -358,35 +345,6 @@ class AnthropicClientAdapter {
|
|
|
358
345
|
usage: accumulator.usage,
|
|
359
346
|
};
|
|
360
347
|
}
|
|
361
|
-
/**
|
|
362
|
-
* Recursively adds additionalProperties: false to all object schemas
|
|
363
|
-
* Required by Anthropic's strict mode for structured outputs
|
|
364
|
-
*
|
|
365
|
-
* @param schema - The JSON schema to process
|
|
366
|
-
* @returns A new schema with additionalProperties: false on all objects
|
|
367
|
-
*/
|
|
368
|
-
addAdditionalPropertiesFalse(schema) {
|
|
369
|
-
if (!schema || typeof schema !== 'object') {
|
|
370
|
-
return schema;
|
|
371
|
-
}
|
|
372
|
-
const processed = { ...schema };
|
|
373
|
-
// If this is an object type, add additionalProperties: false
|
|
374
|
-
if (processed.type === 'object') {
|
|
375
|
-
processed.additionalProperties = false;
|
|
376
|
-
// Process nested properties
|
|
377
|
-
if (processed.properties) {
|
|
378
|
-
processed.properties = {};
|
|
379
|
-
for (const [key, value] of Object.entries(schema.properties)) {
|
|
380
|
-
processed.properties[key] = this.addAdditionalPropertiesFalse(value);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
// Process array items
|
|
385
|
-
if (processed.items) {
|
|
386
|
-
processed.items = this.addAdditionalPropertiesFalse(schema.items);
|
|
387
|
-
}
|
|
388
|
-
return processed;
|
|
389
|
-
}
|
|
390
348
|
/**
|
|
391
349
|
* Formats messages for Anthropic API with proper system message handling
|
|
392
350
|
*
|
|
@@ -504,24 +462,15 @@ class AnthropicClientAdapter {
|
|
|
504
462
|
if (textBlocks.length === 0) {
|
|
505
463
|
throw new Error("Invalid completion structure from Anthropic API");
|
|
506
464
|
}
|
|
507
|
-
// Extract
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
//
|
|
511
|
-
if (completion.thinking_content) {
|
|
512
|
-
reasoning = completion.thinking_content;
|
|
513
|
-
}
|
|
465
|
+
// Extract reasoning from thinking content blocks. This is the only place
|
|
466
|
+
// Anthropic exposes it: `Message` has no `thinking_content` or
|
|
467
|
+
// `reasoning_details` field (the latter is an OpenRouter concept, handled in
|
|
468
|
+
// OpenRouterClientAdapter), so branches reading those could never fire.
|
|
514
469
|
const thinkingContent = completion.content
|
|
515
470
|
.filter((block) => block.type === "thinking" && typeof block.thinking === "string")
|
|
516
471
|
.map((block) => block.thinking)
|
|
517
472
|
.join("");
|
|
518
|
-
|
|
519
|
-
reasoning = thinkingContent;
|
|
520
|
-
}
|
|
521
|
-
// Check for reasoning details that need to be preserved
|
|
522
|
-
if (completion.reasoning_details) {
|
|
523
|
-
reasoning_details = completion.reasoning_details;
|
|
524
|
-
}
|
|
473
|
+
const reasoning = thinkingContent || undefined;
|
|
525
474
|
// Map Anthropic's stop reason to our standard format
|
|
526
475
|
const finishReason = this.mapAnthropicStopReason(completion.stop_reason);
|
|
527
476
|
const choice = {
|
|
@@ -536,10 +485,6 @@ class AnthropicClientAdapter {
|
|
|
536
485
|
if (reasoning && request.settings.reasoning && !request.settings.reasoning.exclude) {
|
|
537
486
|
choice.reasoning = reasoning;
|
|
538
487
|
}
|
|
539
|
-
// Always include reasoning_details if present (for tool use continuation)
|
|
540
|
-
if (reasoning_details) {
|
|
541
|
-
choice.reasoning_details = reasoning_details;
|
|
542
|
-
}
|
|
543
488
|
return {
|
|
544
489
|
id: completion.id,
|
|
545
490
|
provider: request.providerId,
|
|
@@ -565,12 +510,22 @@ class AnthropicClientAdapter {
|
|
|
565
510
|
mapAnthropicStopReason(anthropicReason) {
|
|
566
511
|
if (!anthropicReason)
|
|
567
512
|
return null;
|
|
513
|
+
// Anthropic's StopReason union is exactly:
|
|
514
|
+
// end_turn | max_tokens | stop_sequence | tool_use | pause_turn | refusal
|
|
515
|
+
// Note there is no `content_filter` - that is OpenAI's vocabulary. A model
|
|
516
|
+
// that declines on policy grounds reports `refusal`, which we normalize to
|
|
517
|
+
// `content_filter` so callers can detect a blocked completion with the same
|
|
518
|
+
// check they already use for OpenAI and Gemini.
|
|
568
519
|
const reasonMap = {
|
|
569
520
|
end_turn: "stop",
|
|
570
521
|
max_tokens: "length",
|
|
571
522
|
stop_sequence: "stop",
|
|
572
|
-
content_filter: "content_filter",
|
|
573
523
|
tool_use: "tool_calls",
|
|
524
|
+
refusal: "content_filter",
|
|
525
|
+
// Anthropic-specific: a long-running turn was paused and the caller is
|
|
526
|
+
// expected to continue it. No equivalent in the normalized vocabulary, so
|
|
527
|
+
// it maps to "other" deliberately rather than by falling through.
|
|
528
|
+
pause_turn: "other",
|
|
574
529
|
};
|
|
575
530
|
return reasonMap[anthropicReason] || "other";
|
|
576
531
|
}
|
|
@@ -52,14 +52,6 @@ export declare class OpenAIClientAdapter implements ILLMClientAdapter {
|
|
|
52
52
|
private createTransportOptions;
|
|
53
53
|
private getStreamChoiceState;
|
|
54
54
|
private createSyntheticCompletion;
|
|
55
|
-
/**
|
|
56
|
-
* Recursively adds additionalProperties: false to all object schemas
|
|
57
|
-
* Required by OpenAI's strict mode for structured outputs
|
|
58
|
-
*
|
|
59
|
-
* @param schema - The JSON schema to process
|
|
60
|
-
* @returns A new schema with additionalProperties: false on all objects
|
|
61
|
-
*/
|
|
62
|
-
private addAdditionalPropertiesFalse;
|
|
63
55
|
/**
|
|
64
56
|
* Formats messages for OpenAI API
|
|
65
57
|
*
|
|
@@ -10,6 +10,7 @@ const openai_1 = __importDefault(require("openai"));
|
|
|
10
10
|
const types_1 = require("./types");
|
|
11
11
|
const errorUtils_1 = require("../../shared/adapters/errorUtils");
|
|
12
12
|
const logprobsUtils_1 = require("../../shared/adapters/logprobsUtils");
|
|
13
|
+
const schemaUtils_1 = require("../../shared/adapters/schemaUtils");
|
|
13
14
|
const systemMessageUtils_1 = require("../../shared/adapters/systemMessageUtils");
|
|
14
15
|
const defaultLogger_1 = require("../../logging/defaultLogger");
|
|
15
16
|
/**
|
|
@@ -231,8 +232,10 @@ class OpenAIClientAdapter {
|
|
|
231
232
|
}
|
|
232
233
|
if (request.settings.structuredOutput?.schema && request.settings.structuredOutput.enabled !== false) {
|
|
233
234
|
const so = request.settings.structuredOutput;
|
|
235
|
+
// OpenAI strict mode additionally requires `required` to list every
|
|
236
|
+
// property of each object schema.
|
|
234
237
|
const processedSchema = so.strict !== false
|
|
235
|
-
?
|
|
238
|
+
? (0, schemaUtils_1.applyStrictSchemaConstraints)({ ...so.schema }, { requireAllProperties: true })
|
|
236
239
|
: so.schema;
|
|
237
240
|
completionParams.response_format = {
|
|
238
241
|
type: 'json_schema',
|
|
@@ -288,38 +291,6 @@ class OpenAIClientAdapter {
|
|
|
288
291
|
...(usage && { usage }),
|
|
289
292
|
};
|
|
290
293
|
}
|
|
291
|
-
/**
|
|
292
|
-
* Recursively adds additionalProperties: false to all object schemas
|
|
293
|
-
* Required by OpenAI's strict mode for structured outputs
|
|
294
|
-
*
|
|
295
|
-
* @param schema - The JSON schema to process
|
|
296
|
-
* @returns A new schema with additionalProperties: false on all objects
|
|
297
|
-
*/
|
|
298
|
-
addAdditionalPropertiesFalse(schema) {
|
|
299
|
-
if (!schema || typeof schema !== 'object') {
|
|
300
|
-
return schema;
|
|
301
|
-
}
|
|
302
|
-
const processed = { ...schema };
|
|
303
|
-
// If this is an object type, add additionalProperties: false and ensure required includes all properties
|
|
304
|
-
if (processed.type === 'object') {
|
|
305
|
-
processed.additionalProperties = false;
|
|
306
|
-
// Process nested properties
|
|
307
|
-
if (processed.properties) {
|
|
308
|
-
const propertyKeys = Object.keys(schema.properties);
|
|
309
|
-
processed.properties = {};
|
|
310
|
-
for (const [key, value] of Object.entries(schema.properties)) {
|
|
311
|
-
processed.properties[key] = this.addAdditionalPropertiesFalse(value);
|
|
312
|
-
}
|
|
313
|
-
// OpenAI strict mode requires 'required' to include ALL properties
|
|
314
|
-
processed.required = propertyKeys;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
// Process array items
|
|
318
|
-
if (processed.items) {
|
|
319
|
-
processed.items = this.addAdditionalPropertiesFalse(schema.items);
|
|
320
|
-
}
|
|
321
|
-
return processed;
|
|
322
|
-
}
|
|
323
294
|
/**
|
|
324
295
|
* Formats messages for OpenAI API
|
|
325
296
|
*
|
package/dist/llm/config.js
CHANGED
|
@@ -873,6 +873,10 @@ exports.SUPPORTED_MODELS = [
|
|
|
873
873
|
outputType: 'summary',
|
|
874
874
|
requiresStreamingAbove: 21333,
|
|
875
875
|
},
|
|
876
|
+
structuredOutput: {
|
|
877
|
+
supported: true,
|
|
878
|
+
strictMode: true,
|
|
879
|
+
},
|
|
876
880
|
},
|
|
877
881
|
{
|
|
878
882
|
id: "claude-sonnet-4-5-20250929",
|
|
@@ -897,6 +901,10 @@ exports.SUPPORTED_MODELS = [
|
|
|
897
901
|
outputType: 'summary',
|
|
898
902
|
requiresStreamingAbove: 21333,
|
|
899
903
|
},
|
|
904
|
+
structuredOutput: {
|
|
905
|
+
supported: true,
|
|
906
|
+
strictMode: true,
|
|
907
|
+
},
|
|
900
908
|
},
|
|
901
909
|
{
|
|
902
910
|
id: "claude-haiku-4-5-20251001",
|
|
@@ -921,8 +929,14 @@ exports.SUPPORTED_MODELS = [
|
|
|
921
929
|
outputType: 'summary',
|
|
922
930
|
requiresStreamingAbove: 21333,
|
|
923
931
|
},
|
|
932
|
+
structuredOutput: {
|
|
933
|
+
supported: true,
|
|
934
|
+
strictMode: true,
|
|
935
|
+
},
|
|
924
936
|
},
|
|
925
937
|
// Anthropic Models - Claude 4 Series
|
|
938
|
+
// Note: Anthropic's structured outputs are generally available for Claude 4.5
|
|
939
|
+
// and later models only, so the entries below declare it unsupported.
|
|
926
940
|
{
|
|
927
941
|
id: "claude-sonnet-4-20250514",
|
|
928
942
|
name: "Claude Sonnet 4",
|
|
@@ -946,6 +960,10 @@ exports.SUPPORTED_MODELS = [
|
|
|
946
960
|
outputType: 'summary',
|
|
947
961
|
requiresStreamingAbove: 21333,
|
|
948
962
|
},
|
|
963
|
+
structuredOutput: {
|
|
964
|
+
supported: false,
|
|
965
|
+
notes: "Anthropic structured outputs require Claude 4.5 or later",
|
|
966
|
+
},
|
|
949
967
|
},
|
|
950
968
|
{
|
|
951
969
|
id: "claude-opus-4-20250514",
|
|
@@ -970,6 +988,10 @@ exports.SUPPORTED_MODELS = [
|
|
|
970
988
|
outputType: 'summary',
|
|
971
989
|
requiresStreamingAbove: 21333,
|
|
972
990
|
},
|
|
991
|
+
structuredOutput: {
|
|
992
|
+
supported: false,
|
|
993
|
+
notes: "Anthropic structured outputs require Claude 4.5 or later",
|
|
994
|
+
},
|
|
973
995
|
},
|
|
974
996
|
{
|
|
975
997
|
id: "claude-3-7-sonnet-20250219",
|
|
@@ -994,6 +1016,10 @@ exports.SUPPORTED_MODELS = [
|
|
|
994
1016
|
outputType: 'full',
|
|
995
1017
|
requiresStreamingAbove: 21333,
|
|
996
1018
|
},
|
|
1019
|
+
structuredOutput: {
|
|
1020
|
+
supported: false,
|
|
1021
|
+
notes: "Anthropic structured outputs require Claude 4.5 or later",
|
|
1022
|
+
},
|
|
997
1023
|
},
|
|
998
1024
|
{
|
|
999
1025
|
id: "claude-3-5-sonnet-20241022",
|
|
@@ -1008,6 +1034,10 @@ exports.SUPPORTED_MODELS = [
|
|
|
1008
1034
|
supportsPromptCache: true,
|
|
1009
1035
|
cacheWritesPrice: 3.75,
|
|
1010
1036
|
cacheReadsPrice: 0.3,
|
|
1037
|
+
structuredOutput: {
|
|
1038
|
+
supported: false,
|
|
1039
|
+
notes: "Anthropic structured outputs require Claude 4.5 or later",
|
|
1040
|
+
},
|
|
1011
1041
|
},
|
|
1012
1042
|
{
|
|
1013
1043
|
id: "claude-3-5-haiku-20241022",
|
|
@@ -1022,6 +1052,10 @@ exports.SUPPORTED_MODELS = [
|
|
|
1022
1052
|
supportsPromptCache: true,
|
|
1023
1053
|
cacheWritesPrice: 1.0,
|
|
1024
1054
|
cacheReadsPrice: 0.08,
|
|
1055
|
+
structuredOutput: {
|
|
1056
|
+
supported: false,
|
|
1057
|
+
notes: "Anthropic structured outputs require Claude 4.5 or later",
|
|
1058
|
+
},
|
|
1025
1059
|
},
|
|
1026
1060
|
// Google Gemini Models - Gemini 3 Series (Preview)
|
|
1027
1061
|
{
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface StrictSchemaOptions {
|
|
2
|
+
/**
|
|
3
|
+
* When true, every object schema's `required` is rewritten to list all of its
|
|
4
|
+
* properties. OpenAI's strict mode demands this; Anthropic's does not.
|
|
5
|
+
*
|
|
6
|
+
* @default false
|
|
7
|
+
*/
|
|
8
|
+
requireAllProperties?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Returns a deep copy of `schema` with `additionalProperties: false` set on every
|
|
12
|
+
* object schema, as required by OpenAI's and Anthropic's strict structured-output
|
|
13
|
+
* modes.
|
|
14
|
+
*
|
|
15
|
+
* The input is never mutated.
|
|
16
|
+
*
|
|
17
|
+
* Traversal covers `properties`, `items` (single or tuple form), `prefixItems`,
|
|
18
|
+
* `$defs`, `definitions`, `anyOf`, `oneOf`, `allOf`, and `not`. Note that `$ref`
|
|
19
|
+
* pointers are **not** resolved — they are left as-is, and the schemas they point
|
|
20
|
+
* at are constrained where they are *defined* (typically under `$defs`), which is
|
|
21
|
+
* what the providers validate. Both providers reject genuinely recursive schemas
|
|
22
|
+
* anyway, so resolving refs would buy nothing.
|
|
23
|
+
*
|
|
24
|
+
* Cyclic *JavaScript* object graphs (a schema object reachable from itself) are
|
|
25
|
+
* handled: each input node is copied at most once and revisits reuse that copy,
|
|
26
|
+
* so the walk terminates and shared subschemas stay shared.
|
|
27
|
+
*
|
|
28
|
+
* @param schema - The JSON schema to process
|
|
29
|
+
* @param options - Provider-specific strictness tweaks
|
|
30
|
+
* @returns A new schema with strict-mode constraints applied
|
|
31
|
+
*/
|
|
32
|
+
export declare function applyStrictSchemaConstraints<T>(schema: T, options?: StrictSchemaOptions): T;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// AI Summary: Shared JSON Schema preparation for providers whose strict
|
|
3
|
+
// structured-output mode requires additionalProperties: false on every object
|
|
4
|
+
// schema. Traverses properties, items (incl. tuple form), $defs/definitions,
|
|
5
|
+
// anyOf/oneOf/allOf, prefixItems and not, and is safe against cyclic input.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.applyStrictSchemaConstraints = applyStrictSchemaConstraints;
|
|
8
|
+
/** Keywords whose value is a map of name -> subschema. */
|
|
9
|
+
const SUBSCHEMA_MAPS = ["properties", "$defs", "definitions"];
|
|
10
|
+
/** Keywords whose value is an array of subschemas. */
|
|
11
|
+
const SUBSCHEMA_ARRAYS = ["anyOf", "oneOf", "allOf", "prefixItems"];
|
|
12
|
+
/**
|
|
13
|
+
* Returns a deep copy of `schema` with `additionalProperties: false` set on every
|
|
14
|
+
* object schema, as required by OpenAI's and Anthropic's strict structured-output
|
|
15
|
+
* modes.
|
|
16
|
+
*
|
|
17
|
+
* The input is never mutated.
|
|
18
|
+
*
|
|
19
|
+
* Traversal covers `properties`, `items` (single or tuple form), `prefixItems`,
|
|
20
|
+
* `$defs`, `definitions`, `anyOf`, `oneOf`, `allOf`, and `not`. Note that `$ref`
|
|
21
|
+
* pointers are **not** resolved — they are left as-is, and the schemas they point
|
|
22
|
+
* at are constrained where they are *defined* (typically under `$defs`), which is
|
|
23
|
+
* what the providers validate. Both providers reject genuinely recursive schemas
|
|
24
|
+
* anyway, so resolving refs would buy nothing.
|
|
25
|
+
*
|
|
26
|
+
* Cyclic *JavaScript* object graphs (a schema object reachable from itself) are
|
|
27
|
+
* handled: each input node is copied at most once and revisits reuse that copy,
|
|
28
|
+
* so the walk terminates and shared subschemas stay shared.
|
|
29
|
+
*
|
|
30
|
+
* @param schema - The JSON schema to process
|
|
31
|
+
* @param options - Provider-specific strictness tweaks
|
|
32
|
+
* @returns A new schema with strict-mode constraints applied
|
|
33
|
+
*/
|
|
34
|
+
function applyStrictSchemaConstraints(schema, options = {}) {
|
|
35
|
+
return walk(schema, options, new Map());
|
|
36
|
+
}
|
|
37
|
+
function walk(node, options, seen) {
|
|
38
|
+
if (!node || typeof node !== "object") {
|
|
39
|
+
return node;
|
|
40
|
+
}
|
|
41
|
+
const cached = seen.get(node);
|
|
42
|
+
if (cached !== undefined) {
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(node)) {
|
|
46
|
+
const copy = [];
|
|
47
|
+
seen.set(node, copy);
|
|
48
|
+
for (const entry of node) {
|
|
49
|
+
copy.push(walk(entry, options, seen));
|
|
50
|
+
}
|
|
51
|
+
return copy;
|
|
52
|
+
}
|
|
53
|
+
const source = node;
|
|
54
|
+
const processed = { ...source };
|
|
55
|
+
// Registered before recursing so a cycle resolves to this same object.
|
|
56
|
+
seen.set(node, processed);
|
|
57
|
+
if (source.type === "object") {
|
|
58
|
+
processed.additionalProperties = false;
|
|
59
|
+
const properties = source.properties;
|
|
60
|
+
if (options.requireAllProperties && properties && typeof properties === "object") {
|
|
61
|
+
processed.required = Object.keys(properties);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const key of SUBSCHEMA_MAPS) {
|
|
65
|
+
const value = source[key];
|
|
66
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const [name, subschema] of Object.entries(value)) {
|
|
69
|
+
out[name] = walk(subschema, options, seen);
|
|
70
|
+
}
|
|
71
|
+
processed[key] = out;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const key of SUBSCHEMA_ARRAYS) {
|
|
75
|
+
const value = source[key];
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
processed[key] = value.map((subschema) => walk(subschema, options, seen));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (source.items !== undefined) {
|
|
81
|
+
processed.items = walk(source.items, options, seen);
|
|
82
|
+
}
|
|
83
|
+
if (source.not !== undefined) {
|
|
84
|
+
processed.not = walk(source.not, options, seen);
|
|
85
|
+
}
|
|
86
|
+
return processed;
|
|
87
|
+
}
|