diff --git a/.gitignore b/.gitignore index d48205b1..ea0cda89 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ auths/* AGENTS.md CLAUDE.md *.exe -temp/* \ No newline at end of file +temp/* +.serena/ \ No newline at end of file diff --git a/internal/api/handlers/handlers.go b/internal/api/handlers/handlers.go index 84509306..92d5817c 100644 --- a/internal/api/handlers/handlers.go +++ b/internal/api/handlers/handlers.go @@ -10,7 +10,6 @@ import ( "github.com/gin-gonic/gin" "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/runtime/executor" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" @@ -150,9 +149,6 @@ func (h *BaseAPIHandler) ExecuteWithAuthManager(ctx context.Context, handlerType } resp, err := h.AuthManager.Execute(ctx, providers, req, opts) if err != nil { - if msg, ok := executor.UnwrapError(err); ok { - return nil, msg - } return nil, &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: err} } return cloneBytes(resp.Payload), nil @@ -177,9 +173,6 @@ func (h *BaseAPIHandler) ExecuteCountWithAuthManager(ctx context.Context, handle } resp, err := h.AuthManager.ExecuteCount(ctx, providers, req, opts) if err != nil { - if msg, ok := executor.UnwrapError(err); ok { - return nil, msg - } return nil, &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: err} } return cloneBytes(resp.Payload), nil @@ -208,11 +201,7 @@ func (h *BaseAPIHandler) ExecuteStreamWithAuthManager(ctx context.Context, handl chunks, err := h.AuthManager.ExecuteStream(ctx, providers, req, opts) if err != nil { errChan := make(chan *interfaces.ErrorMessage, 1) - if msg, ok := executor.UnwrapError(err); ok { - errChan <- msg - } else { - errChan <- &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: err} - } + errChan <- &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: err} close(errChan) return nil, errChan } @@ -223,11 +212,7 @@ func (h *BaseAPIHandler) ExecuteStreamWithAuthManager(ctx context.Context, handl defer close(errChan) for chunk := range chunks { if chunk.Err != nil { - if msg, ok := executor.UnwrapError(chunk.Err); ok { - errChan <- msg - } else { - errChan <- &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: chunk.Err} - } + errChan <- &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: chunk.Err} return } if len(chunk.Payload) > 0 { diff --git a/internal/registry/model_registry.go b/internal/registry/model_registry.go index 74e7abf4..079e6271 100644 --- a/internal/registry/model_registry.go +++ b/internal/registry/model_registry.go @@ -393,23 +393,24 @@ func (r *ModelRegistry) GetModelProviders(modelID string) []string { count int } providers := make([]providerCount, 0, len(registration.Providers)) - suspendedByProvider := make(map[string]int) - if registration.SuspendedClients != nil { - for clientID := range registration.SuspendedClients { - if provider, ok := r.clientProviders[clientID]; ok && provider != "" { - suspendedByProvider[provider]++ - } - } - } + // suspendedByProvider := make(map[string]int) + // if registration.SuspendedClients != nil { + // for clientID := range registration.SuspendedClients { + // if provider, ok := r.clientProviders[clientID]; ok && provider != "" { + // suspendedByProvider[provider]++ + // } + // } + // } for name, count := range registration.Providers { if count <= 0 { continue } - adjusted := count - suspendedByProvider[name] - if adjusted <= 0 { - continue - } - providers = append(providers, providerCount{name: name, count: adjusted}) + // adjusted := count - suspendedByProvider[name] + // if adjusted <= 0 { + // continue + // } + // providers = append(providers, providerCount{name: name, count: adjusted}) + providers = append(providers, providerCount{name: name, count: count}) } if len(providers) == 0 { return nil diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 2bea5d5e..45ef782d 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -38,9 +38,7 @@ func (e *ClaudeExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) e func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { apiKey, baseURL := claudeCreds(auth) - if apiKey == "" { - return NewClientAdapter("claude").Execute(ctx, auth, req, opts) - } + if baseURL == "" { baseURL = "https://api.anthropic.com" } @@ -114,9 +112,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) { apiKey, baseURL := claudeCreds(auth) - if apiKey == "" { - return NewClientAdapter("claude").ExecuteStream(ctx, auth, req, opts) - } + if baseURL == "" { baseURL = "https://api.anthropic.com" } @@ -177,9 +173,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { apiKey, baseURL := claudeCreds(auth) - if apiKey == "" { - return NewClientAdapter("claude").Execute(ctx, auth, req, opts) - } + if baseURL == "" { baseURL = "https://api.anthropic.com" } diff --git a/internal/runtime/executor/client_executor.go b/internal/runtime/executor/client_executor.go deleted file mode 100644 index a14d6459..00000000 --- a/internal/runtime/executor/client_executor.go +++ /dev/null @@ -1,185 +0,0 @@ -package executor - -import ( - "context" - "errors" - "fmt" - "net/http" - "sync" - - "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" -) - -// ClientAdapter bridges legacy stateful clients to the new ProviderExecutor contract. -type ClientAdapter struct { - provider string -} - -// NewClientAdapter creates a new adapter for the specified provider key. -func NewClientAdapter(provider string) *ClientAdapter { - return &ClientAdapter{provider: provider} -} - -// Identifier implements cliproxyauth.ProviderExecutor. -func (a *ClientAdapter) Identifier() string { - return a.provider -} - -// PrepareRequest implements optional request preparation hook (no-op for legacy clients). -func (a *ClientAdapter) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error { return nil } - -// Execute implements cliproxyauth.ProviderExecutor. -func (a *ClientAdapter) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { - client, mutex, err := resolveLegacyClient(auth) - if err != nil { - return cliproxyexecutor.Response{}, err - } - unlock := lock(mutex) - defer unlock() - - // Support special actions via request metadata (e.g., countTokens) - if req.Metadata != nil { - if action, _ := req.Metadata["action"].(string); action == "countTokens" { - if tc, ok := any(client).(interface { - SendRawTokenCount(ctx context.Context, modelName string, rawJSON []byte, alt string) ([]byte, *interfaces.ErrorMessage) - }); ok { - payload, errMsg := tc.SendRawTokenCount(ctx, req.Model, req.Payload, opts.Alt) - if errMsg != nil { - return cliproxyexecutor.Response{}, errorFromMessage(errMsg) - } - return cliproxyexecutor.Response{Payload: payload}, nil - } - return cliproxyexecutor.Response{}, fmt.Errorf("legacy client does not support countTokens") - } - } - - payload, errMsg := client.SendRawMessage(ctx, req.Model, req.Payload, opts.Alt) - if errMsg != nil { - return cliproxyexecutor.Response{}, errorFromMessage(errMsg) - } - return cliproxyexecutor.Response{Payload: payload}, nil -} - -// ExecuteStream implements cliproxyauth.ProviderExecutor. -func (a *ClientAdapter) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) { - client, mutex, err := resolveLegacyClient(auth) - if err != nil { - return nil, err - } - unlock := lock(mutex) - - dataCh, errCh := client.SendRawMessageStream(ctx, req.Model, req.Payload, opts.Alt) - if dataCh == nil { - unlock() - if errCh != nil { - if msg := <-errCh; msg != nil { - return nil, errorFromMessage(msg) - } - } - return nil, errors.New("stream not available") - } - - out := make(chan cliproxyexecutor.StreamChunk) - go func() { - defer close(out) - defer unlock() - for chunk := range dataCh { - if chunk == nil { - continue - } - out <- cliproxyexecutor.StreamChunk{Payload: chunk} - } - if errCh != nil { - if msg, ok := <-errCh; ok && msg != nil { - out <- cliproxyexecutor.StreamChunk{Err: errorFromMessage(msg)} - } - } - }() - return out, nil -} - -func (e *ClientAdapter) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { - return cliproxyexecutor.Response{Payload: []byte{}}, nil -} - -// Refresh delegates to the legacy client's refresh logic when available. -func (a *ClientAdapter) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { - client, _, err := resolveLegacyClient(auth) - if err != nil { - return nil, err - } - if refresher, ok := client.(interface{ RefreshTokens(context.Context) error }); ok { - if errRefresh := refresher.RefreshTokens(ctx); errRefresh != nil { - return nil, errRefresh - } - } - return auth, nil -} - -// legacyClient defines the minimum surface required from the historical clients. -type legacyClient interface { - SendRawMessage(ctx context.Context, modelName string, rawJSON []byte, alt string) ([]byte, *interfaces.ErrorMessage) - SendRawMessageStream(ctx context.Context, modelName string, rawJSON []byte, alt string) (<-chan []byte, <-chan *interfaces.ErrorMessage) - GetRequestMutex() *sync.Mutex -} - -func resolveLegacyClient(auth *cliproxyauth.Auth) (legacyClient, *sync.Mutex, error) { - if auth == nil { - return nil, nil, fmt.Errorf("legacy adapter: auth is nil") - } - client, ok := auth.Runtime.(legacyClient) - if !ok || client == nil { - return nil, nil, fmt.Errorf("legacy adapter: runtime client missing for %s", auth.ID) - } - return client, client.GetRequestMutex(), nil -} - -func lock(mutex *sync.Mutex) func() { - if mutex == nil { - return func() {} - } - mutex.Lock() - return func() { - mutex.Unlock() - } -} - -func errorFromMessage(msg *interfaces.ErrorMessage) error { - if msg == nil { - return nil - } - return legacyError{message: msg} -} - -type legacyError struct { - message *interfaces.ErrorMessage -} - -func (e legacyError) Error() string { - if e.message == nil { - return "legacy client error" - } - if e.message.Error != nil { - return e.message.Error.Error() - } - return fmt.Sprintf("legacy client error: status %d", e.message.StatusCode) -} - -// StatusCode implements executor.StatusError, exposing HTTP-like status. -func (e legacyError) StatusCode() int { - if e.message != nil { - return e.message.StatusCode - } - return 0 -} - -// UnwrapError extracts the legacy interfaces.ErrorMessage from adapter errors. -func UnwrapError(err error) (*interfaces.ErrorMessage, bool) { - var legacy legacyError - if errors.As(err, &legacy) { - return legacy.message, true - } - return nil, false -} diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index d07959db..464e2c47 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -41,9 +41,7 @@ func (e *CodexExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) er func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { apiKey, baseURL := codexCreds(auth) - if apiKey == "" { - return NewClientAdapter("codex").Execute(ctx, auth, req, opts) - } + if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } @@ -136,9 +134,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) { apiKey, baseURL := codexCreds(auth) - if apiKey == "" { - return NewClientAdapter("codex").ExecuteStream(ctx, auth, req, opts) - } + if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } diff --git a/internal/runtime/executor/gemini_executor.go b/internal/runtime/executor/gemini_executor.go index 2c46f5b9..40692c2d 100644 --- a/internal/runtime/executor/gemini_executor.go +++ b/internal/runtime/executor/gemini_executor.go @@ -40,10 +40,7 @@ func (e *GeminiExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) e func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { apiKey, bearer := geminiCreds(auth) - if apiKey == "" && bearer == "" { - // Fallback to legacy client - return NewClientAdapter("gemini").Execute(ctx, auth, req, opts) - } + reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) // Official Gemini API via API key or OAuth bearer @@ -102,10 +99,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) { apiKey, bearer := geminiCreds(auth) - if apiKey == "" && bearer == "" { - // Fallback to legacy streaming - return NewClientAdapter("gemini").ExecuteStream(ctx, auth, req, opts) - } + reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth) from := opts.SourceFormat diff --git a/internal/runtime/executor/qwen_executor.go b/internal/runtime/executor/qwen_executor.go index a7bced67..c11bcb72 100644 --- a/internal/runtime/executor/qwen_executor.go +++ b/internal/runtime/executor/qwen_executor.go @@ -40,9 +40,7 @@ func (e *QwenExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) err func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { token, baseURL := qwenCreds(auth) - if token == "" { - return NewClientAdapter("qwen").Execute(ctx, auth, req, opts) - } + if baseURL == "" { baseURL = "https://portal.qwen.ai/v1" } @@ -88,9 +86,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) { token, baseURL := qwenCreds(auth) - if token == "" { - return NewClientAdapter("qwen").ExecuteStream(ctx, auth, req, opts) - } + if baseURL == "" { baseURL = "https://portal.qwen.ai/v1" }