mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 20:30:51 +08:00
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.
238 lines
6.9 KiB
Go
238 lines
6.9 KiB
Go
package executor
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"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/interfaces"
|
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type GeminiWebExecutor struct {
|
|
cfg *config.Config
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func NewGeminiWebExecutor(cfg *config.Config) *GeminiWebExecutor {
|
|
return &GeminiWebExecutor{cfg: cfg}
|
|
}
|
|
|
|
func (e *GeminiWebExecutor) Identifier() string { return "gemini-web" }
|
|
|
|
func (e *GeminiWebExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) error { return nil }
|
|
|
|
func (e *GeminiWebExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
|
|
state, err := e.stateFor(auth)
|
|
if err != nil {
|
|
return cliproxyexecutor.Response{}, err
|
|
}
|
|
if err = state.EnsureClient(); err != nil {
|
|
return cliproxyexecutor.Response{}, err
|
|
}
|
|
reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
|
|
|
|
mutex := state.GetRequestMutex()
|
|
if mutex != nil {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
}
|
|
|
|
payload := bytes.Clone(req.Payload)
|
|
resp, errMsg, prep := state.Send(ctx, req.Model, payload, opts)
|
|
if errMsg != nil {
|
|
return cliproxyexecutor.Response{}, geminiWebErrorFromMessage(errMsg)
|
|
}
|
|
resp = state.ConvertToTarget(ctx, req.Model, prep, resp)
|
|
reporter.publish(ctx, parseGeminiUsage(resp))
|
|
|
|
from := opts.SourceFormat
|
|
to := sdktranslator.FromString("gemini-web")
|
|
var param any
|
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), payload, bytes.Clone(resp), ¶m)
|
|
|
|
return cliproxyexecutor.Response{Payload: []byte(out)}, nil
|
|
}
|
|
|
|
func (e *GeminiWebExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) {
|
|
state, err := e.stateFor(auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = state.EnsureClient(); err != nil {
|
|
return nil, err
|
|
}
|
|
reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
|
|
|
|
mutex := state.GetRequestMutex()
|
|
if mutex != nil {
|
|
mutex.Lock()
|
|
}
|
|
|
|
gemBytes, errMsg, prep := state.Send(ctx, req.Model, bytes.Clone(req.Payload), opts)
|
|
if errMsg != nil {
|
|
if mutex != nil {
|
|
mutex.Unlock()
|
|
}
|
|
return nil, geminiWebErrorFromMessage(errMsg)
|
|
}
|
|
reporter.publish(ctx, parseGeminiUsage(gemBytes))
|
|
|
|
from := opts.SourceFormat
|
|
to := sdktranslator.FromString("gemini-web")
|
|
var param any
|
|
|
|
lines := state.ConvertStream(ctx, req.Model, prep, gemBytes)
|
|
done := state.DoneStream(ctx, req.Model, prep)
|
|
out := make(chan cliproxyexecutor.StreamChunk)
|
|
go func() {
|
|
defer close(out)
|
|
if mutex != nil {
|
|
defer mutex.Unlock()
|
|
}
|
|
for _, line := range lines {
|
|
lines = sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), req.Payload, bytes.Clone([]byte(line)), ¶m)
|
|
for _, l := range lines {
|
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(l)}
|
|
}
|
|
}
|
|
for _, line := range done {
|
|
lines = sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), req.Payload, bytes.Clone([]byte(line)), ¶m)
|
|
for _, l := range lines {
|
|
out <- cliproxyexecutor.StreamChunk{Payload: []byte(l)}
|
|
}
|
|
}
|
|
}()
|
|
return out, nil
|
|
}
|
|
|
|
func (e *GeminiWebExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
|
|
return cliproxyexecutor.Response{Payload: []byte{}}, fmt.Errorf("not implemented")
|
|
}
|
|
|
|
func (e *GeminiWebExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
|
log.Debugf("gemini web executor: refresh called")
|
|
state, err := e.stateFor(auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = state.Refresh(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
ts := state.TokenSnapshot()
|
|
if auth.Metadata == nil {
|
|
auth.Metadata = make(map[string]any)
|
|
}
|
|
auth.Metadata["secure_1psid"] = ts.Secure1PSID
|
|
auth.Metadata["secure_1psidts"] = ts.Secure1PSIDTS
|
|
auth.Metadata["type"] = "gemini-web"
|
|
auth.Metadata["last_refresh"] = time.Now().Format(time.RFC3339)
|
|
return auth, nil
|
|
}
|
|
|
|
type geminiWebRuntime struct {
|
|
state *geminiwebapi.GeminiWebState
|
|
}
|
|
|
|
func (e *GeminiWebExecutor) stateFor(auth *cliproxyauth.Auth) (*geminiwebapi.GeminiWebState, error) {
|
|
if auth == nil {
|
|
return nil, fmt.Errorf("gemini-web executor: auth is nil")
|
|
}
|
|
if runtime, ok := auth.Runtime.(*geminiWebRuntime); ok && runtime != nil && runtime.state != nil {
|
|
return runtime.state, nil
|
|
}
|
|
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if runtime, ok := auth.Runtime.(*geminiWebRuntime); ok && runtime != nil && runtime.state != nil {
|
|
return runtime.state, nil
|
|
}
|
|
|
|
ts, err := parseGeminiWebToken(auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg := e.cfg
|
|
if auth.ProxyURL != "" && cfg != nil {
|
|
copyCfg := *cfg
|
|
copyCfg.ProxyURL = auth.ProxyURL
|
|
cfg = ©Cfg
|
|
}
|
|
|
|
storagePath := ""
|
|
if auth.Attributes != nil {
|
|
if p, ok := auth.Attributes["path"]; ok {
|
|
storagePath = p
|
|
}
|
|
}
|
|
state := geminiwebapi.NewGeminiWebState(cfg, ts, storagePath)
|
|
runtime := &geminiWebRuntime{state: state}
|
|
auth.Runtime = runtime
|
|
return state, nil
|
|
}
|
|
|
|
func parseGeminiWebToken(auth *cliproxyauth.Auth) (*gemini.GeminiWebTokenStorage, error) {
|
|
if auth == nil {
|
|
return nil, fmt.Errorf("gemini-web executor: auth is nil")
|
|
}
|
|
if auth.Metadata == nil {
|
|
return nil, fmt.Errorf("gemini-web executor: missing metadata")
|
|
}
|
|
psid := stringFromMetadata(auth.Metadata, "secure_1psid", "secure_1psid", "__Secure-1PSID")
|
|
psidts := stringFromMetadata(auth.Metadata, "secure_1psidts", "secure_1psidts", "__Secure-1PSIDTS")
|
|
if psid == "" || psidts == "" {
|
|
return nil, fmt.Errorf("gemini-web executor: incomplete cookie metadata")
|
|
}
|
|
return &gemini.GeminiWebTokenStorage{Secure1PSID: psid, Secure1PSIDTS: psidts}, nil
|
|
}
|
|
|
|
func stringFromMetadata(meta map[string]any, keys ...string) string {
|
|
for _, key := range keys {
|
|
if val, ok := meta[key]; ok {
|
|
if s, okStr := val.(string); okStr && s != "" {
|
|
return s
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func geminiWebErrorFromMessage(msg *interfaces.ErrorMessage) error {
|
|
if msg == nil {
|
|
return nil
|
|
}
|
|
return geminiWebError{message: msg}
|
|
}
|
|
|
|
type geminiWebError struct {
|
|
message *interfaces.ErrorMessage
|
|
}
|
|
|
|
func (e geminiWebError) Error() string {
|
|
if e.message == nil {
|
|
return "gemini-web error"
|
|
}
|
|
if e.message.Error != nil {
|
|
return e.message.Error.Error()
|
|
}
|
|
return fmt.Sprintf("gemini-web error: status %d", e.message.StatusCode)
|
|
}
|
|
|
|
func (e geminiWebError) StatusCode() int {
|
|
if e.message == nil {
|
|
return 0
|
|
}
|
|
return e.message.StatusCode
|
|
}
|