mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-10 02:16:49 +08:00
fix(translator): align reasoning merge with Responses visible text rules
Recognize model-visible output_text even when message.role is user, accept assistant string content, and preserve raw merged text without trimming. Always merge the adjacent visible item into native Gemini reasoning turns so trailing output is not dropped by prefill stripping.
This commit is contained in:
@@ -359,7 +359,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
||||
signature := openAIResponsesGeminiThoughtSignature(item.Get("encrypted_content").String())
|
||||
|
||||
visibleText := ""
|
||||
if useGeminiNativeReasoningLayout && i+1 < len(normalized) && !isTrailingOpenAIResponsesAssistantPrefill(normalized, i+1) {
|
||||
if useGeminiNativeReasoningLayout && i+1 < len(normalized) {
|
||||
next := normalized[i+1]
|
||||
if visible, ok := openAIResponsesAssistantVisibleText(next); ok {
|
||||
visibleText = visible
|
||||
@@ -528,19 +528,25 @@ func openAIResponsesAssistantVisibleText(item gjson.Result) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(itemRole)) {
|
||||
case "assistant", "model":
|
||||
default:
|
||||
content := item.Get("content")
|
||||
if !content.Exists() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
contentArray := item.Get("content")
|
||||
if !contentArray.Exists() || !contentArray.IsArray() {
|
||||
if content.Type == gjson.String {
|
||||
switch strings.ToLower(strings.TrimSpace(itemRole)) {
|
||||
case "assistant", "model":
|
||||
return content.String(), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
if !content.IsArray() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
var textParts []string
|
||||
contentArray.ForEach(func(_, contentItem gjson.Result) bool {
|
||||
hasOutputText := false
|
||||
content.ForEach(func(_, contentItem gjson.Result) bool {
|
||||
contentType := contentItem.Get("type").String()
|
||||
if contentType == "" {
|
||||
contentType = "input_text"
|
||||
@@ -548,14 +554,14 @@ func openAIResponsesAssistantVisibleText(item gjson.Result) (string, bool) {
|
||||
if contentType != "output_text" {
|
||||
return true
|
||||
}
|
||||
if text := strings.TrimSpace(contentItem.Get("text").String()); text != "" {
|
||||
textParts = append(textParts, text)
|
||||
}
|
||||
hasOutputText = true
|
||||
textParts = append(textParts, contentItem.Get("text").String())
|
||||
return true
|
||||
})
|
||||
if len(textParts) == 0 {
|
||||
if !hasOutputText {
|
||||
return "", false
|
||||
}
|
||||
// output_text marks model-visible content even when message.role is "user".
|
||||
return strings.Join(textParts, "\n"), true
|
||||
}
|
||||
|
||||
|
||||
@@ -285,6 +285,81 @@ func TestConvertOpenAIResponsesRequestToGemini_MergesReasoningWithAssistantVisib
|
||||
t.Fatalf("visible thoughtSignature = %q, want preserved signature", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToGemini_MergesReasoningWithUserRoleOutputText(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "gemini-3.5-flash",
|
||||
"input": [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"encrypted_content": "gemini#` + testResponsesGeminiThoughtSignature + `",
|
||||
"summary": [{"type": "summary_text", "text": "reasoning summary"}]
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "output_text", "text": "visible from user role"}]
|
||||
}
|
||||
]
|
||||
}`
|
||||
output := ConvertOpenAIResponsesRequestToGemini("gemini-3.5-flash", []byte(inputJSON), false)
|
||||
contents := gjson.GetBytes(output, "contents").Array()
|
||||
if len(contents) != 1 {
|
||||
t.Fatalf("contents length = %d, want 1. Output: %s", len(contents), output)
|
||||
}
|
||||
if got := contents[0].Get("parts.1.text").String(); got != "visible from user role" {
|
||||
t.Fatalf("visible text = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToGemini_MergesReasoningWithAssistantStringContent(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "gemini-3.5-flash",
|
||||
"input": [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"encrypted_content": "gemini#` + testResponsesGeminiThoughtSignature + `",
|
||||
"summary": [{"type": "summary_text", "text": "reasoning summary"}]
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": "string visible answer"
|
||||
}
|
||||
]
|
||||
}`
|
||||
output := ConvertOpenAIResponsesRequestToGemini("gemini-3.5-flash", []byte(inputJSON), false)
|
||||
if got := gjson.GetBytes(output, "contents.0.parts.1.text").String(); got != "string visible answer" {
|
||||
t.Fatalf("visible text = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToGemini_PreservesWhitespaceWhenMergingReasoning(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "gemini-3.5-flash",
|
||||
"input": [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"encrypted_content": "gemini#` + testResponsesGeminiThoughtSignature + `",
|
||||
"summary": [{"type": "summary_text", "text": "reasoning summary"}]
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [{"type": "output_text", "text": " lead trail "}]
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "next"}]
|
||||
}
|
||||
]
|
||||
}`
|
||||
output := ConvertOpenAIResponsesRequestToGemini("gemini-3.5-flash", []byte(inputJSON), false)
|
||||
if got := gjson.GetBytes(output, "contents.0.parts.1.text").String(); got != " lead trail " {
|
||||
t.Fatalf("visible text = %q, want preserved whitespace", got)
|
||||
}
|
||||
}
|
||||
func TestConvertOpenAIResponsesRequestToGemini_SystemAndDeveloperRoles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user