Merge pull request #52 from router-for-me/gemini-web
Add support for image generation with Gemini models through the OpenAI chat completions translator.
This commit is contained in:
@@ -207,7 +207,7 @@ func (c *GeminiWebClient) registerModelsOnce() {
|
|||||||
if c.modelsRegistered {
|
if c.modelsRegistered {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.RegisterModels(GEMINI, geminiWeb.GetGeminiWebAliasedModels())
|
c.RegisterModels(GEMINIWEB, geminiWeb.GetGeminiWebAliasedModels())
|
||||||
c.modelsRegistered = true
|
c.modelsRegistered = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,8 +219,8 @@ func (c *GeminiWebClient) EnsureRegistered() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GeminiWebClient) Type() string { return GEMINI }
|
func (c *GeminiWebClient) Type() string { return GEMINIWEB }
|
||||||
func (c *GeminiWebClient) Provider() string { return GEMINI }
|
func (c *GeminiWebClient) Provider() string { return GEMINIWEB }
|
||||||
func (c *GeminiWebClient) CanProvideModel(modelName string) bool {
|
func (c *GeminiWebClient) CanProvideModel(modelName string) bool {
|
||||||
geminiWeb.EnsureGeminiWebAliasMap()
|
geminiWeb.EnsureGeminiWebAliasMap()
|
||||||
_, ok := geminiWeb.GeminiWebAliasMap[strings.ToLower(modelName)]
|
_, ok := geminiWeb.GeminiWebAliasMap[strings.ToLower(modelName)]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package constant
|
|||||||
const (
|
const (
|
||||||
GEMINI = "gemini"
|
GEMINI = "gemini"
|
||||||
GEMINICLI = "gemini-cli"
|
GEMINICLI = "gemini-cli"
|
||||||
|
GEMINIWEB = "gemini-web"
|
||||||
CODEX = "codex"
|
CODEX = "codex"
|
||||||
CLAUDE = "claude"
|
CLAUDE = "claude"
|
||||||
OPENAI = "openai"
|
OPENAI = "openai"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package chat_completions
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
|
||||||
|
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
|
||||||
|
geminiChat "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/chat-completions"
|
||||||
|
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
translator.Register(
|
||||||
|
OPENAI,
|
||||||
|
GEMINIWEB,
|
||||||
|
geminiChat.ConvertOpenAIRequestToGemini,
|
||||||
|
interfaces.TranslateResponse{
|
||||||
|
Stream: geminiChat.ConvertGeminiResponseToOpenAI,
|
||||||
|
NonStream: geminiChat.ConvertGeminiResponseToOpenAINonStream,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package responses
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/luispater/CLIProxyAPI/v5/internal/constant"
|
||||||
|
"github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
|
||||||
|
geminiResponses "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/responses"
|
||||||
|
"github.com/luispater/CLIProxyAPI/v5/internal/translator/translator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
translator.Register(
|
||||||
|
OPENAI_RESPONSE,
|
||||||
|
GEMINIWEB,
|
||||||
|
geminiResponses.ConvertOpenAIResponsesRequestToGemini,
|
||||||
|
interfaces.TranslateResponse{
|
||||||
|
Stream: geminiResponses.ConvertGeminiResponseToOpenAIResponses,
|
||||||
|
NonStream: geminiResponses.ConvertGeminiResponseToOpenAIResponsesNonStream,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -170,6 +170,31 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
node := []byte(`{"role":"model","parts":[{"text":""}]}`)
|
node := []byte(`{"role":"model","parts":[{"text":""}]}`)
|
||||||
node, _ = sjson.SetBytes(node, "parts.0.text", content.String())
|
node, _ = sjson.SetBytes(node, "parts.0.text", content.String())
|
||||||
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
||||||
|
} else if content.IsArray() {
|
||||||
|
// Assistant multimodal content (e.g. text + image) -> single model content with parts
|
||||||
|
node := []byte(`{"role":"model","parts":[]}`)
|
||||||
|
p := 0
|
||||||
|
for _, item := range content.Array() {
|
||||||
|
switch item.Get("type").String() {
|
||||||
|
case "text":
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", item.Get("text").String())
|
||||||
|
p++
|
||||||
|
case "image_url":
|
||||||
|
// If the assistant returned an inline data URL, preserve it for history fidelity.
|
||||||
|
imageURL := item.Get("image_url.url").String()
|
||||||
|
if len(imageURL) > 5 { // expect data:...
|
||||||
|
pieces := strings.SplitN(imageURL[5:], ";", 2)
|
||||||
|
if len(pieces) == 2 && len(pieces[1]) > 7 {
|
||||||
|
mime := pieces[0]
|
||||||
|
data := pieces[1][7:]
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.mime_type", mime)
|
||||||
|
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.data", data)
|
||||||
|
p++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, _ = sjson.SetRawBytes(out, "contents.-1", node)
|
||||||
} else if !content.Exists() || content.Type == gjson.Null {
|
} else if !content.Exists() || content.Type == gjson.Null {
|
||||||
// Tool calls -> single model content with functionCall parts
|
// Tool calls -> single model content with functionCall parts
|
||||||
tcs := m.Get("tool_calls")
|
tcs := m.Get("tool_calls")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package chat_completions
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -99,6 +100,10 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
partResult := partResults[i]
|
partResult := partResults[i]
|
||||||
partTextResult := partResult.Get("text")
|
partTextResult := partResult.Get("text")
|
||||||
functionCallResult := partResult.Get("functionCall")
|
functionCallResult := partResult.Get("functionCall")
|
||||||
|
inlineDataResult := partResult.Get("inlineData")
|
||||||
|
if !inlineDataResult.Exists() {
|
||||||
|
inlineDataResult = partResult.Get("inline_data")
|
||||||
|
}
|
||||||
|
|
||||||
if partTextResult.Exists() {
|
if partTextResult.Exists() {
|
||||||
// Handle text content, distinguishing between regular content and reasoning/thoughts.
|
// Handle text content, distinguishing between regular content and reasoning/thoughts.
|
||||||
@@ -124,6 +129,34 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallTemplate)
|
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallTemplate)
|
||||||
|
} else if inlineDataResult.Exists() {
|
||||||
|
data := inlineDataResult.Get("data").String()
|
||||||
|
if data == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mimeType := inlineDataResult.Get("mimeType").String()
|
||||||
|
if mimeType == "" {
|
||||||
|
mimeType = inlineDataResult.Get("mime_type").String()
|
||||||
|
}
|
||||||
|
if mimeType == "" {
|
||||||
|
mimeType = "image/png"
|
||||||
|
}
|
||||||
|
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||||
|
imagePayload, err := json.Marshal(map[string]any{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": map[string]string{
|
||||||
|
"url": imageURL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
imagesResult := gjson.Get(template, "choices.0.delta.images")
|
||||||
|
if !imagesResult.Exists() || !imagesResult.IsArray() {
|
||||||
|
template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
|
||||||
|
}
|
||||||
|
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||||
|
template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", string(imagePayload))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,6 +226,10 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
partResult := partsResults[i]
|
partResult := partsResults[i]
|
||||||
partTextResult := partResult.Get("text")
|
partTextResult := partResult.Get("text")
|
||||||
functionCallResult := partResult.Get("functionCall")
|
functionCallResult := partResult.Get("functionCall")
|
||||||
|
inlineDataResult := partResult.Get("inlineData")
|
||||||
|
if !inlineDataResult.Exists() {
|
||||||
|
inlineDataResult = partResult.Get("inline_data")
|
||||||
|
}
|
||||||
|
|
||||||
if partTextResult.Exists() {
|
if partTextResult.Exists() {
|
||||||
// Append text content, distinguishing between regular content and reasoning.
|
// Append text content, distinguishing between regular content and reasoning.
|
||||||
@@ -217,9 +254,34 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls.-1", functionCallItemTemplate)
|
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls.-1", functionCallItemTemplate)
|
||||||
} else {
|
} else if inlineDataResult.Exists() {
|
||||||
// If no usable content is found, return an empty string.
|
data := inlineDataResult.Get("data").String()
|
||||||
return ""
|
if data == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mimeType := inlineDataResult.Get("mimeType").String()
|
||||||
|
if mimeType == "" {
|
||||||
|
mimeType = inlineDataResult.Get("mime_type").String()
|
||||||
|
}
|
||||||
|
if mimeType == "" {
|
||||||
|
mimeType = "image/png"
|
||||||
|
}
|
||||||
|
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||||
|
imagePayload, err := json.Marshal(map[string]any{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": map[string]string{
|
||||||
|
"url": imageURL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
imagesResult := gjson.Get(template, "choices.0.message.images")
|
||||||
|
if !imagesResult.Exists() || !imagesResult.IsArray() {
|
||||||
|
template, _ = sjson.SetRaw(template, "choices.0.message.images", `[]`)
|
||||||
|
}
|
||||||
|
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||||
|
template, _ = sjson.SetRaw(template, "choices.0.message.images.-1", string(imagePayload))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import (
|
|||||||
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/chat-completions"
|
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/chat-completions"
|
||||||
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/responses"
|
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini/openai/responses"
|
||||||
|
|
||||||
|
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini-web/openai/chat-completions"
|
||||||
|
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/gemini-web/openai/responses"
|
||||||
|
|
||||||
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/claude"
|
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/claude"
|
||||||
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/gemini"
|
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/gemini"
|
||||||
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/gemini-cli"
|
_ "github.com/luispater/CLIProxyAPI/v5/internal/translator/openai/gemini-cli"
|
||||||
|
|||||||
Reference in New Issue
Block a user