mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-22 15:44:58 +08:00
- Strip octal NUL (`\0`) escapes in regex patterns to prevent rejection by strict upstream JSON Schema validators - Update tool schema normalization fast-path to check for serialized `\0` escape sequences Closes: #6025
191 lines
5.2 KiB
Go
191 lines
5.2 KiB
Go
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",
|
|
}
|