feat(translator): map OpenAI max_tokens to Gemini's maxOutputTokens

- Added logic to convert OpenAI `max_tokens` and `max_completion_tokens` to `generationConfig.maxOutputTokens`.
- Implemented unit tests to validate proper mapping behavior, including precedence of `max_tokens` over `max_completion_tokens`.

Closes: #4108
This commit is contained in:
Luis Pater
2026-07-09 04:17:28 +08:00
parent bea9567051
commit d899c96239
2 changed files with 40 additions and 0 deletions
@@ -68,6 +68,13 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
out, _ = sjson.SetBytes(out, "generationConfig.topK", tkr.Num)
}
// OpenAI max_tokens / max_completion_tokens -> Gemini generationConfig.maxOutputTokens
if mt := gjson.GetBytes(rawJSON, "max_tokens"); mt.Exists() && mt.Type == gjson.Number {
out, _ = sjson.SetBytes(out, "generationConfig.maxOutputTokens", mt.Num)
} else if mct := gjson.GetBytes(rawJSON, "max_completion_tokens"); mct.Exists() && mct.Type == gjson.Number {
out, _ = sjson.SetBytes(out, "generationConfig.maxOutputTokens", mct.Num)
}
// Candidate count (OpenAI 'n' parameter)
if n := gjson.GetBytes(rawJSON, "n"); n.Exists() && n.Type == gjson.Number {
if val := n.Int(); val > 1 {
@@ -140,6 +140,39 @@ func TestConvertOpenAIRequestToGeminiSkipsEmptyTextPartsWithoutNulls(t *testing.
}
}
func TestConvertOpenAIRequestToGeminiMapsMaxTokens(t *testing.T) {
tests := []struct {
name string
body string
want int64
}{
{
name: "max_tokens",
body: `{"model":"gemini-2.0-flash","messages":[{"role":"user","content":"hi"}],"max_tokens":30}`,
want: 30,
},
{
name: "max_completion_tokens",
body: `{"model":"gemini-2.0-flash","messages":[{"role":"user","content":"hi"}],"max_completion_tokens":40}`,
want: 40,
},
{
name: "max_tokens preferred over max_completion_tokens",
body: `{"model":"gemini-2.0-flash","messages":[{"role":"user","content":"hi"}],"max_tokens":30,"max_completion_tokens":40}`,
want: 30,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out := ConvertOpenAIRequestToGemini("gemini-2.0-flash", []byte(tt.body), false)
if got := gjson.GetBytes(out, "generationConfig.maxOutputTokens").Int(); got != tt.want {
t.Fatalf("generationConfig.maxOutputTokens = %d, want %d. Output: %s", got, tt.want, out)
}
})
}
}
func TestConvertOpenAIRequestToGeminiCleansToolSchemaRequiredFields(t *testing.T) {
inputJSON := `{
"model": "gemini-2.0-flash",