Skip to content

Commit 0971462

Browse files
committed
fix: normalize invalid required fields in tool schemas
1 parent fbf203c commit 0971462

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

proxy/translator.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,7 @@ func stripUnsupportedSchemaKeys(schema map[string]interface{}) {
13711371

13721372
func sanitizeSchemaForUpstream(schema map[string]interface{}) {
13731373
stripUnsupportedSchemaKeys(schema)
1374+
normalizeSchemaRequiredFields(schema)
13741375
ensureArrayItems(schema)
13751376
}
13761377

@@ -1485,6 +1486,56 @@ func ensureFunctionParametersRootObject(schema map[string]any) {
14851486
}
14861487
}
14871488

1489+
func normalizeSchemaRequiredFields(schema map[string]interface{}) {
1490+
if rawRequired, exists := schema["required"]; exists {
1491+
required, ok := rawRequired.([]interface{})
1492+
if !ok {
1493+
delete(schema, "required")
1494+
} else {
1495+
cleaned := make([]interface{}, 0, len(required))
1496+
for _, item := range required {
1497+
if name, ok := item.(string); ok && strings.TrimSpace(name) != "" {
1498+
cleaned = append(cleaned, name)
1499+
}
1500+
}
1501+
if len(cleaned) == 0 {
1502+
delete(schema, "required")
1503+
} else {
1504+
schema["required"] = cleaned
1505+
}
1506+
}
1507+
}
1508+
if props, ok := schema["properties"].(map[string]interface{}); ok {
1509+
for _, v := range props {
1510+
if sub, ok := v.(map[string]interface{}); ok {
1511+
normalizeSchemaRequiredFields(sub)
1512+
}
1513+
}
1514+
}
1515+
if items, ok := schema["items"].(map[string]interface{}); ok {
1516+
normalizeSchemaRequiredFields(items)
1517+
}
1518+
for _, key := range []string{"allOf", "anyOf", "oneOf"} {
1519+
if arr, ok := schema[key].([]interface{}); ok {
1520+
for _, item := range arr {
1521+
if sub, ok := item.(map[string]interface{}); ok {
1522+
normalizeSchemaRequiredFields(sub)
1523+
}
1524+
}
1525+
}
1526+
}
1527+
if addProps, ok := schema["additionalProperties"].(map[string]interface{}); ok {
1528+
normalizeSchemaRequiredFields(addProps)
1529+
}
1530+
if defs, ok := schema["$defs"].(map[string]interface{}); ok {
1531+
for _, v := range defs {
1532+
if sub, ok := v.(map[string]interface{}); ok {
1533+
normalizeSchemaRequiredFields(sub)
1534+
}
1535+
}
1536+
}
1537+
}
1538+
14881539
// ensureArrayItems 递归为缺失 items 的数组 schema 补上空 schema,
14891540
// 兼容上游对 array 必须声明 items 的校验。
14901541
func ensureArrayItems(schema map[string]interface{}) {

proxy/translator_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,46 @@ func TestTranslateRequest_FillsMissingArrayItemsInToolSchema(t *testing.T) {
141141
}
142142
}
143143

144+
func TestTranslateRequest_DropsInvalidRequiredInToolSchema(t *testing.T) {
145+
raw := []byte(`{
146+
"model":"gpt-5.4",
147+
"messages":[{"role":"user","content":"test"}],
148+
"tools":[
149+
{
150+
"type":"function",
151+
"function":{
152+
"name":"session_status",
153+
"parameters":{
154+
"type":"object",
155+
"required":null,
156+
"properties":{
157+
"session_id":{"type":"string"},
158+
"metadata":{
159+
"type":"object",
160+
"required":[null, "", "kind"],
161+
"properties":{"kind":{"type":"string"}}
162+
}
163+
}
164+
}
165+
}
166+
}
167+
]
168+
}`)
169+
170+
got, err := TranslateRequest(raw)
171+
if err != nil {
172+
t.Fatalf("TranslateRequest returned error: %v", err)
173+
}
174+
175+
if required := gjson.GetBytes(got, "tools.0.parameters.required"); required.Exists() {
176+
t.Fatalf("null required should be removed from tool schema, got %s; body=%s", required.Raw, got)
177+
}
178+
nestedRequired := gjson.GetBytes(got, "tools.0.parameters.properties.metadata.required")
179+
if nestedRequired.Raw != `["kind"]` {
180+
t.Fatalf("nested required should keep only string entries, got %s; body=%s", nestedRequired.Raw, got)
181+
}
182+
}
183+
144184
func TestPrepareResponsesBody_FillsMissingArrayItemsInToolSchema(t *testing.T) {
145185
raw := []byte(`{
146186
"model":"gpt-5.4",
@@ -167,6 +207,41 @@ func TestPrepareResponsesBody_FillsMissingArrayItemsInToolSchema(t *testing.T) {
167207
}
168208
}
169209

210+
func TestPrepareResponsesBody_DropsInvalidRequiredInToolSchema(t *testing.T) {
211+
raw := []byte(`{
212+
"model":"gpt-5.4",
213+
"input":"test",
214+
"tools":[
215+
{
216+
"type":"function",
217+
"name":"session_status",
218+
"parameters":{
219+
"type":"object",
220+
"required":null,
221+
"properties":{
222+
"session_id":{"type":"string"},
223+
"metadata":{
224+
"type":"object",
225+
"required":[null, "", "kind"],
226+
"properties":{"kind":{"type":"string"}}
227+
}
228+
}
229+
}
230+
}
231+
]
232+
}`)
233+
234+
got, _ := PrepareResponsesBody(raw)
235+
236+
if required := gjson.GetBytes(got, "tools.0.parameters.required"); required.Exists() {
237+
t.Fatalf("null required should be removed from tool schema, got %s; body=%s", required.Raw, got)
238+
}
239+
nestedRequired := gjson.GetBytes(got, "tools.0.parameters.properties.metadata.required")
240+
if nestedRequired.Raw != `["kind"]` {
241+
t.Fatalf("nested required should keep only string entries, got %s; body=%s", nestedRequired.Raw, got)
242+
}
243+
}
244+
170245
func TestPrepareResponsesBody_DefaultsNullFunctionToolParameters(t *testing.T) {
171246
raw := []byte(`{
172247
"model":"gpt-5.4",

0 commit comments

Comments
 (0)