package util import "encoding/json" const emptyClaudeToolInputSchema = `{"type":"object","properties":{}}` // NormalizeClaudeToolInputSchema makes a JSON Schema compatible with Claude's // requirement that a tool input schema is an object without root-level unions. func NormalizeClaudeToolInputSchema(schema []byte) []byte { var root map[string]json.RawMessage if len(schema) == 0 || json.Unmarshal(schema, &root) != nil || root == nil { return []byte(emptyClaudeToolInputSchema) } properties := claudeSchemaObject(root["properties"]) for _, unionName := range []string{"anyOf", "oneOf", "allOf"} { unionRaw, exists := root[unionName] if !exists { continue } delete(root, unionName) var branches []json.RawMessage if json.Unmarshal(unionRaw, &branches) != nil { continue } for _, branchRaw := range branches { var branch map[string]json.RawMessage if json.Unmarshal(branchRaw, &branch) != nil || !claudeSchemaCanBeObject(branch) { continue } for name, property := range claudeSchemaObject(branch["properties"]) { if _, exists = properties[name]; !exists { properties[name] = property } } if unionName == "allOf" { mergeClaudeSchemaRequired(root, branch["required"]) } } } root["type"] = json.RawMessage(`"object"`) propertiesRaw, errMarshalProperties := json.Marshal(properties) if errMarshalProperties != nil { return []byte(emptyClaudeToolInputSchema) } root["properties"] = propertiesRaw normalized, errMarshalRoot := json.Marshal(root) if errMarshalRoot != nil { return []byte(emptyClaudeToolInputSchema) } return normalized } func claudeSchemaObject(raw json.RawMessage) map[string]json.RawMessage { object := make(map[string]json.RawMessage) if len(raw) == 0 { return object } if errUnmarshal := json.Unmarshal(raw, &object); errUnmarshal != nil || object == nil { return make(map[string]json.RawMessage) } return object } func claudeSchemaCanBeObject(schema map[string]json.RawMessage) bool { typeRaw, exists := schema["type"] if !exists { return true } var schemaType string if json.Unmarshal(typeRaw, &schemaType) == nil { return schemaType == "object" } var schemaTypes []string if json.Unmarshal(typeRaw, &schemaTypes) != nil { return false } for _, candidate := range schemaTypes { if candidate == "object" { return true } } return false } func mergeClaudeSchemaRequired(root map[string]json.RawMessage, branchRequired json.RawMessage) { var required []string if rootRequired, exists := root["required"]; exists { if errUnmarshal := json.Unmarshal(rootRequired, &required); errUnmarshal != nil { required = nil } } var branchNames []string if json.Unmarshal(branchRequired, &branchNames) != nil { return } seen := make(map[string]struct{}, len(required)+len(branchNames)) for _, name := range required { seen[name] = struct{}{} } for _, name := range branchNames { if _, exists := seen[name]; exists { continue } required = append(required, name) seen[name] = struct{}{} } if len(required) == 0 { return } requiredRaw, errMarshal := json.Marshal(required) if errMarshal == nil { root["required"] = requiredRaw } } // HasUnsupportedUnicodePropertyEscape reports whether a regular expression string // contains a construct that strict upstream JSON Schema validators reject when // they compile "pattern" values on the way to a provider. // // Two constructs are known to fail, both of which Anthropic's own API accepts // verbatim (it does not validate pattern values), so they only surface after the // request is translated or forwarded: // // - Unicode property escapes (\p{...} or \P{...}): Python's built-in re module, // and validators relying on it, fail with "bad escape \p". // - The octal NUL escape (\0): validators that re-serialize or normalize the // schema reject it with "is not a 'regex'", while the equivalent \x00 // spelling is accepted. Claude Code's built-in Artifact tool emits \0 in its // NUL guards (e.g. "^[^\0]*$" on the upload_asset file_paths item). // // Dropping the attribute is safe: the client validates its own input locally, so // removing an upstream-side pattern constraint does not make a call succeed that // would otherwise have been rejected by the tool. func HasUnsupportedUnicodePropertyEscape(pattern string) bool { for i := 0; i < len(pattern); i++ { if pattern[i] != '\\' { continue } if i+1 >= len(pattern) { break } next := pattern[i+1] if (next == 'p' || next == 'P') && i+2 < len(pattern) && pattern[i+2] == '{' { return true } if next == '0' { return true } i++ // skip the escaped character (including escaped backslash) } return false } // SchemaMapKeywords lists JSON Schema keywords whose values are maps of subschemas. var SchemaMapKeywords = [...]string{ "properties", "$defs", "definitions", "patternProperties", "dependentSchemas", "dependencies", } // SchemaValueKeywords lists JSON Schema keywords with a single nested subschema or a slice of subschemas. var SchemaValueKeywords = [...]string{ "items", "prefixItems", "contains", "additionalProperties", "propertyNames", "unevaluatedProperties", "unevaluatedItems", "additionalItems", "contentSchema", "anyOf", "oneOf", "allOf", "not", "if", "then", "else", }