From 2e91e99e0339f1f2592cb4fa36b1f71ca89bf8dd Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Thu, 6 Aug 2026 05:57:09 +0800 Subject: [PATCH] fix(responses): translate `text.format` into `response_format` in request conversion Closes: #4802 --- .../openai_openai-responses_request.go | 30 ++++++++ .../openai_openai-responses_request_test.go | 68 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_request.go b/internal/translator/openai/openai/responses/openai_openai-responses_request.go index 4522b5ced..a329d4e3c 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_request.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_request.go @@ -45,6 +45,13 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu // Set stream configuration out, _ = sjson.SetBytes(out, "stream", stream) + // Map Responses text format to Chat Completions response format. + if textFormat := root.Get("text.format"); textFormat.Exists() { + if responseFormat := convertResponsesTextFormatToChatResponseFormat(textFormat); len(responseFormat) > 0 { + out, _ = sjson.SetRawBytes(out, "response_format", responseFormat) + } + } + // Map generation parameters from responses format to chat completions format if maxTokens := root.Get("max_output_tokens"); maxTokens.Exists() { out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) @@ -344,6 +351,29 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu return out } +func convertResponsesTextFormatToChatResponseFormat(textFormat gjson.Result) []byte { + formatType := textFormat.Get("type").String() + switch formatType { + case "text", "json_object": + responseFormat := []byte(`{"type":""}`) + responseFormat, _ = sjson.SetBytes(responseFormat, "type", formatType) + return responseFormat + case "json_schema": + responseFormat := []byte(`{"type":"json_schema","json_schema":{}}`) + for _, field := range []string{"name", "description", "strict"} { + if value := textFormat.Get(field); value.Exists() { + responseFormat, _ = sjson.SetBytes(responseFormat, "json_schema."+field, value.Value()) + } + } + if schema := textFormat.Get("schema"); schema.Exists() { + responseFormat, _ = sjson.SetRawBytes(responseFormat, "json_schema.schema", []byte(schema.Raw)) + } + return responseFormat + default: + return nil + } +} + func setFunctionCallOutputContent(toolMessage []byte, output gjson.Result) []byte { structuredContent := output if output.Type == gjson.String { diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_request_test.go b/internal/translator/openai/openai/responses/openai_openai-responses_request_test.go index c94ced4f3..749185a07 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_request_test.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_request_test.go @@ -581,6 +581,74 @@ func TestConvertOpenAIResponsesRequestToOpenAIChatCompletions_PreservesParallelT } } +func TestConvertOpenAIResponsesRequestToOpenAIChatCompletions_PreservesJSONSchemaTextFormat(t *testing.T) { + raw := []byte(`{ + "text": { + "format": { + "type": "json_schema", + "name": "answer", + "description": "Structured answer", + "strict": true, + "schema": { + "type": "object", + "properties": { + "ok": {"type": "boolean"} + }, + "required": ["ok"], + "additionalProperties": false + } + } + } + }`) + + out := ConvertOpenAIResponsesRequestToOpenAIChatCompletions("deepseek-v4-flash", raw, false) + + if got := gjson.GetBytes(out, "response_format.type").String(); got != "json_schema" { + t.Fatalf("response_format.type = %q, want json_schema; output=%s", got, out) + } + if got := gjson.GetBytes(out, "response_format.json_schema.name").String(); got != "answer" { + t.Fatalf("response_format.json_schema.name = %q, want answer; output=%s", got, out) + } + if got := gjson.GetBytes(out, "response_format.json_schema.description").String(); got != "Structured answer" { + t.Fatalf("response_format.json_schema.description = %q, want Structured answer; output=%s", got, out) + } + if got := gjson.GetBytes(out, "response_format.json_schema.strict"); !got.Exists() || !got.Bool() { + t.Fatalf("response_format.json_schema.strict = %v, want true; output=%s", got.Value(), out) + } + if got := gjson.GetBytes(out, "response_format.json_schema.schema.properties.ok.type").String(); got != "boolean" { + t.Fatalf("response_format.json_schema.schema.properties.ok.type = %q, want boolean; output=%s", got, out) + } + if got := gjson.GetBytes(out, "response_format.json_schema.schema.required.0").String(); got != "ok" { + t.Fatalf("response_format.json_schema.schema.required.0 = %q, want ok; output=%s", got, out) + } + if got := gjson.GetBytes(out, "response_format.json_schema.schema.additionalProperties"); !got.Exists() || got.Bool() { + t.Fatalf("response_format.json_schema.schema.additionalProperties = %v, want false; output=%s", got.Value(), out) + } +} + +func TestConvertOpenAIResponsesRequestToOpenAIChatCompletions_PreservesJSONObjectTextFormat(t *testing.T) { + raw := []byte(`{"text":{"format":{"type":"json_object"}}}`) + + out := ConvertOpenAIResponsesRequestToOpenAIChatCompletions("deepseek-v4-flash", raw, false) + + if got := gjson.GetBytes(out, "response_format.type").String(); got != "json_object" { + t.Fatalf("response_format.type = %q, want json_object; output=%s", got, out) + } + if got := gjson.GetBytes(out, "response_format.json_schema"); got.Exists() { + t.Fatalf("response_format.json_schema should be omitted; output=%s", out) + } +} + +func TestConvertOpenAIResponsesRequestToOpenAIChatCompletions_OmitsResponseFormatWithoutTextFormat(t *testing.T) { + raw := []byte(`{"input":"Return plain text."}`) + + out := ConvertOpenAIResponsesRequestToOpenAIChatCompletions("deepseek-v4-flash", raw, false) + + if got := gjson.GetBytes(out, "response_format"); got.Exists() { + t.Fatalf("response_format should be omitted, got %s; output=%s", got.Raw, out) + } +} + func TestConvertOpenAIResponsesRequestToOpenAIChatCompletions_NormalizesInputImageDetail(t *testing.T) { tests := []struct { name string