mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Add response_format compatibility mapping
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user