feat(translator): sanitize parametersJsonSchema in OpenAI to Gemini request handling

- Applied `CleanJSONSchemaForGemini` to ensure compatibility by removing unsupported fields and cleaning schema requirements.
- Added test cases to validate schema transformation, ensuring only necessary `required` fields are retained.
- Enhanced both request and response flows to consistently clean and sanitize tool parameter schemas.

Closes: #3863
This commit is contained in:
Luis Pater
2026-06-20 13:34:05 +08:00
parent a79ae80f6e
commit b4bec344f6
4 changed files with 88 additions and 1 deletions
@@ -363,6 +363,9 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
fnRawBytes := []byte(fnRaw)
fnRawBytes, _ = sjson.SetBytes(fnRawBytes, "name", util.SanitizeFunctionName(fn.Get("name").String()))
fnRaw = string(fnRawBytes)
if parameters := gjson.Get(fnRaw, "parametersJsonSchema"); parameters.Exists() {
fnRaw, _ = sjson.SetRaw(fnRaw, "parametersJsonSchema", util.CleanJSONSchemaForGemini(parameters.Raw))
}
fnRaw, _ = sjson.Delete(fnRaw, "strict")
if !hasFunction {
functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations", []byte("[]"))
@@ -58,3 +58,46 @@ func TestConvertOpenAIRequestToGeminiPreservesInputAudio(t *testing.T) {
t.Fatalf("audio data = %q, want %q", got, "SUQzBA==")
}
}
func TestConvertOpenAIRequestToGeminiCleansToolSchemaRequiredFields(t *testing.T) {
inputJSON := `{
"model": "gemini-2.0-flash",
"messages": [{"role": "user", "content": "hi"}],
"tools": [{
"type": "function",
"function": {
"name": "search_company",
"description": "Search",
"parameters": {
"type": "object",
"title": "SearchCompany",
"properties": {
"country": {"type": "string"},
"industry": {"type": "string"}
},
"required": ["country", "industry", "stale_field", "another_stale"]
}
}
}]
}`
output := ConvertOpenAIRequestToGemini("gemini-2.0-flash", []byte(inputJSON), false)
schema := gjson.GetBytes(output, "tools.0.functionDeclarations.0.parametersJsonSchema")
if !schema.Exists() {
t.Fatalf("parametersJsonSchema missing. Output: %s", output)
}
if schema.Get("title").Exists() {
t.Fatalf("schema title should be removed. Output: %s", output)
}
required := schema.Get("required").Array()
if len(required) != 2 {
t.Fatalf("required length = %d, want 2. Schema: %s", len(required), schema.Raw)
}
if got := required[0].String(); got != "country" {
t.Fatalf("required[0] = %q, want country. Schema: %s", got, schema.Raw)
}
if got := required[1].String(); got != "industry" {
t.Fatalf("required[1] = %q, want industry. Schema: %s", got, schema.Raw)
}
}
@@ -395,7 +395,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
funcDecl, _ = sjson.SetBytes(funcDecl, "description", desc.String())
}
if params := tool.Get("parameters"); params.Exists() {
funcDecl, _ = sjson.SetRawBytes(funcDecl, "parametersJsonSchema", []byte(params.Raw))
funcDecl, _ = sjson.SetRawBytes(funcDecl, "parametersJsonSchema", []byte(util.CleanJSONSchemaForGemini(params.Raw)))
}
geminiTools, _ = sjson.SetRawBytes(geminiTools, "0.functionDeclarations.-1", funcDecl)
@@ -245,6 +245,47 @@ func TestConvertOpenAIResponsesRequestToGemini_SystemAndDeveloperRoles(t *testin
}
}
func TestConvertOpenAIResponsesRequestToGeminiCleansToolSchemaRequiredFields(t *testing.T) {
inputJSON := `{
"model": "gemini-2.0-flash",
"input": "hi",
"tools": [{
"type": "function",
"name": "search_company",
"description": "Search",
"parameters": {
"type": "object",
"title": "SearchCompany",
"properties": {
"country": {"type": "string"},
"industry": {"type": "string"}
},
"required": ["country", "industry", "stale_field", "another_stale"]
}
}]
}`
output := ConvertOpenAIResponsesRequestToGemini("gemini-2.0-flash", []byte(inputJSON), false)
schema := gjson.GetBytes(output, "tools.0.functionDeclarations.0.parametersJsonSchema")
if !schema.Exists() {
t.Fatalf("parametersJsonSchema missing. Output: %s", output)
}
if schema.Get("title").Exists() {
t.Fatalf("schema title should be removed. Output: %s", output)
}
required := schema.Get("required").Array()
if len(required) != 2 {
t.Fatalf("required length = %d, want 2. Schema: %s", len(required), schema.Raw)
}
if got := required[0].String(); got != "country" {
t.Fatalf("required[0] = %q, want country. Schema: %s", got, schema.Raw)
}
if got := required[1].String(); got != "industry" {
t.Fatalf("required[1] = %q, want industry. Schema: %s", got, schema.Raw)
}
}
func validResponsesGPTReasoningSignature() string {
raw := make([]byte, 1+8+16+16+32)
raw[0] = 0x80