@@ -9,6 +9,170 @@ import { trackForcedToolUsage } from '@/providers/utils'
99
1010const logger = createLogger ( 'AnthropicUtils' )
1111
12+ /**
13+ * Supported string formats for Anthropic structured outputs.
14+ * @see https://platform.claude.com/docs/en/build-with-claude/structured-outputs
15+ */
16+ const SUPPORTED_STRING_FORMATS = new Set ( [
17+ 'date-time' ,
18+ 'time' ,
19+ 'date' ,
20+ 'duration' ,
21+ 'email' ,
22+ 'hostname' ,
23+ 'uri' ,
24+ 'ipv4' ,
25+ 'ipv6' ,
26+ 'uuid' ,
27+ ] )
28+
29+ /**
30+ * Removes a key from an object and returns its value.
31+ */
32+ function pop < T extends Record < string , unknown > , K extends keyof T > ( obj : T , key : K ) : T [ K ] {
33+ const value = obj [ key ]
34+ delete obj [ key ]
35+ return value
36+ }
37+
38+ /**
39+ * Transforms a JSON schema to be compatible with Anthropic's structured outputs.
40+ *
41+ * This function is adapted from the official Anthropic SDK (MIT licensed).
42+ * @see https://github.com/anthropics/anthropic-sdk-typescript
43+ *
44+ * It performs the following transformations:
45+ * - Adds `additionalProperties: false` to ALL object types (required by Anthropic)
46+ * - Removes unsupported JSON Schema constraints (minimum, maximum, minLength, etc.)
47+ * - Filters string formats to only supported ones
48+ * - Moves unsupported constraints to description for model guidance
49+ */
50+ export function transformJSONSchema ( jsonSchema : Record < string , unknown > ) : Record < string , unknown > {
51+ const workingCopy = JSON . parse ( JSON . stringify ( jsonSchema ) )
52+ return _transformJSONSchema ( workingCopy )
53+ }
54+
55+ function _transformJSONSchema ( jsonSchema : Record < string , unknown > ) : Record < string , unknown > {
56+ const strictSchema : Record < string , unknown > = { }
57+
58+ const ref = pop ( jsonSchema , '$ref' )
59+ if ( ref !== undefined ) {
60+ strictSchema . $ref = ref
61+ return strictSchema
62+ }
63+
64+ const defs = pop ( jsonSchema , '$defs' ) as Record < string , Record < string , unknown > > | undefined
65+ if ( defs !== undefined ) {
66+ const strictDefs : Record < string , unknown > = { }
67+ strictSchema . $defs = strictDefs
68+ for ( const [ name , defSchema ] of Object . entries ( defs ) ) {
69+ strictDefs [ name ] = _transformJSONSchema ( defSchema )
70+ }
71+ }
72+
73+ const type = pop ( jsonSchema , 'type' )
74+ const anyOf = pop ( jsonSchema , 'anyOf' ) as Record < string , unknown > [ ] | undefined
75+ const oneOf = pop ( jsonSchema , 'oneOf' ) as Record < string , unknown > [ ] | undefined
76+ const allOf = pop ( jsonSchema , 'allOf' ) as Record < string , unknown > [ ] | undefined
77+
78+ if ( Array . isArray ( anyOf ) ) {
79+ strictSchema . anyOf = anyOf . map ( ( variant ) => _transformJSONSchema ( variant ) )
80+ } else if ( Array . isArray ( oneOf ) ) {
81+ strictSchema . anyOf = oneOf . map ( ( variant ) => _transformJSONSchema ( variant ) )
82+ } else if ( Array . isArray ( allOf ) ) {
83+ strictSchema . allOf = allOf . map ( ( entry ) => _transformJSONSchema ( entry ) )
84+ } else {
85+ if ( type === undefined ) {
86+ throw new Error ( 'JSON schema must have a type defined if anyOf/oneOf/allOf are not used' )
87+ }
88+ strictSchema . type = type
89+ }
90+
91+ const description = pop ( jsonSchema , 'description' )
92+ if ( description !== undefined ) {
93+ strictSchema . description = description
94+ }
95+
96+ const title = pop ( jsonSchema , 'title' )
97+ if ( title !== undefined ) {
98+ strictSchema . title = title
99+ }
100+
101+ if ( type === 'object' ) {
102+ const properties = ( pop ( jsonSchema , 'properties' ) || { } ) as Record <
103+ string ,
104+ Record < string , unknown >
105+ >
106+ strictSchema . properties = Object . fromEntries (
107+ Object . entries ( properties ) . map ( ( [ key , propSchema ] ) => [ key , _transformJSONSchema ( propSchema ) ] )
108+ )
109+ pop ( jsonSchema , 'additionalProperties' )
110+ strictSchema . additionalProperties = false
111+
112+ const required = pop ( jsonSchema , 'required' )
113+ if ( required !== undefined ) {
114+ strictSchema . required = required
115+ }
116+ } else if ( type === 'string' ) {
117+ const format = pop ( jsonSchema , 'format' ) as string | undefined
118+ if ( format !== undefined && SUPPORTED_STRING_FORMATS . has ( format ) ) {
119+ strictSchema . format = format
120+ } else if ( format !== undefined ) {
121+ jsonSchema . format = format
122+ }
123+
124+ const enumValues = pop ( jsonSchema , 'enum' )
125+ if ( enumValues !== undefined ) {
126+ strictSchema . enum = enumValues
127+ }
128+
129+ const constValue = pop ( jsonSchema , 'const' )
130+ if ( constValue !== undefined ) {
131+ strictSchema . const = constValue
132+ }
133+ } else if ( type === 'array' ) {
134+ const items = pop ( jsonSchema , 'items' ) as Record < string , unknown > | undefined
135+ if ( items !== undefined ) {
136+ strictSchema . items = _transformJSONSchema ( items )
137+ }
138+
139+ const minItems = pop ( jsonSchema , 'minItems' ) as number | undefined
140+ if ( minItems !== undefined && ( minItems === 0 || minItems === 1 ) ) {
141+ strictSchema . minItems = minItems
142+ } else if ( minItems !== undefined ) {
143+ jsonSchema . minItems = minItems
144+ }
145+ } else if ( type === 'number' || type === 'integer' ) {
146+ const enumValues = pop ( jsonSchema , 'enum' )
147+ if ( enumValues !== undefined ) {
148+ strictSchema . enum = enumValues
149+ }
150+
151+ const constValue = pop ( jsonSchema , 'const' )
152+ if ( constValue !== undefined ) {
153+ strictSchema . const = constValue
154+ }
155+ } else if ( type === 'boolean' ) {
156+ const constValue = pop ( jsonSchema , 'const' )
157+ if ( constValue !== undefined ) {
158+ strictSchema . const = constValue
159+ }
160+ }
161+
162+ if ( Object . keys ( jsonSchema ) . length > 0 ) {
163+ const existingDescription = strictSchema . description as string | undefined
164+ strictSchema . description =
165+ ( existingDescription ? `${ existingDescription } \n\n` : '' ) +
166+ '{' +
167+ Object . entries ( jsonSchema )
168+ . map ( ( [ key , value ] ) => `${ key } : ${ JSON . stringify ( value ) } ` )
169+ . join ( ', ' ) +
170+ '}'
171+ }
172+
173+ return strictSchema
174+ }
175+
12176export interface AnthropicStreamUsage {
13177 input_tokens : number
14178 output_tokens : number
0 commit comments