Skip to content

Commit 68c5db7

Browse files
committed
copy internal transformJSONSchema from anthropic
1 parent 76f8b23 commit 68c5db7

File tree

2 files changed

+165
-7
lines changed

2 files changed

+165
-7
lines changed

apps/sim/providers/anthropic/index.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import Anthropic from '@anthropic-ai/sdk'
2-
import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema'
32
import { createLogger } from '@sim/logger'
43
import type { StreamingExecution } from '@/executor/types'
54
import { MAX_TOOL_ITERATIONS } from '@/providers'
65
import {
76
checkForForcedToolUsage,
87
createReadableStreamFromAnthropicStream,
98
generateToolUseId,
9+
transformJSONSchema,
1010
} from '@/providers/anthropic/utils'
1111
import {
1212
getProviderDefaultModel,
@@ -186,12 +186,6 @@ export const anthropicProvider: ProviderConfig = {
186186
const schema = request.responseFormat.schema || request.responseFormat
187187

188188
if (useNativeStructuredOutputs) {
189-
// Use the official Anthropic SDK transformation which:
190-
// - Adds additionalProperties: false to ALL nested objects
191-
// - Removes unsupported JSON Schema constraints (minimum, maximum, minLength, etc.)
192-
// - Filters string formats to supported list only
193-
// - Moves unsupported constraints to description for model guidance
194-
// See: https://platform.claude.com/docs/en/build-with-claude/structured-outputs
195189
const transformedSchema = transformJSONSchema(schema)
196190
payload.output_format = {
197191
type: 'json_schema',

apps/sim/providers/anthropic/utils.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,170 @@ import { trackForcedToolUsage } from '@/providers/utils'
99

1010
const 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+
12176
export interface AnthropicStreamUsage {
13177
input_tokens: number
14178
output_tokens: number

0 commit comments

Comments
 (0)