From bc652c7bf0a03a639a6c29484965bd8203a9bd5f Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sat, 20 Jun 2026 10:14:39 +0800 Subject: [PATCH] feat(translator): add support for `text.format` conversion in OpenAI to Gemini requests - Implemented `applyOpenAIResponsesTextFormatToGemini` to handle `json_object` and `json_schema` formats. - Updated generation config to set appropriate `responseMimeType` and optional `responseJsonSchema`. - Introduced unit tests to validate correct handling of `text.format` conversion scenarios. Closes: #3721 --- .../gemini_openai-responses_request.go | 37 ++++++++ .../gemini_openai-responses_request_test.go | 87 +++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index 0907519b..f38df578 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -445,6 +445,8 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte out, _ = sjson.SetBytes(out, "generationConfig.stopSequences", sequences) } + out = applyOpenAIResponsesTextFormatToGemini(out, root) + // Apply thinking configuration: convert OpenAI Responses API reasoning.effort to Gemini thinkingConfig. // Inline translation-only mapping; capability checks happen later in ApplyThinking. re := root.Get("reasoning.effort") @@ -470,3 +472,38 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte func openAIResponsesGeminiThoughtSignature(rawSignature string) string { return sigcompat.GeminiReplaySignatureOrBypass(rawSignature, sigcompat.SignatureBlockKindGeminiModelPart) } + +func applyOpenAIResponsesTextFormatToGemini(out []byte, root gjson.Result) []byte { + textFormat := root.Get("text.format") + if !textFormat.Exists() { + return out + } + + formatType := strings.ToLower(strings.TrimSpace(textFormat.Get("type").String())) + switch formatType { + case "json_object": + out = ensureGeminiGenerationConfig(out) + out, _ = sjson.SetBytes(out, "generationConfig.responseMimeType", "application/json") + case "json_schema": + out = ensureGeminiGenerationConfig(out) + out, _ = sjson.SetBytes(out, "generationConfig.responseMimeType", "application/json") + out, _ = sjson.DeleteBytes(out, "generationConfig.responseSchema") + + schema := textFormat.Get("schema") + if !schema.Exists() { + schema = textFormat.Get("json_schema.schema") + } + if schema.Exists() { + out, _ = sjson.SetRawBytes(out, "generationConfig.responseJsonSchema", []byte(schema.Raw)) + } + } + + return out +} + +func ensureGeminiGenerationConfig(out []byte) []byte { + if !gjson.GetBytes(out, "generationConfig").Exists() { + out, _ = sjson.SetRawBytes(out, "generationConfig", []byte(`{}`)) + } + return out +} diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request_test.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request_test.go index 071fadc8..1bd14145 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request_test.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request_test.go @@ -38,6 +38,93 @@ func TestConvertOpenAIResponsesRequestToGemini_StripsTrailingAssistantPrefill(t } } +func TestConvertOpenAIResponsesRequestToGemini_TextFormatJSONSchema(t *testing.T) { + inputJSON := `{ + "model": "gemini-flash-lite", + "temperature": 0.2, + "input": [ + { + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Return structured JSON." + } + ] + } + ], + "text": { + "format": { + "type": "json_schema", + "strict": true, + "name": "response", + "schema": { + "type": "object", + "properties": { + "cleanedContent": { + "type": "string" + } + }, + "required": [ + "cleanedContent" + ], + "additionalProperties": false + } + } + } + }` + + output := ConvertOpenAIResponsesRequestToGemini("gemini-3.1-flash-lite", []byte(inputJSON), false) + result := gjson.ParseBytes(output) + genConfig := result.Get("generationConfig") + + if got := genConfig.Get("responseMimeType").String(); got != "application/json" { + t.Fatalf("responseMimeType = %q, want application/json. Output: %s", got, output) + } + schema := genConfig.Get("responseJsonSchema") + if !schema.Exists() { + t.Fatalf("responseJsonSchema missing. Output: %s", output) + } + if genConfig.Get("responseSchema").Exists() { + t.Fatalf("responseSchema should not be set with responseJsonSchema. Output: %s", output) + } + if got := schema.Get("type").String(); got != "object" { + t.Fatalf("schema type = %q, want object. Output: %s", got, output) + } + if got := schema.Get("properties.cleanedContent.type").String(); got != "string" { + t.Fatalf("cleanedContent type = %q, want string. Output: %s", got, output) + } + if additionalProperties := schema.Get("additionalProperties"); !additionalProperties.Exists() || additionalProperties.Bool() { + t.Fatalf("additionalProperties = %s, want false. Output: %s", additionalProperties.Raw, output) + } + if got := genConfig.Get("temperature").Float(); got != 0.2 { + t.Fatalf("temperature = %v, want 0.2. Output: %s", got, output) + } +} + +func TestConvertOpenAIResponsesRequestToGemini_TextFormatJSONObject(t *testing.T) { + inputJSON := `{ + "model": "gemini-flash-lite", + "input": "Return a JSON object.", + "text": { + "format": { + "type": "json_object" + } + } + }` + + output := ConvertOpenAIResponsesRequestToGemini("gemini-3.1-flash-lite", []byte(inputJSON), false) + result := gjson.ParseBytes(output) + genConfig := result.Get("generationConfig") + + if got := genConfig.Get("responseMimeType").String(); got != "application/json" { + t.Fatalf("responseMimeType = %q, want application/json. Output: %s", got, output) + } + if genConfig.Get("responseJsonSchema").Exists() { + t.Fatalf("responseJsonSchema should not be set for json_object. Output: %s", output) + } +} + func TestConvertOpenAIResponsesRequestToGemini_ReasoningSignatureCompatibility(t *testing.T) { tests := []struct { name string -- 2.51.2