fix(responses): translate text.format into response_format in request conversion

Closes: #4802
This commit is contained in:
Luis Pater
2026-08-06 05:57:09 +08:00
parent 94808f1087
commit 2e91e99e03
2 changed files with 98 additions and 0 deletions

View File

@@ -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 {

View File

@@ -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