shinnmen / internal /runtime /executor /iflow_executor_refactored.go
shinmentakezo07
refactor(executor): split providers and preserve legacy entrypoints
3e5ab39
Raw
History Blame Contribute Delete
9.07 kB
package executor
import (
"context"
"fmt"
"net/http"
"strings"
"time"
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
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"
)
// IFlowExecutorRefactored executes OpenAI-compatible chat completions against the iFlow API.
type IFlowExecutorRefactored struct {
cfg *config.Config
base *BaseExecutor
}
// NewIFlowExecutorRefactored constructs a new executor instance.
func NewIFlowExecutorRefactored(cfg *config.Config) *IFlowExecutorRefactored {
provider := &IFlowProvider{}
return &IFlowExecutorRefactored{
cfg: cfg,
base: NewBaseExecutor(cfg, provider),
}
}
// Identifier returns the provider key.
func (e *IFlowExecutorRefactored) Identifier() string { return "iflow" }
// PrepareRequest injects iFlow credentials into the outgoing HTTP request.
func (e *IFlowExecutorRefactored) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error {
if req == nil {
return nil
}
apiKey, _ := iflowCreds(auth)
if strings.TrimSpace(apiKey) != "" {
req.Header.Set("Authorization", "Bearer "+apiKey)
}
return nil
}
// HttpRequest injects iFlow credentials into the request and executes it.
func (e *IFlowExecutorRefactored) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) {
if req == nil {
return nil, fmt.Errorf("iflow executor: request is nil")
}
if ctx == nil {
ctx = req.Context()
}
httpReq := req.WithContext(ctx)
if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err
}
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq)
}
// Execute performs a non-streaming chat completion request.
func (e *IFlowExecutorRefactored) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
if opts.Alt == "responses/compact" {
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
}
apiKey, _ := iflowCreds(auth)
if strings.TrimSpace(apiKey) == "" {
return resp, fmt.Errorf("iflow executor: missing api key")
}
return e.base.Execute(ctx, auth, req, opts)
}
// ExecuteStream performs a streaming chat completion request.
func (e *IFlowExecutorRefactored) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) {
if opts.Alt == "responses/compact" {
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
}
apiKey, _ := iflowCreds(auth)
if strings.TrimSpace(apiKey) == "" {
return nil, fmt.Errorf("iflow executor: missing api key")
}
return e.base.ExecuteStream(ctx, auth, req, opts)
}
// CountTokens estimates token count for iFlow requests.
func (e *IFlowExecutorRefactored) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
baseModel := thinking.ParseSuffix(req.Model).ModelName
from := opts.SourceFormat
to := sdktranslator.FromString("openai")
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
enc, err := tokenizerForModel(baseModel)
if err != nil {
return cliproxyexecutor.Response{}, fmt.Errorf("iflow executor: tokenizer init failed: %w", err)
}
count, err := countOpenAIChatTokens(enc, body)
if err != nil {
return cliproxyexecutor.Response{}, fmt.Errorf("iflow executor: token counting failed: %w", err)
}
usageJSON := buildOpenAIUsageJSON(count)
translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
}
// Refresh refreshes OAuth tokens or cookie-based API keys and updates the stored API key.
func (e *IFlowExecutorRefactored) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
log.Debugf("iflow executor: refresh called")
if auth == nil {
return nil, fmt.Errorf("iflow executor: auth is nil")
}
// Check if this is cookie-based authentication
var cookie string
var email string
if auth.Metadata != nil {
if v, ok := auth.Metadata["cookie"].(string); ok {
cookie = strings.TrimSpace(v)
}
if v, ok := auth.Metadata["email"].(string); ok {
email = strings.TrimSpace(v)
}
}
// If cookie is present, use cookie-based refresh
if cookie != "" && email != "" {
return e.refreshCookieBased(ctx, auth, cookie, email)
}
// Otherwise, use OAuth-based refresh
return e.refreshOAuthBased(ctx, auth)
}
// refreshCookieBased refreshes API key using browser cookie
func (e *IFlowExecutorRefactored) refreshCookieBased(ctx context.Context, auth *cliproxyauth.Auth, cookie, email string) (*cliproxyauth.Auth, error) {
log.Debugf("iflow executor: checking refresh need for cookie-based API key for user: %s", email)
// Get current expiry time from metadata
var currentExpire string
if auth.Metadata != nil {
if v, ok := auth.Metadata["expired"].(string); ok {
currentExpire = strings.TrimSpace(v)
}
}
// Check if refresh is needed
needsRefresh, _, err := iflowauth.ShouldRefreshAPIKey(currentExpire)
if err != nil {
log.Warnf("iflow executor: failed to check refresh need: %v", err)
// If we can't check, continue with refresh anyway as a safety measure
} else if !needsRefresh {
log.Debugf("iflow executor: no refresh needed for user: %s", email)
return auth, nil
}
log.Infof("iflow executor: refreshing cookie-based API key for user: %s", email)
svc := iflowauth.NewIFlowAuth(e.cfg)
keyData, err := svc.RefreshAPIKey(ctx, cookie, email)
if err != nil {
log.Errorf("iflow executor: cookie-based API key refresh failed: %v", err)
return nil, err
}
if auth.Metadata == nil {
auth.Metadata = make(map[string]any)
}
auth.Metadata["api_key"] = keyData.APIKey
auth.Metadata["expired"] = keyData.ExpireTime
auth.Metadata["type"] = "iflow"
auth.Metadata["last_refresh"] = time.Now().Format(time.RFC3339)
auth.Metadata["cookie"] = cookie
auth.Metadata["email"] = email
log.Infof("iflow executor: cookie-based API key refreshed successfully, new expiry: %s", keyData.ExpireTime)
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
auth.Attributes["api_key"] = keyData.APIKey
return auth, nil
}
// refreshOAuthBased refreshes tokens using OAuth refresh token
func (e *IFlowExecutorRefactored) refreshOAuthBased(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
refreshToken := ""
oldAccessToken := ""
if auth.Metadata != nil {
if v, ok := auth.Metadata["refresh_token"].(string); ok {
refreshToken = strings.TrimSpace(v)
}
if v, ok := auth.Metadata["access_token"].(string); ok {
oldAccessToken = strings.TrimSpace(v)
}
}
if refreshToken == "" {
return auth, nil
}
// Log the old access token (masked) before refresh
if oldAccessToken != "" {
log.Debugf("iflow executor: refreshing access token, old: %s", util.HideAPIKey(oldAccessToken))
}
svc := iflowauth.NewIFlowAuth(e.cfg)
tokenData, err := svc.RefreshTokens(ctx, refreshToken)
if err != nil {
log.Errorf("iflow executor: token refresh failed: %v", err)
return nil, err
}
if auth.Metadata == nil {
auth.Metadata = make(map[string]any)
}
auth.Metadata["access_token"] = tokenData.AccessToken
if tokenData.RefreshToken != "" {
auth.Metadata["refresh_token"] = tokenData.RefreshToken
}
if tokenData.APIKey != "" {
auth.Metadata["api_key"] = tokenData.APIKey
}
auth.Metadata["expired"] = tokenData.Expire
auth.Metadata["type"] = "iflow"
auth.Metadata["last_refresh"] = time.Now().Format(time.RFC3339)
// Log the new access token (masked) after successful refresh
log.Debugf("iflow executor: token refresh successful, new: %s", util.HideAPIKey(tokenData.AccessToken))
if auth.Attributes == nil {
auth.Attributes = make(map[string]string)
}
if tokenData.APIKey != "" {
auth.Attributes["api_key"] = tokenData.APIKey
}
return auth, nil
}
func iflowCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
if a == nil {
return "", ""
}
if a.Attributes != nil {
if v := strings.TrimSpace(a.Attributes["api_key"]); v != "" {
apiKey = v
}
if v := strings.TrimSpace(a.Attributes["base_url"]); v != "" {
baseURL = v
}
}
if apiKey == "" && a.Metadata != nil {
if v, ok := a.Metadata["api_key"].(string); ok {
apiKey = strings.TrimSpace(v)
}
}
if baseURL == "" && a.Metadata != nil {
if v, ok := a.Metadata["base_url"].(string); ok {
baseURL = strings.TrimSpace(v)
}
}
return apiKey, baseURL
}