From d899c9623952299042507cbbfbf6372d0a52d741 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Thu, 9 Jul 2026 04:17:28 +0800 Subject: [PATCH] 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 --- .../chat-completions/gemini_openai_request.go | 7 ++++ .../gemini_openai_request_test.go | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index bfe2ca13..d7b5e178 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -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 { diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request_test.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request_test.go index f29221f2..bbeaae7c 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request_test.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request_test.go @@ -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",