diff --git a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go index 9bc05899..3dbfb182 100644 --- a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go +++ b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go @@ -49,6 +49,33 @@ func ConvertGeminiRequestToGeminiCLI(_ string, rawJSON []byte, _ bool) []byte { } rawJSON = []byte(template) + // Normalize roles in request.contents: default to valid values if missing/invalid + contents := gjson.GetBytes(rawJSON, "request.contents") + if contents.Exists() { + prevRole := "" + idx := 0 + contents.ForEach(func(_ gjson.Result, value gjson.Result) bool { + role := value.Get("role").String() + valid := role == "user" || role == "model" + if role == "" || !valid { + var newRole string + if prevRole == "" { + newRole = "user" + } else if prevRole == "user" { + newRole = "model" + } else { + newRole = "user" + } + path := fmt.Sprintf("request.contents.%d.role", idx) + rawJSON, _ = sjson.SetBytes(rawJSON, path, newRole) + role = newRole + } + prevRole = role + idx++ + return true + }) + } + return rawJSON } diff --git a/internal/translator/gemini/gemini/gemini_gemini_request.go b/internal/translator/gemini/gemini/gemini_gemini_request.go new file mode 100644 index 00000000..bb49a4ce --- /dev/null +++ b/internal/translator/gemini/gemini/gemini_gemini_request.go @@ -0,0 +1,54 @@ +// Package gemini provides in-provider request normalization for Gemini API. +// It ensures incoming v1beta requests meet minimal schema requirements +// expected by Google's Generative Language API. +package gemini + +import ( + "fmt" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// ConvertGeminiRequestToGemini normalizes Gemini v1beta requests. +// - Adds a default role for each content if missing or invalid. +// The first message defaults to "user", then alternates user/model when needed. +// +// It keeps the payload otherwise unchanged. +func ConvertGeminiRequestToGemini(_ string, rawJSON []byte, _ bool) []byte { + // Fast path: if no contents field, return as-is + contents := gjson.GetBytes(rawJSON, "contents") + if !contents.Exists() { + return rawJSON + } + + // Walk contents and fix roles + out := rawJSON + prevRole := "" + idx := 0 + contents.ForEach(func(_ gjson.Result, value gjson.Result) bool { + role := value.Get("role").String() + + // Only user/model are valid for Gemini v1beta requests + valid := role == "user" || role == "model" + if role == "" || !valid { + var newRole string + if prevRole == "" { + newRole = "user" + } else if prevRole == "user" { + newRole = "model" + } else { + newRole = "user" + } + path := fmt.Sprintf("contents.%d.role", idx) + out, _ = sjson.SetBytes(out, path, newRole) + role = newRole + } + + prevRole = role + idx++ + return true + }) + + return out +} diff --git a/internal/translator/gemini/gemini/gemini_gemini_response.go b/internal/translator/gemini/gemini/gemini_gemini_response.go new file mode 100644 index 00000000..5a9906d1 --- /dev/null +++ b/internal/translator/gemini/gemini/gemini_gemini_response.go @@ -0,0 +1,15 @@ +package gemini + +import ( + "context" +) + +// PassthroughGeminiResponseStream forwards Gemini responses unchanged. +func PassthroughGeminiResponseStream(_ context.Context, _ string, rawJSON []byte, _ *any) []string { + return []string{string(rawJSON)} +} + +// PassthroughGeminiResponseNonStream forwards Gemini responses unchanged. +func PassthroughGeminiResponseNonStream(_ context.Context, _ string, rawJSON []byte, _ *any) string { + return string(rawJSON) +} diff --git a/internal/translator/gemini/gemini/init.go b/internal/translator/gemini/gemini/init.go new file mode 100644 index 00000000..5fbef6e1 --- /dev/null +++ b/internal/translator/gemini/gemini/init.go @@ -0,0 +1,21 @@ +package gemini + +import ( + . "github.com/luispater/CLIProxyAPI/internal/constant" + "github.com/luispater/CLIProxyAPI/internal/interfaces" + "github.com/luispater/CLIProxyAPI/internal/translator/translator" +) + +// Register a no-op response translator and a request normalizer for Gemini→Gemini. +// The request converter ensures missing or invalid roles are normalized to valid values. +func init() { + translator.Register( + GEMINI, + GEMINI, + ConvertGeminiRequestToGemini, + interfaces.TranslateResponse{ + Stream: PassthroughGeminiResponseStream, + NonStream: PassthroughGeminiResponseNonStream, + }, + ) +} diff --git a/internal/translator/init.go b/internal/translator/init.go index e7b4fa0c..7e3f2b60 100644 --- a/internal/translator/init.go +++ b/internal/translator/init.go @@ -12,6 +12,7 @@ import ( _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/gemini" _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini-cli/openai" _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/claude" + _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/gemini" _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/gemini-cli" _ "github.com/luispater/CLIProxyAPI/internal/translator/gemini/openai" _ "github.com/luispater/CLIProxyAPI/internal/translator/openai/claude"