Skip to content

Commit 830fd8e

Browse files
committed
Fix responses-format handling for chat completions
1 parent dbcbe48 commit 830fd8e

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package chat_completions
2+
3+
import (
4+
"os"
5+
"strings"
6+
"testing"
7+
8+
responsesconverter "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/responses"
9+
"github.com/tidwall/gjson"
10+
)
11+
12+
func TestResponsesPayloadToolsArePreserved(t *testing.T) {
13+
data, err := os.ReadFile("../../../../../error1.log")
14+
if err != nil {
15+
t.Fatalf("read log: %v", err)
16+
}
17+
18+
var requestLine string
19+
for _, line := range strings.Split(string(data), "\n") {
20+
trimmed := strings.TrimSpace(line)
21+
if strings.HasPrefix(trimmed, "{\"user\"") {
22+
requestLine = trimmed
23+
break
24+
}
25+
}
26+
if requestLine == "" {
27+
t.Fatalf("failed to extract request body from log")
28+
}
29+
30+
raw := []byte(requestLine)
31+
chatPayload := responsesconverter.ConvertOpenAIResponsesRequestToOpenAIChatCompletions("gpt-5.1-codex-max(xhigh)", raw, true)
32+
codexPayload := ConvertOpenAIRequestToCodex("gpt-5.1-codex-max(xhigh)", chatPayload, true)
33+
34+
tools := gjson.GetBytes(codexPayload, "tools")
35+
if !tools.IsArray() || len(tools.Array()) == 0 {
36+
t.Fatalf("expected tools array, got: %s", tools.Raw)
37+
}
38+
for i, tool := range tools.Array() {
39+
if name := strings.TrimSpace(tool.Get("name").String()); name == "" {
40+
t.Fatalf("tool %d missing name after conversion: %s", i, tool.Raw)
41+
}
42+
}
43+
}

sdk/api/handlers/openai/openai_handlers.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
. "github.com/router-for-me/CLIProxyAPI/v6/internal/constant"
1818
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
1919
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
20+
responsesconverter "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/responses"
2021
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers"
2122
"github.com/tidwall/gjson"
2223
"github.com/tidwall/sjson"
@@ -109,14 +110,39 @@ func (h *OpenAIAPIHandler) ChatCompletions(c *gin.Context) {
109110

110111
// Check if the client requested a streaming response.
111112
streamResult := gjson.GetBytes(rawJSON, "stream")
112-
if streamResult.Type == gjson.True {
113+
stream := streamResult.Type == gjson.True
114+
115+
// Some clients send OpenAI Responses-format payloads to /v1/chat/completions.
116+
// Convert them to Chat Completions so downstream translators preserve tool metadata.
117+
if shouldTreatAsResponsesFormat(rawJSON) {
118+
modelName := gjson.GetBytes(rawJSON, "model").String()
119+
rawJSON = responsesconverter.ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName, rawJSON, stream)
120+
stream = gjson.GetBytes(rawJSON, "stream").Bool()
121+
}
122+
123+
if stream {
113124
h.handleStreamingResponse(c, rawJSON)
114125
} else {
115126
h.handleNonStreamingResponse(c, rawJSON)
116127
}
117128

118129
}
119130

131+
// shouldTreatAsResponsesFormat detects OpenAI Responses-style payloads that are
132+
// accidentally sent to the Chat Completions endpoint.
133+
func shouldTreatAsResponsesFormat(rawJSON []byte) bool {
134+
if gjson.GetBytes(rawJSON, "messages").Exists() {
135+
return false
136+
}
137+
if gjson.GetBytes(rawJSON, "input").Exists() {
138+
return true
139+
}
140+
if gjson.GetBytes(rawJSON, "instructions").Exists() {
141+
return true
142+
}
143+
return false
144+
}
145+
120146
// Completions handles the /v1/completions endpoint.
121147
// It determines whether the request is for a streaming or non-streaming response
122148
// and calls the appropriate handler based on the model provider.

0 commit comments

Comments
 (0)