mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
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:
@@ -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, ¶m)
|
return translator.Response(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, gemBytes, ¶m)
|
||||||
}
|
}
|
||||||
|
|
||||||
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]"), ¶m)
|
return translator.Response(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, []byte("[DONE]"), ¶m)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user