forked from anthropics/anthropic-sdk-go
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathschemautil.go
More file actions
244 lines (216 loc) · 6.38 KB
/
schemautil.go
File metadata and controls
244 lines (216 loc) · 6.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
package anthropic
import (
"fmt"
"maps"
"slices"
"sort"
"strings"
)
// BetaJSONSchemaOutputFormat creates a BetaJSONOutputFormatParam from a JSON schema map.
// It transforms the schema to ensure compatibility with Anthropic's JSON schema requirements.
//
// Example:
//
// schema := map[string]any{
// "type": "object",
// "properties": map[string]any{
// "name": map[string]any{"type": "string"},
// "age": map[string]any{"type": "integer", "minimum": 0},
// },
// "required": []string{"name"},
// }
// outputFormat := BetaJSONSchemaOutputFormat(schema)
//
// msg, _ := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
// Model: anthropic.Model("claude-sonnet-4-5"),
// Messages: anthropic.F([]anthropic.BetaMessageParam{...}),
// MaxTokens: 1024,
// OutputFormat: outputFormat,
// })
func BetaJSONSchemaOutputFormat(jsonSchema map[string]any) BetaJSONOutputFormatParam {
return BetaJSONOutputFormatParam{Schema: transformSchema(jsonSchema)}
}
// BetaToolInputSchema creates a BetaToolInputSchemaParam from a JSON schema map.
// It transforms the schema to ensure compatibility with Anthropic's tool calling requirements.
func BetaToolInputSchema(jsonSchema map[string]any) BetaToolInputSchemaParam {
return BetaToolInputSchemaParam{ExtraFields: transformSchema(jsonSchema)}
}
var supportedStringFormats = []string{
"date-time",
"time",
"date",
"duration",
"email",
"hostname",
"uri",
"ipv4",
"ipv6",
"uuid",
}
var supportedSchemaKeys = []string{
// Top-level schema keys
"$ref",
"$defs",
"type",
"anyOf",
"oneOf",
"description",
"title",
// Object-specific keys
"properties",
"additionalProperties",
"required",
// Array-specific keys
"items",
"minItems",
// String-specific keys
"format",
}
// TransformSchema transforms a JSON schema to ensure it conforms to the Anthropic API's expectations.
// It returns nil if the transformed schema is empty.
//
// The transformation process:
// - Preserves $ref references
// - Transforms $defs recursively
// - Handles anyOf/oneOf by converting oneOf to anyOf
// - Ensures objects have additionalProperties: false
// - Filters string formats to only supported ones
// - Limits array minItems to 0 or 1
// - Appends unsupported properties to the description
//
// Example:
//
// schema := map[string]any{
// "type": "integer",
// "minimum": 1,
// "maximum": 10,
// "description": "A number",
// }
// transformed := TransformSchema(schema)
// // Result: {"type": "integer", "description": "A number\n\n{minimum: 1, maximum: 10}"}
func transformSchema(jsonSchema map[string]any) map[string]any {
if jsonSchema == nil {
return nil
}
strictSchema := make(map[string]any)
// Create a copy to avoid modifying the original
schemaCopy := make(map[string]any)
maps.Copy(schemaCopy, jsonSchema)
// $ref is not supported alongside other properties
if ref, ok := schemaCopy["$ref"]; ok {
strictSchema["$ref"] = ref
return strictSchema
}
for _, key := range supportedSchemaKeys {
value, exists := schemaCopy[key]
if exists {
delete(schemaCopy, key)
strictSchema[key] = value
}
}
if defs, ok := strictSchema["$defs"]; ok {
if defsMap, ok := defs.(map[string]any); ok {
strictDefs := make(map[string]any)
strictSchema["$defs"] = strictDefs
for name, schema := range defsMap {
if schemaMap, ok := schema.(map[string]any); ok {
strictDefs[name] = transformSchema(schemaMap)
}
}
}
}
typeValue, _ := strictSchema["type"]
anyOf, _ := strictSchema["anyOf"]
oneOf, _ := strictSchema["oneOf"]
if anyOfSlice, ok := anyOf.([]any); ok {
transformedVariants := make([]any, 0, len(anyOfSlice))
for _, variant := range anyOfSlice {
variantMap, ok := variant.(map[string]any)
if !ok {
continue
}
if transformed := transformSchema(variantMap); transformed != nil {
transformedVariants = append(transformedVariants, transformed)
}
}
strictSchema["anyOf"] = transformedVariants
} else if oneOfSlice, ok := oneOf.([]any); ok {
transformedVariants := make([]any, 0, len(oneOfSlice))
for _, variant := range oneOfSlice {
if variantMap, ok := variant.(map[string]any); ok {
if transformed := transformSchema(variantMap); transformed != nil {
transformedVariants = append(transformedVariants, transformed)
}
}
}
delete(strictSchema, "oneOf")
strictSchema["anyOf"] = transformedVariants
} else {
if typeValue == nil {
// schema is completely invalid, we have to bail
return nil
}
strictSchema["type"] = typeValue
}
typeStr, _ := typeValue.(string)
switch typeStr {
case "object":
if properties, ok := strictSchema["properties"]; ok {
if propsMap, ok := properties.(map[string]any); ok {
transformedProps := make(map[string]any)
for key, propSchema := range propsMap {
if propSchemaMap, ok := propSchema.(map[string]any); ok {
transformedProps[key] = transformSchema(propSchemaMap)
}
}
strictSchema["properties"] = transformedProps
}
} else {
strictSchema["properties"] = make(map[string]any)
}
strictSchema["additionalProperties"] = false
case "string":
if format, ok := strictSchema["format"]; ok {
if formatStr, ok := format.(string); ok {
if !slices.Contains(supportedStringFormats, formatStr) {
schemaCopy["format"] = format
delete(strictSchema, "format")
}
}
}
case "array":
if items, ok := strictSchema["items"]; ok {
if itemsMap, ok := items.(map[string]any); ok {
strictSchema["items"] = transformSchema(itemsMap)
}
}
if minItems, ok := strictSchema["minItems"]; ok {
if minItems != 0 && minItems != 1 {
schemaCopy["minItems"] = minItems
delete(strictSchema, "minItems")
}
}
case "boolean", "integer", "number", "null":
// These types are supported as-is
}
if len(schemaCopy) > 0 {
description := strictSchema["description"]
descStr, _ := description.(string)
// Sort keys for deterministic output
keys := make([]string, 0, len(schemaCopy))
for key := range schemaCopy {
keys = append(keys, key)
}
sort.Strings(keys)
extraProps := make([]string, 0, len(keys))
for _, key := range keys {
extraProps = append(extraProps, fmt.Sprintf("%s: %v", key, schemaCopy[key]))
}
if descStr != "" {
strictSchema["description"] = descStr + "\n\n{" + strings.Join(extraProps, ", ") + "}"
} else {
strictSchema["description"] = "{" + strings.Join(extraProps, ", ") + "}"
}
}
return strictSchema
}