mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-11 06:37:47 +08:00
fix(translator): ignore null and empty finish reasons in openai to gemini response
- Check that `finish_reason` is a non-empty string before mapping to Gemini `finishReason`. - Prevent chunks or messages with `null` or empty `finish_reason` from emitting unexpected completion statuses. Closes: #5651
This commit is contained in:
@@ -201,7 +201,7 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
||||
}
|
||||
|
||||
// Handle finish reason
|
||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||
if finishReason := choice.Get("finish_reason"); finishReason.Type == gjson.String && finishReason.String() != "" {
|
||||
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
|
||||
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", geminiFinishReason)
|
||||
|
||||
@@ -606,7 +606,7 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
||||
}
|
||||
|
||||
// Handle finish reason
|
||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||
if finishReason := choice.Get("finish_reason"); finishReason.Type == gjson.String && finishReason.String() != "" {
|
||||
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
|
||||
out, _ = sjson.SetBytes(out, "candidates.0.finishReason", geminiFinishReason)
|
||||
}
|
||||
|
||||
@@ -85,3 +85,93 @@ func TestConvertOpenAIResponseToGeminiNonStream_MultiChoicePartsOverlay(t *testi
|
||||
t.Fatalf("expected functionCall.id to be 'call_2', got %q", parts3[0].Get("functionCall.id").String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponseToGeminiStream_NullFinishReasonIgnored(t *testing.T) {
|
||||
var param any
|
||||
|
||||
// Chunk 1: Contentless delta with explicit finish_reason: null (e.g. role declaration or pre-reasoning delta)
|
||||
chunk1 := []byte(`{"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}`)
|
||||
out1 := ConvertOpenAIResponseToGemini(context.Background(), "gpt-test", nil, nil, chunk1, ¶m)
|
||||
for i, chunk := range out1 {
|
||||
if fr := gjson.GetBytes(chunk, "candidates.0.finishReason"); fr.Exists() && fr.String() != "" {
|
||||
t.Fatalf("chunk1[%d] unexpectedly set finishReason = %q on non-final chunk; payload=%s", i, fr.String(), chunk)
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk 2: Reasoning delta with finish_reason: null
|
||||
chunk2 := []byte(`{"choices":[{"index":0,"delta":{"reasoning_content":"thinking..."},"finish_reason":null}]}`)
|
||||
out2 := ConvertOpenAIResponseToGemini(context.Background(), "gpt-test", nil, nil, chunk2, ¶m)
|
||||
if len(out2) == 0 {
|
||||
t.Fatalf("expected output for reasoning chunk, got 0 chunks")
|
||||
}
|
||||
if gotThought := gjson.GetBytes(out2[0], "candidates.0.content.parts.0.thought").Bool(); !gotThought {
|
||||
t.Fatalf("expected thought: true on reasoning chunk, got false")
|
||||
}
|
||||
if gotText := gjson.GetBytes(out2[0], "candidates.0.content.parts.0.text").String(); gotText != "thinking..." {
|
||||
t.Fatalf("expected reasoning text %q, got %q", "thinking...", gotText)
|
||||
}
|
||||
for i, chunk := range out2 {
|
||||
if fr := gjson.GetBytes(chunk, "candidates.0.finishReason"); fr.Exists() && fr.String() != "" {
|
||||
t.Fatalf("chunk2[%d] unexpectedly set finishReason = %q on reasoning chunk; payload=%s", i, fr.String(), chunk)
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk 3: Contentless delta with finish_reason: "" (empty string should not be treated as stop)
|
||||
chunk3 := []byte(`{"choices":[{"index":0,"delta":{},"finish_reason":""}]}`)
|
||||
out3 := ConvertOpenAIResponseToGemini(context.Background(), "gpt-test", nil, nil, chunk3, ¶m)
|
||||
for i, chunk := range out3 {
|
||||
if fr := gjson.GetBytes(chunk, "candidates.0.finishReason"); fr.Exists() && fr.String() != "" {
|
||||
t.Fatalf("chunk3[%d] unexpectedly set finishReason = %q on contentless chunk with empty finish_reason; payload=%s", i, fr.String(), chunk)
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk 4: Content delta with finish_reason: null
|
||||
chunk4 := []byte(`{"choices":[{"index":0,"delta":{"content":"hello world"},"finish_reason":null}]}`)
|
||||
out4 := ConvertOpenAIResponseToGemini(context.Background(), "gpt-test", nil, nil, chunk4, ¶m)
|
||||
if len(out4) == 0 {
|
||||
t.Fatalf("expected output for content chunk, got 0 chunks")
|
||||
}
|
||||
if gotText := gjson.GetBytes(out4[0], "candidates.0.content.parts.0.text").String(); gotText != "hello world" {
|
||||
t.Fatalf("expected content text %q, got %q", "hello world", gotText)
|
||||
}
|
||||
for i, chunk := range out4 {
|
||||
if fr := gjson.GetBytes(chunk, "candidates.0.finishReason"); fr.Exists() && fr.String() != "" {
|
||||
t.Fatalf("chunk4[%d] unexpectedly set finishReason = %q on non-final content chunk; payload=%s", i, fr.String(), chunk)
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk 5: Final chunk with finish_reason: "stop"
|
||||
chunk5 := []byte(`{"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}`)
|
||||
out5 := ConvertOpenAIResponseToGemini(context.Background(), "gpt-test", nil, nil, chunk5, ¶m)
|
||||
if len(out5) == 0 {
|
||||
t.Fatalf("expected output for final chunk, got 0 chunks")
|
||||
}
|
||||
if got := gjson.GetBytes(out5[len(out5)-1], "candidates.0.finishReason").String(); got != "STOP" {
|
||||
t.Fatalf("expected finishReason STOP on final chunk, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponseToGeminiNonStream_NullFinishReasonIgnored(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
}{
|
||||
{
|
||||
name: "null finish_reason",
|
||||
payload: []byte(`{"choices":[{"index":0,"message":{"role":"assistant","content":"hello"},"finish_reason":null}]}`),
|
||||
},
|
||||
{
|
||||
name: "empty finish_reason",
|
||||
payload: []byte(`{"choices":[{"index":0,"message":{"role":"assistant","content":"hello"},"finish_reason":""}]}`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out := ConvertOpenAIResponseToGeminiNonStream(context.Background(), "gpt-test", nil, nil, tc.payload, nil)
|
||||
if fr := gjson.GetBytes(out, "candidates.0.finishReason"); fr.Exists() && fr.String() != "" {
|
||||
t.Fatalf("unexpected finishReason = %q on non-stream response; output=%s", fr.String(), out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user