refactor(gemini-web): Move provider logic to its own package

The Gemini Web API client logic has been relocated from `internal/client/gemini-web` to a new, more specific `internal/provider/gemini-web` package. This refactoring improves code organization and modularity by better isolating provider-specific implementations.

As a result of this move, the `GeminiWebState` struct and its methods have been exported (capitalized) to make them accessible from the executor. All call sites have been updated to use the new package path and the exported identifiers.
This commit is contained in:
hkfires
2025-09-24 10:24:12 +08:00
parent a2c5fdaf66
commit e9707c2f9e
14 changed files with 117 additions and 84 deletions

View File

@@ -1,4 +1,4 @@
package executor package geminiwebapi
import ( import (
"bytes" "bytes"
@@ -10,8 +10,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
geminiwebapi "github.com/router-for-me/CLIProxyAPI/v6/internal/client/gemini-web"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/constant" "github.com/router-for-me/CLIProxyAPI/v6/internal/constant"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
@@ -25,7 +25,7 @@ const (
geminiWebDefaultTimeoutSec = 300 geminiWebDefaultTimeoutSec = 300
) )
type geminiWebState struct { type GeminiWebState struct {
cfg *config.Config cfg *config.Config
token *gemini.GeminiWebTokenStorage token *gemini.GeminiWebTokenStorage
storagePath string storagePath string
@@ -34,29 +34,29 @@ type geminiWebState struct {
accountID string accountID string
reqMu sync.Mutex reqMu sync.Mutex
client *geminiwebapi.GeminiClient client *GeminiClient
tokenMu sync.Mutex tokenMu sync.Mutex
tokenDirty bool tokenDirty bool
convMu sync.RWMutex convMu sync.RWMutex
convStore map[string][]string convStore map[string][]string
convData map[string]geminiwebapi.ConversationRecord convData map[string]ConversationRecord
convIndex map[string]string convIndex map[string]string
lastRefresh time.Time lastRefresh time.Time
} }
func newGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage, storagePath string) *geminiWebState { func NewGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage, storagePath string) *GeminiWebState {
state := &geminiWebState{ state := &GeminiWebState{
cfg: cfg, cfg: cfg,
token: token, token: token,
storagePath: storagePath, storagePath: storagePath,
convStore: make(map[string][]string), convStore: make(map[string][]string),
convData: make(map[string]geminiwebapi.ConversationRecord), convData: make(map[string]ConversationRecord),
convIndex: make(map[string]string), convIndex: make(map[string]string),
} }
suffix := geminiwebapi.Sha256Hex(token.Secure1PSID) suffix := Sha256Hex(token.Secure1PSID)
if len(suffix) > 16 { if len(suffix) > 16 {
suffix = suffix[:16] suffix = suffix[:16]
} }
@@ -75,39 +75,39 @@ func newGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage,
return state return state
} }
func (s *geminiWebState) loadConversationCaches() { func (s *GeminiWebState) loadConversationCaches() {
if path := s.convStorePath(); path != "" { if path := s.convStorePath(); path != "" {
if store, err := geminiwebapi.LoadConvStore(path); err == nil { if store, err := LoadConvStore(path); err == nil {
s.convStore = store s.convStore = store
} }
} }
if path := s.convDataPath(); path != "" { if path := s.convDataPath(); path != "" {
if items, index, err := geminiwebapi.LoadConvData(path); err == nil { if items, index, err := LoadConvData(path); err == nil {
s.convData = items s.convData = items
s.convIndex = index s.convIndex = index
} }
} }
} }
func (s *geminiWebState) convStorePath() string { func (s *GeminiWebState) convStorePath() string {
base := s.storagePath base := s.storagePath
if base == "" { if base == "" {
base = s.accountID + ".json" base = s.accountID + ".json"
} }
return geminiwebapi.ConvStorePath(base) return ConvStorePath(base)
} }
func (s *geminiWebState) convDataPath() string { func (s *GeminiWebState) convDataPath() string {
base := s.storagePath base := s.storagePath
if base == "" { if base == "" {
base = s.accountID + ".json" base = s.accountID + ".json"
} }
return geminiwebapi.ConvDataPath(base) return ConvDataPath(base)
} }
func (s *geminiWebState) getRequestMutex() *sync.Mutex { return &s.reqMu } func (s *GeminiWebState) GetRequestMutex() *sync.Mutex { return &s.reqMu }
func (s *geminiWebState) ensureClient() error { func (s *GeminiWebState) EnsureClient() error {
if s.client != nil && s.client.Running { if s.client != nil && s.client.Running {
return nil return nil
} }
@@ -115,7 +115,7 @@ func (s *geminiWebState) ensureClient() error {
if s.cfg != nil { if s.cfg != nil {
proxyURL = s.cfg.ProxyURL proxyURL = s.cfg.ProxyURL
} }
s.client = geminiwebapi.NewGeminiClient( s.client = NewGeminiClient(
s.token.Secure1PSID, s.token.Secure1PSID,
s.token.Secure1PSIDTS, s.token.Secure1PSIDTS,
proxyURL, proxyURL,
@@ -129,13 +129,13 @@ func (s *geminiWebState) ensureClient() error {
return nil return nil
} }
func (s *geminiWebState) refresh(ctx context.Context) error { func (s *GeminiWebState) Refresh(ctx context.Context) error {
_ = ctx _ = ctx
proxyURL := "" proxyURL := ""
if s.cfg != nil { if s.cfg != nil {
proxyURL = s.cfg.ProxyURL proxyURL = s.cfg.ProxyURL
} }
s.client = geminiwebapi.NewGeminiClient( s.client = NewGeminiClient(
s.token.Secure1PSID, s.token.Secure1PSID,
s.token.Secure1PSIDTS, s.token.Secure1PSIDTS,
proxyURL, proxyURL,
@@ -158,7 +158,7 @@ func (s *geminiWebState) refresh(ctx context.Context) error {
return nil return nil
} }
func (s *geminiWebState) tokenSnapshot() *gemini.GeminiWebTokenStorage { func (s *GeminiWebState) TokenSnapshot() *gemini.GeminiWebTokenStorage {
s.tokenMu.Lock() s.tokenMu.Lock()
defer s.tokenMu.Unlock() defer s.tokenMu.Unlock()
c := *s.token c := *s.token
@@ -170,15 +170,15 @@ type geminiWebPrepared struct {
translatedRaw []byte translatedRaw []byte
prompt string prompt string
uploaded []string uploaded []string
chat *geminiwebapi.ChatSession chat *ChatSession
cleaned []geminiwebapi.RoleText cleaned []RoleText
underlying string underlying string
reuse bool reuse bool
tagged bool tagged bool
originalRaw []byte originalRaw []byte
} }
func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON []byte, stream bool, original []byte) (*geminiWebPrepared, *interfaces.ErrorMessage) { func (s *GeminiWebState) prepare(ctx context.Context, modelName string, rawJSON []byte, stream bool, original []byte) (*geminiWebPrepared, *interfaces.ErrorMessage) {
res := &geminiWebPrepared{originalRaw: original} res := &geminiWebPrepared{originalRaw: original}
res.translatedRaw = bytes.Clone(rawJSON) res.translatedRaw = bytes.Clone(rawJSON)
if handler, ok := ctx.Value("handler").(interfaces.APIHandler); ok && handler != nil { if handler, ok := ctx.Value("handler").(interfaces.APIHandler); ok && handler != nil {
@@ -187,14 +187,14 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
} }
recordAPIRequest(ctx, s.cfg, res.translatedRaw) recordAPIRequest(ctx, s.cfg, res.translatedRaw)
messages, files, mimes, msgFileIdx, err := geminiwebapi.ParseMessagesAndFiles(res.translatedRaw) messages, files, mimes, msgFileIdx, err := ParseMessagesAndFiles(res.translatedRaw)
if err != nil { if err != nil {
return nil, &interfaces.ErrorMessage{StatusCode: 400, Error: fmt.Errorf("bad request: %w", err)} return nil, &interfaces.ErrorMessage{StatusCode: 400, Error: fmt.Errorf("bad request: %w", err)}
} }
cleaned := geminiwebapi.SanitizeAssistantMessages(messages) cleaned := SanitizeAssistantMessages(messages)
res.cleaned = cleaned res.cleaned = cleaned
res.underlying = geminiwebapi.MapAliasToUnderlying(modelName) res.underlying = MapAliasToUnderlying(modelName)
model, err := geminiwebapi.ModelFromName(res.underlying) model, err := ModelFromName(res.underlying)
if err != nil { if err != nil {
return nil, &interfaces.ErrorMessage{StatusCode: 400, Error: err} return nil, &interfaces.ErrorMessage{StatusCode: 400, Error: err}
} }
@@ -210,11 +210,11 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
res.reuse = true res.reuse = true
meta = reuseMeta meta = reuseMeta
if len(remaining) == 1 { if len(remaining) == 1 {
useMsgs = []geminiwebapi.RoleText{remaining[0]} useMsgs = []RoleText{remaining[0]}
} else if len(remaining) > 1 { } else if len(remaining) > 1 {
useMsgs = remaining useMsgs = remaining
} else if len(cleaned) > 0 { } else if len(cleaned) > 0 {
useMsgs = []geminiwebapi.RoleText{cleaned[len(cleaned)-1]} useMsgs = []RoleText{cleaned[len(cleaned)-1]}
} }
if len(useMsgs) == 1 && len(messages) > 0 && len(msgFileIdx) == len(messages) { if len(useMsgs) == 1 && len(messages) > 0 && len(msgFileIdx) == len(messages) {
lastIdx := len(msgFileIdx) - 1 lastIdx := len(msgFileIdx) - 1
@@ -242,8 +242,8 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
} }
} else { } else {
if len(cleaned) >= 2 && strings.EqualFold(cleaned[len(cleaned)-2].Role, "assistant") { if len(cleaned) >= 2 && strings.EqualFold(cleaned[len(cleaned)-2].Role, "assistant") {
keyUnderlying := geminiwebapi.AccountMetaKey(s.accountID, res.underlying) keyUnderlying := AccountMetaKey(s.accountID, res.underlying)
keyAlias := geminiwebapi.AccountMetaKey(s.accountID, modelName) keyAlias := AccountMetaKey(s.accountID, modelName)
s.convMu.RLock() s.convMu.RLock()
fallbackMeta := s.convStore[keyUnderlying] fallbackMeta := s.convStore[keyUnderlying]
if len(fallbackMeta) == 0 { if len(fallbackMeta) == 0 {
@@ -252,7 +252,7 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
s.convMu.RUnlock() s.convMu.RUnlock()
if len(fallbackMeta) > 0 { if len(fallbackMeta) > 0 {
meta = fallbackMeta meta = fallbackMeta
useMsgs = []geminiwebapi.RoleText{cleaned[len(cleaned)-1]} useMsgs = []RoleText{cleaned[len(cleaned)-1]}
res.reuse = true res.reuse = true
filesSubset = nil filesSubset = nil
mimesSubset = nil mimesSubset = nil
@@ -260,8 +260,8 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
} }
} }
} else { } else {
keyUnderlying := geminiwebapi.AccountMetaKey(s.accountID, res.underlying) keyUnderlying := AccountMetaKey(s.accountID, res.underlying)
keyAlias := geminiwebapi.AccountMetaKey(s.accountID, modelName) keyAlias := AccountMetaKey(s.accountID, modelName)
s.convMu.RLock() s.convMu.RLock()
if v, ok := s.convStore[keyUnderlying]; ok && len(v) > 0 { if v, ok := s.convStore[keyUnderlying]; ok && len(v) > 0 {
meta = v meta = v
@@ -271,26 +271,26 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
s.convMu.RUnlock() s.convMu.RUnlock()
} }
res.tagged = geminiwebapi.NeedRoleTags(useMsgs) res.tagged = NeedRoleTags(useMsgs)
if res.reuse && len(useMsgs) == 1 { if res.reuse && len(useMsgs) == 1 {
res.tagged = false res.tagged = false
} }
enableXML := s.cfg != nil && s.cfg.GeminiWeb.CodeMode enableXML := s.cfg != nil && s.cfg.GeminiWeb.CodeMode
useMsgs = geminiwebapi.AppendXMLWrapHintIfNeeded(useMsgs, !enableXML) useMsgs = AppendXMLWrapHintIfNeeded(useMsgs, !enableXML)
res.prompt = geminiwebapi.BuildPrompt(useMsgs, res.tagged, res.tagged) res.prompt = BuildPrompt(useMsgs, res.tagged, res.tagged)
if strings.TrimSpace(res.prompt) == "" { if strings.TrimSpace(res.prompt) == "" {
return nil, &interfaces.ErrorMessage{StatusCode: 400, Error: errors.New("bad request: empty prompt after filtering system/thought content")} return nil, &interfaces.ErrorMessage{StatusCode: 400, Error: errors.New("bad request: empty prompt after filtering system/thought content")}
} }
uploaded, upErr := geminiwebapi.MaterializeInlineFiles(filesSubset, mimesSubset) uploaded, upErr := MaterializeInlineFiles(filesSubset, mimesSubset)
if upErr != nil { if upErr != nil {
return nil, upErr return nil, upErr
} }
res.uploaded = uploaded res.uploaded = uploaded
if err = s.ensureClient(); err != nil { if err = s.EnsureClient(); err != nil {
return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: err} return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: err}
} }
chat := s.client.StartChat(model, s.getConfiguredGem(), meta) chat := s.client.StartChat(model, s.getConfiguredGem(), meta)
@@ -300,14 +300,14 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
return res, nil return res, nil
} }
func (s *geminiWebState) send(ctx context.Context, modelName string, reqPayload []byte, opts cliproxyexecutor.Options) ([]byte, *interfaces.ErrorMessage, *geminiWebPrepared) { func (s *GeminiWebState) Send(ctx context.Context, modelName string, reqPayload []byte, opts cliproxyexecutor.Options) ([]byte, *interfaces.ErrorMessage, *geminiWebPrepared) {
prep, errMsg := s.prepare(ctx, modelName, reqPayload, opts.Stream, opts.OriginalRequest) prep, errMsg := s.prepare(ctx, modelName, reqPayload, opts.Stream, opts.OriginalRequest)
if errMsg != nil { if errMsg != nil {
return nil, errMsg, nil return nil, errMsg, nil
} }
defer geminiwebapi.CleanupFiles(prep.uploaded) defer CleanupFiles(prep.uploaded)
output, err := geminiwebapi.SendWithSplit(prep.chat, prep.prompt, prep.uploaded, s.cfg) output, err := SendWithSplit(prep.chat, prep.prompt, prep.uploaded, s.cfg)
if err != nil { if err != nil {
return nil, s.wrapSendError(err), nil return nil, s.wrapSendError(err), nil
} }
@@ -331,7 +331,7 @@ func (s *geminiWebState) send(ctx context.Context, modelName string, reqPayload
} }
} }
gemBytes, err := geminiwebapi.ConvertOutputToGemini(&output, modelName, prep.prompt) gemBytes, err := ConvertOutputToGemini(&output, modelName, prep.prompt)
if err != nil { if err != nil {
return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: err}, nil return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: err}, nil
} }
@@ -341,13 +341,13 @@ func (s *geminiWebState) send(ctx context.Context, modelName string, reqPayload
return gemBytes, nil, prep return gemBytes, nil, prep
} }
func (s *geminiWebState) wrapSendError(genErr error) *interfaces.ErrorMessage { func (s *GeminiWebState) wrapSendError(genErr error) *interfaces.ErrorMessage {
status := 500 status := 500
var usage *geminiwebapi.UsageLimitExceeded var usage *UsageLimitExceeded
var blocked *geminiwebapi.TemporarilyBlocked var blocked *TemporarilyBlocked
var invalid *geminiwebapi.ModelInvalid var invalid *ModelInvalid
var valueErr *geminiwebapi.ValueError var valueErr *ValueError
var timeout *geminiwebapi.TimeoutError var timeout *TimeoutError
switch { switch {
case errors.As(genErr, &usage): case errors.As(genErr, &usage):
status = 429 status = 429
@@ -363,14 +363,14 @@ func (s *geminiWebState) wrapSendError(genErr error) *interfaces.ErrorMessage {
return &interfaces.ErrorMessage{StatusCode: status, Error: genErr} return &interfaces.ErrorMessage{StatusCode: status, Error: genErr}
} }
func (s *geminiWebState) persistConversation(modelName string, prep *geminiWebPrepared, output *geminiwebapi.ModelOutput) { func (s *GeminiWebState) persistConversation(modelName string, prep *geminiWebPrepared, output *ModelOutput) {
if output == nil || prep == nil || prep.chat == nil { if output == nil || prep == nil || prep.chat == nil {
return return
} }
metadata := prep.chat.Metadata() metadata := prep.chat.Metadata()
if len(metadata) > 0 { if len(metadata) > 0 {
keyUnderlying := geminiwebapi.AccountMetaKey(s.accountID, prep.underlying) keyUnderlying := AccountMetaKey(s.accountID, prep.underlying)
keyAlias := geminiwebapi.AccountMetaKey(s.accountID, modelName) keyAlias := AccountMetaKey(s.accountID, modelName)
s.convMu.Lock() s.convMu.Lock()
s.convStore[keyUnderlying] = metadata s.convStore[keyUnderlying] = metadata
s.convStore[keyAlias] = metadata s.convStore[keyAlias] = metadata
@@ -384,18 +384,18 @@ func (s *geminiWebState) persistConversation(modelName string, prep *geminiWebPr
storeSnapshot[k] = cp storeSnapshot[k] = cp
} }
s.convMu.Unlock() s.convMu.Unlock()
_ = geminiwebapi.SaveConvStore(s.convStorePath(), storeSnapshot) _ = SaveConvStore(s.convStorePath(), storeSnapshot)
} }
if !s.useReusableContext() { if !s.useReusableContext() {
return return
} }
rec, ok := geminiwebapi.BuildConversationRecord(prep.underlying, s.stableClientID, prep.cleaned, output, metadata) rec, ok := BuildConversationRecord(prep.underlying, s.stableClientID, prep.cleaned, output, metadata)
if !ok { if !ok {
return return
} }
stableHash := geminiwebapi.HashConversation(rec.ClientID, prep.underlying, rec.Messages) stableHash := HashConversation(rec.ClientID, prep.underlying, rec.Messages)
accountHash := geminiwebapi.HashConversation(s.accountID, prep.underlying, rec.Messages) accountHash := HashConversation(s.accountID, prep.underlying, rec.Messages)
s.convMu.Lock() s.convMu.Lock()
s.convData[stableHash] = rec s.convData[stableHash] = rec
@@ -403,7 +403,7 @@ func (s *geminiWebState) persistConversation(modelName string, prep *geminiWebPr
if accountHash != stableHash { if accountHash != stableHash {
s.convIndex["hash:"+accountHash] = stableHash s.convIndex["hash:"+accountHash] = stableHash
} }
dataSnapshot := make(map[string]geminiwebapi.ConversationRecord, len(s.convData)) dataSnapshot := make(map[string]ConversationRecord, len(s.convData))
for k, v := range s.convData { for k, v := range s.convData {
dataSnapshot[k] = v dataSnapshot[k] = v
} }
@@ -412,14 +412,14 @@ func (s *geminiWebState) persistConversation(modelName string, prep *geminiWebPr
indexSnapshot[k] = v indexSnapshot[k] = v
} }
s.convMu.Unlock() s.convMu.Unlock()
_ = geminiwebapi.SaveConvData(s.convDataPath(), dataSnapshot, indexSnapshot) _ = SaveConvData(s.convDataPath(), dataSnapshot, indexSnapshot)
} }
func (s *geminiWebState) addAPIResponseData(ctx context.Context, line []byte) { func (s *GeminiWebState) addAPIResponseData(ctx context.Context, line []byte) {
appendAPIResponseChunk(ctx, s.cfg, line) appendAPIResponseChunk(ctx, s.cfg, line)
} }
func (s *geminiWebState) convertToTarget(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []byte { func (s *GeminiWebState) ConvertToTarget(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []byte {
if prep == nil || prep.handlerType == "" { if prep == nil || prep.handlerType == "" {
return gemBytes return gemBytes
} }
@@ -437,7 +437,7 @@ func (s *geminiWebState) convertToTarget(ctx context.Context, modelName string,
return []byte(out) return []byte(out)
} }
func (s *geminiWebState) convertStream(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []string { func (s *GeminiWebState) ConvertStream(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []string {
if prep == nil || prep.handlerType == "" { if prep == nil || prep.handlerType == "" {
return []string{string(gemBytes)} return []string{string(gemBytes)}
} }
@@ -448,7 +448,7 @@ func (s *geminiWebState) convertStream(ctx context.Context, modelName string, pr
return translator.Response(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, gemBytes, &param) return translator.Response(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, gemBytes, &param)
} }
func (s *geminiWebState) doneStream(ctx context.Context, modelName string, prep *geminiWebPrepared) []string { func (s *GeminiWebState) DoneStream(ctx context.Context, modelName string, prep *geminiWebPrepared) []string {
if prep == nil || prep.handlerType == "" { if prep == nil || prep.handlerType == "" {
return nil return nil
} }
@@ -459,24 +459,56 @@ func (s *geminiWebState) doneStream(ctx context.Context, modelName string, prep
return translator.Response(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, []byte("[DONE]"), &param) return translator.Response(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, []byte("[DONE]"), &param)
} }
func (s *geminiWebState) useReusableContext() bool { func (s *GeminiWebState) useReusableContext() bool {
if s.cfg == nil { if s.cfg == nil {
return true return true
} }
return s.cfg.GeminiWeb.Context return s.cfg.GeminiWeb.Context
} }
func (s *geminiWebState) findReusableSession(modelName string, msgs []geminiwebapi.RoleText) ([]string, []geminiwebapi.RoleText) { func (s *GeminiWebState) findReusableSession(modelName string, msgs []RoleText) ([]string, []RoleText) {
s.convMu.RLock() s.convMu.RLock()
items := s.convData items := s.convData
index := s.convIndex index := s.convIndex
s.convMu.RUnlock() s.convMu.RUnlock()
return geminiwebapi.FindReusableSessionIn(items, index, s.stableClientID, s.accountID, modelName, msgs) return FindReusableSessionIn(items, index, s.stableClientID, s.accountID, modelName, msgs)
} }
func (s *geminiWebState) getConfiguredGem() *geminiwebapi.Gem { func (s *GeminiWebState) getConfiguredGem() *Gem {
if s.cfg != nil && s.cfg.GeminiWeb.CodeMode { if s.cfg != nil && s.cfg.GeminiWeb.CodeMode {
return &geminiwebapi.Gem{ID: "coding-partner", Name: "Coding partner", Predefined: true} return &Gem{ID: "coding-partner", Name: "Coding partner", Predefined: true}
} }
return nil return nil
} }
// recordAPIRequest stores the upstream request payload in Gin context for request logging.
func recordAPIRequest(ctx context.Context, cfg *config.Config, payload []byte) {
if cfg == nil || !cfg.RequestLog || len(payload) == 0 {
return
}
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil {
ginCtx.Set("API_REQUEST", bytes.Clone(payload))
}
}
// appendAPIResponseChunk appends an upstream response chunk to Gin context for request logging.
func appendAPIResponseChunk(ctx context.Context, cfg *config.Config, chunk []byte) {
if cfg == nil || !cfg.RequestLog {
return
}
data := bytes.TrimSpace(bytes.Clone(chunk))
if len(data) == 0 {
return
}
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil {
if existing, exists := ginCtx.Get("API_RESPONSE"); exists {
if prev, okBytes := existing.([]byte); okBytes {
prev = append(prev, data...)
prev = append(prev, []byte("\n\n")...)
ginCtx.Set("API_RESPONSE", prev)
return
}
}
ginCtx.Set("API_RESPONSE", data)
}
}

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
geminiwebapi "github.com/router-for-me/CLIProxyAPI/v6/internal/provider/gemini-web"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
@@ -35,23 +36,23 @@ func (e *GeminiWebExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
if err = state.ensureClient(); err != nil { if err = state.EnsureClient(); err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
mutex := state.getRequestMutex() mutex := state.GetRequestMutex()
if mutex != nil { if mutex != nil {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
} }
payload := bytes.Clone(req.Payload) payload := bytes.Clone(req.Payload)
resp, errMsg, prep := state.send(ctx, req.Model, payload, opts) resp, errMsg, prep := state.Send(ctx, req.Model, payload, opts)
if errMsg != nil { if errMsg != nil {
return cliproxyexecutor.Response{}, geminiWebErrorFromMessage(errMsg) return cliproxyexecutor.Response{}, geminiWebErrorFromMessage(errMsg)
} }
resp = state.convertToTarget(ctx, req.Model, prep, resp) resp = state.ConvertToTarget(ctx, req.Model, prep, resp)
reporter.publish(ctx, parseGeminiUsage(resp)) reporter.publish(ctx, parseGeminiUsage(resp))
from := opts.SourceFormat from := opts.SourceFormat
@@ -67,17 +68,17 @@ func (e *GeminiWebExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = state.ensureClient(); err != nil { if err = state.EnsureClient(); err != nil {
return nil, err return nil, err
} }
reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
mutex := state.getRequestMutex() mutex := state.GetRequestMutex()
if mutex != nil { if mutex != nil {
mutex.Lock() mutex.Lock()
} }
gemBytes, errMsg, prep := state.send(ctx, req.Model, bytes.Clone(req.Payload), opts) gemBytes, errMsg, prep := state.Send(ctx, req.Model, bytes.Clone(req.Payload), opts)
if errMsg != nil { if errMsg != nil {
if mutex != nil { if mutex != nil {
mutex.Unlock() mutex.Unlock()
@@ -90,8 +91,8 @@ func (e *GeminiWebExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
to := sdktranslator.FromString("gemini-web") to := sdktranslator.FromString("gemini-web")
var param any var param any
lines := state.convertStream(ctx, req.Model, prep, gemBytes) lines := state.ConvertStream(ctx, req.Model, prep, gemBytes)
done := state.doneStream(ctx, req.Model, prep) done := state.DoneStream(ctx, req.Model, prep)
out := make(chan cliproxyexecutor.StreamChunk) out := make(chan cliproxyexecutor.StreamChunk)
go func() { go func() {
defer close(out) defer close(out)
@@ -124,10 +125,10 @@ func (e *GeminiWebExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = state.refresh(ctx); err != nil { if err = state.Refresh(ctx); err != nil {
return nil, err return nil, err
} }
ts := state.tokenSnapshot() ts := state.TokenSnapshot()
if auth.Metadata == nil { if auth.Metadata == nil {
auth.Metadata = make(map[string]any) auth.Metadata = make(map[string]any)
} }
@@ -139,10 +140,10 @@ func (e *GeminiWebExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth
} }
type geminiWebRuntime struct { type geminiWebRuntime struct {
state *geminiWebState state *geminiwebapi.GeminiWebState
} }
func (e *GeminiWebExecutor) stateFor(auth *cliproxyauth.Auth) (*geminiWebState, error) { func (e *GeminiWebExecutor) stateFor(auth *cliproxyauth.Auth) (*geminiwebapi.GeminiWebState, error) {
if auth == nil { if auth == nil {
return nil, fmt.Errorf("gemini-web executor: auth is nil") return nil, fmt.Errorf("gemini-web executor: auth is nil")
} }
@@ -175,7 +176,7 @@ func (e *GeminiWebExecutor) stateFor(auth *cliproxyauth.Auth) (*geminiWebState,
storagePath = p storagePath = p
} }
} }
state := newGeminiWebState(cfg, ts, storagePath) state := geminiwebapi.NewGeminiWebState(cfg, ts, storagePath)
runtime := &geminiWebRuntime{state: state} runtime := &geminiWebRuntime{state: state}
auth.Runtime = runtime auth.Runtime = runtime
return state, nil return state, nil

View File

@@ -10,7 +10,7 @@ import (
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api" "github.com/router-for-me/CLIProxyAPI/v6/internal/api"
geminiwebclient "github.com/router-for-me/CLIProxyAPI/v6/internal/client/gemini-web" geminiwebclient "github.com/router-for-me/CLIProxyAPI/v6/internal/provider/gemini-web"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor" "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor"