Add response_format compatibility mapping

This commit is contained in:
CHOS1N
2026-07-05 13:58:08 +08:00
parent b650bdd68d
commit 5fcbe7e307
6 changed files with 214 additions and 0 deletions
@@ -38,6 +38,9 @@ func ResponsesToChatCompletionsRequest(req *ResponsesRequest) (*ChatCompletionsR
if len(req.ToolChoice) > 0 {
out.ToolChoice = responsesToolChoiceToChatToolChoice(req.ToolChoice)
}
if req.Text != nil {
out.ResponseFormat = responsesTextFormatToChatResponseFormat(req.Text.Format)
}
return out, nil
}
@@ -73,6 +73,60 @@ func TestResponsesToChatCompletionsRequest_InstructionsAndInputDeveloperRole(t *
assert.JSONEq(t, `"Hello"`, string(out.Messages[2].Content))
}
func TestResponsesToChatCompletionsRequest_TextFormatJsonObject(t *testing.T) {
req := &ResponsesRequest{
Model: "gpt-4o",
Input: json.RawMessage(`[
{"role":"user","content":"Return JSON"}
]`),
Text: &ResponsesText{
Format: json.RawMessage(`{"type":"json_object"}`),
},
}
out, err := ResponsesToChatCompletionsRequest(req)
require.NoError(t, err)
assert.JSONEq(t, `{"type":"json_object"}`, string(out.ResponseFormat))
}
func TestResponsesToChatCompletionsRequest_TextFormatJsonSchema(t *testing.T) {
req := &ResponsesRequest{
Model: "gpt-4o",
Input: json.RawMessage(`[
{"role":"user","content":"Return structured JSON"}
]`),
Text: &ResponsesText{
Format: json.RawMessage(`{
"type":"json_schema",
"name":"answer",
"schema":{
"type":"object",
"properties":{"ok":{"type":"boolean"}},
"required":["ok"],
"additionalProperties":false
},
"strict":true
}`),
},
}
out, err := ResponsesToChatCompletionsRequest(req)
require.NoError(t, err)
assert.JSONEq(t, `{
"type":"json_schema",
"json_schema":{
"name":"answer",
"schema":{
"type":"object",
"properties":{"ok":{"type":"boolean"}},
"required":["ok"],
"additionalProperties":false
},
"strict":true
}
}`, string(out.ResponseFormat))
}
func chatMessageRoles(messages []ChatMessage) []string {
roles := make([]string, 0, len(messages))
for _, message := range messages {
@@ -242,6 +242,62 @@ func TestChatCompletionsToResponses_ReasoningEffort(t *testing.T) {
assert.Equal(t, "auto", resp.Reasoning.Summary)
}
func TestChatCompletionsToResponses_ResponseFormatJsonObject(t *testing.T) {
req := &ChatCompletionsRequest{
Model: "gpt-4o",
Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Return JSON"`)}},
ResponseFormat: json.RawMessage(`{"type":"json_object"}`),
}
resp, err := ChatCompletionsToResponses(req)
require.NoError(t, err)
require.NotNil(t, resp.Text)
assert.JSONEq(t, `{"type":"json_object"}`, string(resp.Text.Format))
payload, err := json.Marshal(resp)
require.NoError(t, err)
var serialized struct {
Text ResponsesText `json:"text"`
}
require.NoError(t, json.Unmarshal(payload, &serialized))
assert.JSONEq(t, `{"type":"json_object"}`, string(serialized.Text.Format))
}
func TestChatCompletionsToResponses_ResponseFormatJsonSchema(t *testing.T) {
req := &ChatCompletionsRequest{
Model: "gpt-4o",
Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`"Return structured JSON"`)}},
ResponseFormat: json.RawMessage(`{
"type":"json_schema",
"json_schema":{
"name":"answer",
"schema":{
"type":"object",
"properties":{"ok":{"type":"boolean"}},
"required":["ok"],
"additionalProperties":false
},
"strict":true
}
}`),
}
resp, err := ChatCompletionsToResponses(req)
require.NoError(t, err)
require.NotNil(t, resp.Text)
assert.JSONEq(t, `{
"type":"json_schema",
"name":"answer",
"schema":{
"type":"object",
"properties":{"ok":{"type":"boolean"}},
"required":["ok"],
"additionalProperties":false
},
"strict":true
}`, string(resp.Text.Format))
}
func TestChatCompletionsToResponses_ImageURL(t *testing.T) {
content := `[{"type":"text","text":"Describe this"},{"type":"image_url","image_url":{"url":"data:image/png;base64,abc123"}}]`
req := &ChatCompletionsRequest{
@@ -69,6 +69,13 @@ func ChatCompletionsToResponses(req *ChatCompletionsRequest) (*ResponsesRequest,
}
}
if format := chatResponseFormatToResponsesTextFormat(req.ResponseFormat); len(format) > 0 {
if out.Text == nil {
out.Text = &ResponsesText{}
}
out.Text.Format = format
}
// tools[] and legacy functions[] → ResponsesTool[]
if len(req.Tools) > 0 || len(req.Functions) > 0 {
out.Tools = convertChatToolsToResponses(req.Tools, req.Functions)
@@ -0,0 +1,92 @@
package apicompat
import "encoding/json"
func chatResponseFormatToResponsesTextFormat(raw json.RawMessage) json.RawMessage {
raw = normalizedRawJSON(raw)
if len(raw) == 0 {
return nil
}
obj, ok := rawJSONObject(raw)
if !ok || rawString(obj["type"]) != "json_schema" {
return raw
}
schemaRaw := normalizedRawJSON(obj["json_schema"])
if len(schemaRaw) == 0 {
return raw
}
var schema map[string]json.RawMessage
if err := json.Unmarshal(schemaRaw, &schema); err != nil {
return raw
}
schema["type"] = rawJSONString("json_schema")
out, err := json.Marshal(schema)
if err != nil {
return raw
}
return out
}
func responsesTextFormatToChatResponseFormat(raw json.RawMessage) json.RawMessage {
raw = normalizedRawJSON(raw)
if len(raw) == 0 {
return nil
}
obj, ok := rawJSONObject(raw)
if !ok || rawString(obj["type"]) != "json_schema" {
return raw
}
if _, alreadyChatShape := obj["json_schema"]; alreadyChatShape {
return raw
}
schema := make(map[string]json.RawMessage, len(obj))
for key, value := range obj {
if key == "type" {
continue
}
schema[key] = value
}
if len(schema) == 0 {
return raw
}
schemaRaw, err := json.Marshal(schema)
if err != nil {
return raw
}
out, err := json.Marshal(map[string]json.RawMessage{
"type": rawJSONString("json_schema"),
"json_schema": schemaRaw,
})
if err != nil {
return raw
}
return out
}
func normalizedRawJSON(raw json.RawMessage) json.RawMessage {
raw = bytesTrimSpace(raw)
if len(raw) == 0 || string(raw) == "null" {
return nil
}
return append(json.RawMessage(nil), raw...)
}
func rawJSONObject(raw json.RawMessage) (map[string]json.RawMessage, bool) {
var obj map[string]json.RawMessage
if err := json.Unmarshal(raw, &obj); err != nil {
return nil, false
}
return obj, true
}
func rawJSONString(value string) json.RawMessage {
data, _ := json.Marshal(value)
return data
}
+2
View File
@@ -216,6 +216,7 @@ type ResponsesReasoning struct {
// ResponsesText configures text output options in the Responses API.
type ResponsesText struct {
Format json.RawMessage `json:"format,omitempty"`
Verbosity string `json:"verbosity,omitempty"` // "low" | "medium" | "high"
}
@@ -438,6 +439,7 @@ type ChatCompletionsRequest struct {
ReasoningEffort string `json:"reasoning_effort,omitempty"` // "low" | "medium" | "high" | "xhigh"
ServiceTier string `json:"service_tier,omitempty"`
Stop json.RawMessage `json:"stop,omitempty"` // string or []string
ResponseFormat json.RawMessage `json:"response_format,omitempty"`
// Legacy function calling (deprecated but still supported)
Functions []ChatFunction `json:"functions,omitempty"`