diff --git a/internal/api/handlers/claude/code_handlers.go b/internal/api/handlers/claude/code_handlers.go index 81eeeac5..ed7b0339 100644 --- a/internal/api/handlers/claude/code_handlers.go +++ b/internal/api/handlers/claude/code_handlers.go @@ -43,7 +43,7 @@ func NewClaudeCodeAPIHandler(apiHandlers *handlers.BaseAPIHandler) *ClaudeCodeAP // HandlerType returns the identifier for this handler implementation. func (h *ClaudeCodeAPIHandler) HandlerType() string { - return CLAUDE + return Claude } // Models returns a list of models supported by this handler. diff --git a/internal/api/handlers/gemini/gemini-cli_handlers.go b/internal/api/handlers/gemini/gemini-cli_handlers.go index 6b2b4170..75afda33 100644 --- a/internal/api/handlers/gemini/gemini-cli_handlers.go +++ b/internal/api/handlers/gemini/gemini-cli_handlers.go @@ -38,7 +38,7 @@ func NewGeminiCLIAPIHandler(apiHandlers *handlers.BaseAPIHandler) *GeminiCLIAPIH // HandlerType returns the type of this handler. func (h *GeminiCLIAPIHandler) HandlerType() string { - return GEMINICLI + return GeminiCLI } // Models returns a list of models supported by this handler. diff --git a/internal/api/handlers/gemini/gemini_handlers.go b/internal/api/handlers/gemini/gemini_handlers.go index e72378d8..1fab54ba 100644 --- a/internal/api/handlers/gemini/gemini_handlers.go +++ b/internal/api/handlers/gemini/gemini_handlers.go @@ -38,7 +38,7 @@ func NewGeminiAPIHandler(apiHandlers *handlers.BaseAPIHandler) *GeminiAPIHandler // HandlerType returns the identifier for this handler implementation. func (h *GeminiAPIHandler) HandlerType() string { - return GEMINI + return Gemini } // Models returns the Gemini-compatible model metadata supported by this handler. diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 05d2807d..270567c5 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -147,7 +147,7 @@ func (h *Handler) UploadAuthFile(c *gin.Context) { c.JSON(500, gin.H{"error": fmt.Sprintf("failed to write file: %v", errWrite)}) return } - if err := h.registerAuthFromFile(ctx, dst, data); err != nil { + if err = h.registerAuthFromFile(ctx, dst, data); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } diff --git a/internal/api/handlers/openai/openai_handlers.go b/internal/api/handlers/openai/openai_handlers.go index d5dc1213..504c2859 100644 --- a/internal/api/handlers/openai/openai_handlers.go +++ b/internal/api/handlers/openai/openai_handlers.go @@ -44,7 +44,7 @@ func NewOpenAIAPIHandler(apiHandlers *handlers.BaseAPIHandler) *OpenAIAPIHandler // HandlerType returns the identifier for this handler implementation. func (h *OpenAIAPIHandler) HandlerType() string { - return OPENAI + return OpenAI } // Models returns the OpenAI-compatible model metadata supported by this handler. diff --git a/internal/api/handlers/openai/openai_responses_handlers.go b/internal/api/handlers/openai/openai_responses_handlers.go index 477dea23..22bef82e 100644 --- a/internal/api/handlers/openai/openai_responses_handlers.go +++ b/internal/api/handlers/openai/openai_responses_handlers.go @@ -43,7 +43,7 @@ func NewOpenAIResponsesAPIHandler(apiHandlers *handlers.BaseAPIHandler) *OpenAIR // HandlerType returns the identifier for this handler implementation. func (h *OpenAIResponsesAPIHandler) HandlerType() string { - return OPENAI_RESPONSE + return OpenaiResponse } // Models returns the OpenAIResponses-compatible model metadata supported by this handler. @@ -161,6 +161,7 @@ func (h *OpenAIResponsesAPIHandler) forwardResponsesStream(c *gin.Context, flush return case chunk, ok := <-data: if !ok { + _, _ = c.Writer.Write([]byte("\n")) flusher.Flush() cancel(nil) return diff --git a/internal/api/server.go b/internal/api/server.go index 77b2f5c5..8d6f1c61 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -439,38 +439,14 @@ func (s *Server) UpdateClients(cfg *config.Config) { s.mgmt.SetAuthManager(s.handlers.AuthManager) } - // Count types from AuthManager state + config - authFiles := 0 - glAPIKeyCount := 0 - claudeAPIKeyCount := 0 - codexAPIKeyCount := 0 + // Count client sources from configuration and auth directory + authFiles := util.CountAuthFiles(cfg.AuthDir) + glAPIKeyCount := len(cfg.GlAPIKey) + claudeAPIKeyCount := len(cfg.ClaudeKey) + codexAPIKeyCount := len(cfg.CodexKey) openAICompatCount := 0 - - if s.handlers != nil && s.handlers.AuthManager != nil { - for _, a := range s.handlers.AuthManager.List() { - if a == nil { - continue - } - if a.Attributes != nil { - if p := a.Attributes["path"]; p != "" { - authFiles++ - continue - } - } - switch strings.ToLower(a.Provider) { - case "gemini": - glAPIKeyCount++ - case "claude": - claudeAPIKeyCount++ - case "codex": - codexAPIKeyCount++ - } - } - } - if cfg != nil { - for i := range cfg.OpenAICompatibility { - openAICompatCount += len(cfg.OpenAICompatibility[i].APIKeys) - } + for i := range cfg.OpenAICompatibility { + openAICompatCount += len(cfg.OpenAICompatibility[i].APIKeys) } total := authFiles + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount diff --git a/internal/client/gemini-web/client.go b/internal/client/gemini-web/client.go index 9b6c8b5b..6005cb5d 100644 --- a/internal/client/gemini-web/client.go +++ b/internal/client/gemini-web/client.go @@ -96,7 +96,7 @@ func (c *GeminiClient) Init(timeoutSec float64, autoClose bool, closeDelaySec fl tr := &http.Transport{} if c.Proxy != "" { - if pu, err := url.Parse(c.Proxy); err == nil { + if pu, errParse := url.Parse(c.Proxy); errParse == nil { tr.Proxy = http.ProxyURL(pu) } } @@ -348,7 +348,9 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, if err != nil { return empty, &TimeoutError{GeminiError{Msg: "Generate content request timed out."}} } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode == 429 { // Surface 429 as TemporarilyBlocked to match Python behavior @@ -368,7 +370,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, return empty, &APIError{Msg: "Invalid response data received."} } var responseJSON []any - if err := json.Unmarshal([]byte(parts[2]), &responseJSON); err != nil { + if err = json.Unmarshal([]byte(parts[2]), &responseJSON); err != nil { c.Close(0) return empty, &APIError{Msg: "Invalid response data received."} } @@ -388,7 +390,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, continue } var mainPart []any - if err := json.Unmarshal([]byte(s), &mainPart); err != nil { + if err = json.Unmarshal([]byte(s), &mainPart); err != nil { continue } if len(mainPart) > 4 && mainPart[4] != nil { @@ -406,7 +408,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, continue } var top []any - if err := json.Unmarshal([]byte(line), &top); err != nil { + if err = json.Unmarshal([]byte(line), &top); err != nil { continue } lastTop = top @@ -420,7 +422,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, continue } var mainPart []any - if err := json.Unmarshal([]byte(s), &mainPart); err != nil { + if err = json.Unmarshal([]byte(s), &mainPart); err != nil { continue } if len(mainPart) > 4 && mainPart[4] != nil { @@ -465,7 +467,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, if len(bodyArr) > 1 { if metaArr, ok := bodyArr[1].([]any); ok { for _, v := range metaArr { - if s, ok := v.(string); ok { + if s, isOk := v.(string); isOk { metadata = append(metadata, s) } } @@ -482,22 +484,22 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, reGen := regexp.MustCompile(`http://googleusercontent\.com/image_generation_content/\d+`) for ci, candAny := range candContainer { - cArr, ok := candAny.([]any) - if !ok { + cArr, isOk := candAny.([]any) + if !isOk { continue } // text: cArr[1][0] var text string if len(cArr) > 1 { - if sArr, ok := cArr[1].([]any); ok && len(sArr) > 0 { + if sArr, isOk1 := cArr[1].([]any); isOk1 && len(sArr) > 0 { text, _ = sArr[0].(string) } } if reCard.MatchString(text) { // candidate[22] and candidate[22][0] or text if len(cArr) > 22 { - if arr, ok := cArr[22].([]any); ok && len(arr) > 0 { - if s, ok := arr[0].(string); ok { + if arr, isOk1 := cArr[22].([]any); isOk1 && len(arr) > 0 { + if s, isOk2 := arr[0].(string); isOk2 { text = s } } @@ -507,9 +509,9 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, // thoughts: candidate[37][0][0] var thoughts *string if len(cArr) > 37 { - if a, ok := cArr[37].([]any); ok && len(a) > 0 { - if b, ok := a[0].([]any); ok && len(b) > 0 { - if s, ok := b[0].(string); ok { + if a, ok1 := cArr[37].([]any); ok1 && len(a) > 0 { + if b1, ok2 := a[0].([]any); ok2 && len(b1) > 0 { + if s, ok3 := b1[0].(string); ok3 { ss := decodeHTML(s) thoughts = &ss } @@ -518,34 +520,34 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, } // web images: candidate[12][1] - webImages := []WebImage{} + var webImages []WebImage var imgSection any if len(cArr) > 12 { imgSection = cArr[12] } - if arr, ok := imgSection.([]any); ok && len(arr) > 1 { - if imagesArr, ok := arr[1].([]any); ok { + if arr, ok1 := imgSection.([]any); ok1 && len(arr) > 1 { + if imagesArr, ok2 := arr[1].([]any); ok2 { for _, wiAny := range imagesArr { - wiArr, ok := wiAny.([]any) - if !ok { + wiArr, ok3 := wiAny.([]any) + if !ok3 { continue } // url: wiArr[0][0][0], title: wiArr[7][0], alt: wiArr[0][4] var urlStr, title, alt string if len(wiArr) > 0 { - if a, ok := wiArr[0].([]any); ok && len(a) > 0 { - if b, ok := a[0].([]any); ok && len(b) > 0 { - urlStr, _ = b[0].(string) + if a, ok5 := wiArr[0].([]any); ok5 && len(a) > 0 { + if b1, ok6 := a[0].([]any); ok6 && len(b1) > 0 { + urlStr, _ = b1[0].(string) } if len(a) > 4 { - if s, ok := a[4].(string); ok { + if s, ok6 := a[4].(string); ok6 { alt = s } } } } if len(wiArr) > 7 { - if a, ok := wiArr[7].([]any); ok && len(a) > 0 { + if a, ok4 := wiArr[7].([]any); ok4 && len(a) > 0 { title, _ = a[0].(string) } } @@ -555,10 +557,10 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, } // generated images - genImages := []GeneratedImage{} + var genImages []GeneratedImage hasGen := false - if arr, ok := imgSection.([]any); ok && len(arr) > 7 { - if a, ok := arr[7].([]any); ok && len(a) > 0 && a[0] != nil { + if arr, ok1 := imgSection.([]any); ok1 && len(arr) > 7 { + if a, ok2 := arr[7].([]any); ok2 && len(a) > 0 && a[0] != nil { hasGen = true } } @@ -567,23 +569,23 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, var imgBody []any for pi := bodyIndex; pi < len(responseJSON); pi++ { part := responseJSON[pi] - arr, ok := part.([]any) - if !ok || len(arr) < 3 { + arr, ok1 := part.([]any) + if !ok1 || len(arr) < 3 { continue } - s, ok := arr[2].(string) - if !ok { + s, ok1 := arr[2].(string) + if !ok1 { continue } var mp []any - if err := json.Unmarshal([]byte(s), &mp); err != nil { + if err = json.Unmarshal([]byte(s), &mp); err != nil { continue } if len(mp) > 4 { - if tt, ok := mp[4].([]any); ok && len(tt) > ci { - if sec, ok := tt[ci].([]any); ok && len(sec) > 12 { - if ss, ok := sec[12].([]any); ok && len(ss) > 7 { - if first, ok := ss[7].([]any); ok && len(first) > 0 && first[0] != nil { + if tt, ok2 := mp[4].([]any); ok2 && len(tt) > ci { + if sec, ok3 := tt[ci].([]any); ok3 && len(sec) > 12 { + if ss, ok4 := sec[12].([]any); ok4 && len(ss) > 7 { + if first, ok5 := ss[7].([]any); ok5 && len(first) > 0 && first[0] != nil { imgBody = mp break } @@ -597,34 +599,34 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, } imgCand := imgBody[4].([]any)[ci].([]any) if len(imgCand) > 1 { - if a, ok := imgCand[1].([]any); ok && len(a) > 0 { - if s, ok := a[0].(string); ok { + if a, ok1 := imgCand[1].([]any); ok1 && len(a) > 0 { + if s, ok2 := a[0].(string); ok2 { text = strings.TrimSpace(reGen.ReplaceAllString(s, "")) } } } // images list at imgCand[12][7][0] if len(imgCand) > 12 { - if s1, ok := imgCand[12].([]any); ok && len(s1) > 7 { - if s2, ok := s1[7].([]any); ok && len(s2) > 0 { - if s3, ok := s2[0].([]any); ok { + if s1, ok1 := imgCand[12].([]any); ok1 && len(s1) > 7 { + if s2, ok2 := s1[7].([]any); ok2 && len(s2) > 0 { + if s3, ok3 := s2[0].([]any); ok3 { for ii, giAny := range s3 { - ga, ok := giAny.([]any) - if !ok || len(ga) < 4 { + ga, ok4 := giAny.([]any) + if !ok4 || len(ga) < 4 { continue } // url: ga[0][3][3] var urlStr, title, alt string - if a, ok := ga[0].([]any); ok && len(a) > 3 { - if b, ok := a[3].([]any); ok && len(b) > 3 { - urlStr, _ = b[3].(string) + if a, ok5 := ga[0].([]any); ok5 && len(a) > 3 { + if b1, ok6 := a[3].([]any); ok6 && len(b1) > 3 { + urlStr, _ = b1[3].(string) } } // title from ga[3][6] if len(ga) > 3 { - if a, ok := ga[3].([]any); ok { + if a, ok5 := ga[3].([]any); ok5 { if len(a) > 6 { - if v, ok := a[6].(float64); ok && v != 0 { + if v, ok6 := a[6].(float64); ok6 && v != 0 { title = fmt.Sprintf("[Generated Image %.0f]", v) } else { title = "[Generated Image]" @@ -634,13 +636,13 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, } // alt from ga[3][5][ii] fallback if len(a) > 5 { - if tt, ok := a[5].([]any); ok { + if tt, ok6 := a[5].([]any); ok6 { if ii < len(tt) { - if s, ok := tt[ii].(string); ok { + if s, ok7 := tt[ii].(string); ok7 { alt = s } } else if len(tt) > 0 { - if s, ok := tt[0].(string); ok { + if s, ok7 := tt[0].(string); ok7 { alt = s } } @@ -709,14 +711,6 @@ func extractErrorCode(top []any) (int, bool) { return int(f), true } -// truncateForLog returns a shortened string for logging -func truncateForLog(s string, n int) string { - if n <= 0 || len(s) <= n { - return s - } - return s[:n] -} - // StartChat returns a ChatSession attached to the client func (c *GeminiClient) StartChat(model Model, gem *Gem, metadata []string) *ChatSession { return &ChatSession{client: c, metadata: normalizeMeta(metadata), model: model, gem: gem, requestedModel: strings.ToLower(model.Name)} diff --git a/internal/client/gemini-web/media.go b/internal/client/gemini-web/media.go index c566bd42..58651453 100644 --- a/internal/client/gemini-web/media.go +++ b/internal/client/gemini-web/media.go @@ -122,7 +122,7 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("Error downloading image: %d %s", resp.StatusCode, resp.Status) + return "", fmt.Errorf("error downloading image: %d %s", resp.StatusCode, resp.Status) } if ct := resp.Header.Get("Content-Type"); ct != "" && !strings.Contains(strings.ToLower(ct), "image") { Warning("Content type of %s is not image, but %s.", filename, ct) diff --git a/internal/client/gemini-web/persistence.go b/internal/client/gemini-web/persistence.go index 52a5f0be..59e14ddf 100644 --- a/internal/client/gemini-web/persistence.go +++ b/internal/client/gemini-web/persistence.go @@ -101,7 +101,9 @@ func LoadConvStore(path string) (map[string][]string, error) { if err != nil { return nil, err } - defer db.Close() + defer func() { + _ = db.Close() + }() out := map[string][]string{} err = db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("account_meta")) @@ -138,24 +140,26 @@ func SaveConvStore(path string, data map[string][]string) error { if err != nil { return err } - defer db.Close() + defer func() { + _ = db.Close() + }() return db.Update(func(tx *bolt.Tx) error { // Recreate bucket to reflect the given snapshot exactly. if b := tx.Bucket([]byte("account_meta")); b != nil { - if err := tx.DeleteBucket([]byte("account_meta")); err != nil { + if err = tx.DeleteBucket([]byte("account_meta")); err != nil { return err } } - b, err := tx.CreateBucket([]byte("account_meta")) - if err != nil { - return err + b, errCreateBucket := tx.CreateBucket([]byte("account_meta")) + if errCreateBucket != nil { + return errCreateBucket } for k, v := range data { enc, e := json.Marshal(v) if e != nil { return e } - if e := b.Put([]byte(k), enc); e != nil { + if e = b.Put([]byte(k), enc); e != nil { return e } } @@ -177,7 +181,9 @@ func LoadConvData(path string) (map[string]ConversationRecord, map[string]string if err != nil { return nil, nil, err } - defer db.Close() + defer func() { + _ = db.Close() + }() items := map[string]ConversationRecord{} index := map[string]string{} err = db.View(func(tx *bolt.Tx) error { @@ -229,37 +235,39 @@ func SaveConvData(path string, items map[string]ConversationRecord, index map[st if err != nil { return err } - defer db.Close() + defer func() { + _ = db.Close() + }() return db.Update(func(tx *bolt.Tx) error { // Recreate items bucket if b := tx.Bucket([]byte("conv_items")); b != nil { - if err := tx.DeleteBucket([]byte("conv_items")); err != nil { + if err = tx.DeleteBucket([]byte("conv_items")); err != nil { return err } } - bi, err := tx.CreateBucket([]byte("conv_items")) - if err != nil { - return err + bi, errCreateBucket := tx.CreateBucket([]byte("conv_items")) + if errCreateBucket != nil { + return errCreateBucket } for k, rec := range items { enc, e := json.Marshal(rec) if e != nil { return e } - if e := bi.Put([]byte(k), enc); e != nil { + if e = bi.Put([]byte(k), enc); e != nil { return e } } // Recreate index bucket if b := tx.Bucket([]byte("conv_index")); b != nil { - if err := tx.DeleteBucket([]byte("conv_index")); err != nil { + if err = tx.DeleteBucket([]byte("conv_index")); err != nil { return err } } - bx, err := tx.CreateBucket([]byte("conv_index")) - if err != nil { - return err + bx, errCreateBucket := tx.CreateBucket([]byte("conv_index")) + if errCreateBucket != nil { + return errCreateBucket } for k, v := range index { if e := bx.Put([]byte(k), []byte(v)); e != nil { diff --git a/internal/client/gemini-web/request.go b/internal/client/gemini-web/request.go index 9142b7e2..2e9a4830 100644 --- a/internal/client/gemini-web/request.go +++ b/internal/client/gemini-web/request.go @@ -79,10 +79,6 @@ func SendWithSplit(chat *ChatSession, text string, files []string, cfg *config.C useHint = false chunkSize = maxChars } - if chunkSize <= 0 { - // As a last resort, split by single rune to avoid exceeding the limit - chunkSize = 1 - } // Split into rune-safe chunks chunks := ChunkByRunes(text, chunkSize) diff --git a/internal/constant/constant.go b/internal/constant/constant.go index bfa7558d..5330af52 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -1,11 +1,11 @@ package constant const ( - GEMINI = "gemini" - GEMINICLI = "gemini-cli" - GEMINIWEB = "gemini-web" - CODEX = "codex" - CLAUDE = "claude" - OPENAI = "openai" - OPENAI_RESPONSE = "openai-response" + Gemini = "gemini" + GeminiCLI = "gemini-cli" + GeminiWeb = "gemini-web" + Codex = "codex" + Claude = "claude" + OpenAI = "openai" + OpenaiResponse = "openai-response" ) diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 066f4e06..07bc64f2 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" @@ -18,9 +19,11 @@ import ( // ClaudeExecutor is a stateless executor for Anthropic Claude over the messages API. // If api_key is unavailable on auth, it falls back to legacy via ClientAdapter. -type ClaudeExecutor struct{} +type ClaudeExecutor struct { + cfg *config.Config +} -func NewClaudeExecutor() *ClaudeExecutor { return &ClaudeExecutor{} } +func NewClaudeExecutor(cfg *config.Config) *ClaudeExecutor { return &ClaudeExecutor{cfg: cfg} } func (e *ClaudeExecutor) Identifier() string { return "claude" } @@ -43,6 +46,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r } url := fmt.Sprintf("%s/v1/messages?beta=true", baseURL) + recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err @@ -62,12 +66,14 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) + appendAPIResponseChunk(ctx, e.cfg, b) return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} } data, err := io.ReadAll(resp.Body) if err != nil { return cliproxyexecutor.Response{}, err } + appendAPIResponseChunk(ctx, e.cfg, data) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m) return cliproxyexecutor.Response{Payload: []byte(out)}, nil @@ -87,6 +93,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A body, _ = sjson.SetRawBytes(body, "system", []byte(misc.ClaudeCodeInstructions)) url := fmt.Sprintf("%s/v1/messages?beta=true", baseURL) + recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err @@ -107,6 +114,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) + appendAPIResponseChunk(ctx, e.cfg, b) return nil, statusErr{code: resp.StatusCode, msg: string(b)} } out := make(chan cliproxyexecutor.StreamChunk) @@ -119,6 +127,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A var param any for scanner.Scan() { line := scanner.Bytes() + appendAPIResponseChunk(ctx, e.cfg, line) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m) for i := range chunks { out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index e9f68502..f41bb704 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "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" @@ -17,9 +18,11 @@ import ( // CodexExecutor is a stateless executor for Codex (OpenAI Responses API entrypoint). // If api_key is unavailable on auth, it falls back to legacy via ClientAdapter. -type CodexExecutor struct{} +type CodexExecutor struct { + cfg *config.Config +} -func NewCodexExecutor() *CodexExecutor { return &CodexExecutor{} } +func NewCodexExecutor(cfg *config.Config) *CodexExecutor { return &CodexExecutor{cfg: cfg} } func (e *CodexExecutor) Identifier() string { return "codex" } @@ -65,6 +68,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re } url := strings.TrimSuffix(baseURL, "/") + "/responses" + recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err @@ -83,12 +87,14 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) + appendAPIResponseChunk(ctx, e.cfg, b) return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} } data, err := io.ReadAll(resp.Body) if err != nil { return cliproxyexecutor.Response{}, err } + appendAPIResponseChunk(ctx, e.cfg, data) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m) return cliproxyexecutor.Response{Payload: []byte(out)}, nil @@ -134,6 +140,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } url := strings.TrimSuffix(baseURL, "/") + "/responses" + recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err @@ -153,6 +160,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) + appendAPIResponseChunk(ctx, e.cfg, b) return nil, statusErr{code: resp.StatusCode, msg: string(b)} } out := make(chan cliproxyexecutor.StreamChunk) @@ -165,6 +173,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au var param any for scanner.Scan() { line := scanner.Bytes() + appendAPIResponseChunk(ctx, e.cfg, line) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m) for i := range chunks { out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index 68615aad..1d87c20f 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 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" @@ -33,9 +34,13 @@ var geminiOauthScopes = []string{ } // GeminiCLIExecutor talks to the Cloud Code Assist endpoint using OAuth credentials from auth metadata. -type GeminiCLIExecutor struct{} +type GeminiCLIExecutor struct { + cfg *config.Config +} -func NewGeminiCLIExecutor() *GeminiCLIExecutor { return &GeminiCLIExecutor{} } +func NewGeminiCLIExecutor(cfg *config.Config) *GeminiCLIExecutor { + return &GeminiCLIExecutor{cfg: cfg} +} func (e *GeminiCLIExecutor) Identifier() string { return "gemini-cli" } @@ -91,6 +96,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth url = url + fmt.Sprintf("?$alt=%s", opts.Alt) } + recordAPIRequest(ctx, e.cfg, payload) reqHTTP, errReq := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) if errReq != nil { return cliproxyexecutor.Response{}, errReq @@ -105,6 +111,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth } data, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() + appendAPIResponseChunk(ctx, e.cfg, data) if resp.StatusCode >= 200 && resp.StatusCode < 300 { var param any out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, bytes.Clone(opts.OriginalRequest), payload, data, ¶m) @@ -117,6 +124,9 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth } } + if len(lastBody) > 0 { + appendAPIResponseChunk(ctx, e.cfg, lastBody) + } return cliproxyexecutor.Response{}, statusErr{code: lastStatus, msg: string(lastBody)} } @@ -162,6 +172,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut url = url + fmt.Sprintf("?$alt=%s", opts.Alt) } + recordAPIRequest(ctx, e.cfg, payload) reqHTTP, errReq := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) if errReq != nil { return nil, errReq @@ -177,6 +188,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut if resp.StatusCode < 200 || resp.StatusCode >= 300 { data, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() + appendAPIResponseChunk(ctx, e.cfg, data) lastStatus = resp.StatusCode lastBody = data if resp.StatusCode == 429 { @@ -196,6 +208,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut var param any for scanner.Scan() { line := scanner.Bytes() + appendAPIResponseChunk(ctx, e.cfg, line) if bytes.HasPrefix(line, dataTag) { segments := sdktranslator.TranslateStream(respCtx, to, from, attempt, bytes.Clone(opts.OriginalRequest), reqBody, bytes.Clone(line), ¶m) for i := range segments { @@ -219,6 +232,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut out <- cliproxyexecutor.StreamChunk{Err: errRead} return } + appendAPIResponseChunk(ctx, e.cfg, data) var param any segments := sdktranslator.TranslateStream(respCtx, to, from, attempt, bytes.Clone(opts.OriginalRequest), reqBody, data, ¶m) for i := range segments { @@ -325,7 +339,7 @@ func updateGeminiCLITokenMetadata(auth *cliproxyauth.Auth, base map[string]any, } if raw, err := json.Marshal(tok); err == nil { var tokenMap map[string]any - if err := json.Unmarshal(raw, &tokenMap); err == nil { + if err = json.Unmarshal(raw, &tokenMap); err == nil { for k, v := range tokenMap { merged[k] = v } diff --git a/internal/runtime/executor/gemini_executor.go b/internal/runtime/executor/gemini_executor.go index 7764c3ba..f9e86b21 100644 --- a/internal/runtime/executor/gemini_executor.go +++ b/internal/runtime/executor/gemini_executor.go @@ -8,6 +8,7 @@ import ( "io" "net/http" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 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" @@ -20,9 +21,11 @@ const ( // GeminiExecutor is a stateless executor for the official Gemini API using API keys. // If no API key is found on the auth entry, it falls back to the legacy client via ClientAdapter. -type GeminiExecutor struct{} +type GeminiExecutor struct { + cfg *config.Config +} -func NewGeminiExecutor() *GeminiExecutor { return &GeminiExecutor{} } +func NewGeminiExecutor(cfg *config.Config) *GeminiExecutor { return &GeminiExecutor{cfg: cfg} } func (e *GeminiExecutor) Identifier() string { return "gemini" } @@ -51,6 +54,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r url = url + fmt.Sprintf("?$alt=%s", opts.Alt) } + recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err @@ -73,12 +77,14 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) + appendAPIResponseChunk(ctx, e.cfg, b) return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} } data, err := io.ReadAll(resp.Body) if err != nil { return cliproxyexecutor.Response{}, err } + appendAPIResponseChunk(ctx, e.cfg, data) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m) return cliproxyexecutor.Response{Payload: []byte(out)}, nil @@ -101,6 +107,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A } else { url = url + fmt.Sprintf("?$alt=%s", opts.Alt) } + recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err @@ -123,6 +130,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) + appendAPIResponseChunk(ctx, e.cfg, b) return nil, statusErr{code: resp.StatusCode, msg: string(b)} } out := make(chan cliproxyexecutor.StreamChunk) @@ -135,6 +143,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A var param any for scanner.Scan() { line := scanner.Bytes() + appendAPIResponseChunk(ctx, e.cfg, line) lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m) for i := range lines { out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])} diff --git a/internal/runtime/executor/gemini_web_state.go b/internal/runtime/executor/gemini_web_state.go index 28668abb..379970e5 100644 --- a/internal/runtime/executor/gemini_web_state.go +++ b/internal/runtime/executor/gemini_web_state.go @@ -10,7 +10,6 @@ import ( "sync" "time" - "github.com/gin-gonic/gin" "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" @@ -192,8 +191,8 @@ func (s *geminiWebState) onCookiesRefreshed() { func (s *geminiWebState) tokenSnapshot() *gemini.GeminiWebTokenStorage { s.tokenMu.Lock() defer s.tokenMu.Unlock() - copy := *s.token - return © + c := *s.token + return &c } func (s *geminiWebState) ShouldRefresh(now time.Time, _ *cliproxyauth.Auth) bool { @@ -225,13 +224,9 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON res.translatedRaw = bytes.Clone(rawJSON) if handler, ok := ctx.Value("handler").(interfaces.APIHandler); ok && handler != nil { res.handlerType = handler.HandlerType() - res.translatedRaw = translator.Request(res.handlerType, constant.GEMINIWEB, modelName, res.translatedRaw, stream) - } - if s.cfg != nil && s.cfg.RequestLog { - if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil { - ginCtx.Set("API_REQUEST", res.translatedRaw) - } + res.translatedRaw = translator.Request(res.handlerType, constant.GeminiWeb, modelName, res.translatedRaw, stream) } + recordAPIRequest(ctx, s.cfg, res.translatedRaw) messages, files, mimes, msgFileIdx, err := geminiwebapi.ParseMessagesAndFiles(res.translatedRaw) if err != nil { @@ -336,7 +331,7 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON } res.uploaded = uploaded - if err := s.ensureClient(); err != nil { + if err = s.ensureClient(); err != nil { return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: err} } chat := s.client.StartChat(model, s.getConfiguredGem(), meta) @@ -443,36 +438,19 @@ func (s *geminiWebState) persistConversation(modelName string, prep *geminiWebPr } func (s *geminiWebState) addAPIResponseData(ctx context.Context, line []byte) { - if s.cfg == nil || !s.cfg.RequestLog { - return - } - data := bytes.TrimSpace(bytes.Clone(line)) - 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) - } + appendAPIResponseChunk(ctx, s.cfg, line) } func (s *geminiWebState) convertToTarget(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []byte { if prep == nil || prep.handlerType == "" { return gemBytes } - if !translator.NeedConvert(prep.handlerType, constant.GEMINIWEB) { + if !translator.NeedConvert(prep.handlerType, constant.GeminiWeb) { return gemBytes } var param any - out := translator.ResponseNonStream(prep.handlerType, constant.GEMINIWEB, ctx, modelName, prep.originalRaw, prep.translatedRaw, gemBytes, ¶m) - if prep.handlerType == constant.OPENAI && out != "" { + out := translator.ResponseNonStream(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, gemBytes, ¶m) + if prep.handlerType == constant.OpenAI && out != "" { newID := fmt.Sprintf("chatcmpl-%x", time.Now().UnixNano()) if v := gjson.Parse(out).Get("id"); v.Exists() { out, _ = sjson.Set(out, "id", newID) @@ -485,22 +463,22 @@ func (s *geminiWebState) convertStream(ctx context.Context, modelName string, pr if prep == nil || prep.handlerType == "" { return []string{string(gemBytes)} } - if !translator.NeedConvert(prep.handlerType, constant.GEMINIWEB) { + if !translator.NeedConvert(prep.handlerType, constant.GeminiWeb) { return []string{string(gemBytes)} } var param any - 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 { if prep == nil || prep.handlerType == "" { return nil } - if !translator.NeedConvert(prep.handlerType, constant.GEMINIWEB) { + if !translator.NeedConvert(prep.handlerType, constant.GeminiWeb) { return nil } var param any - 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 { diff --git a/internal/runtime/executor/openai_compat_executor.go b/internal/runtime/executor/openai_compat_executor.go index 03dd5513..0da61eae 100644 --- a/internal/runtime/executor/openai_compat_executor.go +++ b/internal/runtime/executor/openai_compat_executor.go @@ -5,12 +5,14 @@ import ( "bytes" "context" "fmt" - 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" "io" "net/http" "strings" + + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + 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" ) // OpenAICompatExecutor implements a stateless executor for OpenAI-compatible providers. @@ -18,11 +20,12 @@ import ( // using per-auth credentials (API key) and per-auth HTTP transport (proxy) from context. type OpenAICompatExecutor struct { provider string + cfg *config.Config } // NewOpenAICompatExecutor creates an executor bound to a provider key (e.g., "openrouter"). -func NewOpenAICompatExecutor(provider string) *OpenAICompatExecutor { - return &OpenAICompatExecutor{provider: provider} +func NewOpenAICompatExecutor(provider string, cfg *config.Config) *OpenAICompatExecutor { + return &OpenAICompatExecutor{provider: provider, cfg: cfg} } // Identifier implements cliproxyauth.ProviderExecutor. @@ -45,6 +48,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), opts.Stream) url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" + recordAPIRequest(ctx, e.cfg, translated) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) if err != nil { return cliproxyexecutor.Response{}, err @@ -64,12 +68,14 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) + appendAPIResponseChunk(ctx, e.cfg, b) return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} } body, err := io.ReadAll(resp.Body) if err != nil { return cliproxyexecutor.Response{}, err } + appendAPIResponseChunk(ctx, e.cfg, body) // Translate response back to source format when needed var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, body, ¶m) @@ -86,6 +92,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" + recordAPIRequest(ctx, e.cfg, translated) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) if err != nil { return nil, err @@ -107,6 +114,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) + appendAPIResponseChunk(ctx, e.cfg, b) return nil, statusErr{code: resp.StatusCode, msg: string(b)} } out := make(chan cliproxyexecutor.StreamChunk) @@ -119,6 +127,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy var param any for scanner.Scan() { line := scanner.Bytes() + appendAPIResponseChunk(ctx, e.cfg, line) if len(line) == 0 { continue } @@ -129,7 +138,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} } } - if err := scanner.Err(); err != nil { + if err = scanner.Err(); err != nil { out <- cliproxyexecutor.StreamChunk{Err: err} } }() diff --git a/internal/runtime/executor/qwen_executor.go b/internal/runtime/executor/qwen_executor.go index abe60665..d9d0a8e7 100644 --- a/internal/runtime/executor/qwen_executor.go +++ b/internal/runtime/executor/qwen_executor.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" 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" @@ -18,9 +19,11 @@ import ( // QwenExecutor is a stateless executor for Qwen Code using OpenAI-compatible chat completions. // If access token is unavailable, it falls back to legacy via ClientAdapter. -type QwenExecutor struct{} +type QwenExecutor struct { + cfg *config.Config +} -func NewQwenExecutor() *QwenExecutor { return &QwenExecutor{} } +func NewQwenExecutor(cfg *config.Config) *QwenExecutor { return &QwenExecutor{cfg: cfg} } func (e *QwenExecutor) Identifier() string { return "qwen" } @@ -40,6 +43,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" + recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err @@ -58,12 +62,14 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) + appendAPIResponseChunk(ctx, e.cfg, b) return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} } data, err := io.ReadAll(resp.Body) if err != nil { return cliproxyexecutor.Response{}, err } + appendAPIResponseChunk(ctx, e.cfg, data) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m) return cliproxyexecutor.Response{Payload: []byte(out)}, nil @@ -90,6 +96,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut } url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" + recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err @@ -109,6 +116,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) + appendAPIResponseChunk(ctx, e.cfg, b) return nil, statusErr{code: resp.StatusCode, msg: string(b)} } out := make(chan cliproxyexecutor.StreamChunk) @@ -121,6 +129,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut var param any for scanner.Scan() { line := scanner.Bytes() + appendAPIResponseChunk(ctx, e.cfg, line) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m) for i := range chunks { out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} diff --git a/internal/translator/claude/gemini-cli/init.go b/internal/translator/claude/gemini-cli/init.go index 95d88229..8a7b822c 100644 --- a/internal/translator/claude/gemini-cli/init.go +++ b/internal/translator/claude/gemini-cli/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - GEMINICLI, - CLAUDE, + GeminiCLI, + Claude, ConvertGeminiCLIRequestToClaude, interfaces.TranslateResponse{ Stream: ConvertClaudeResponseToGeminiCLI, diff --git a/internal/translator/claude/gemini/init.go b/internal/translator/claude/gemini/init.go index 705ea7de..00d75ac9 100644 --- a/internal/translator/claude/gemini/init.go +++ b/internal/translator/claude/gemini/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - GEMINI, - CLAUDE, + Gemini, + Claude, ConvertGeminiRequestToClaude, interfaces.TranslateResponse{ Stream: ConvertClaudeResponseToGemini, diff --git a/internal/translator/claude/openai/chat-completions/init.go b/internal/translator/claude/openai/chat-completions/init.go index d1b417a9..a18840ba 100644 --- a/internal/translator/claude/openai/chat-completions/init.go +++ b/internal/translator/claude/openai/chat-completions/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - OPENAI, - CLAUDE, + OpenAI, + Claude, ConvertOpenAIRequestToClaude, interfaces.TranslateResponse{ Stream: ConvertClaudeResponseToOpenAI, diff --git a/internal/translator/claude/openai/responses/init.go b/internal/translator/claude/openai/responses/init.go index e636b2a4..595fecc6 100644 --- a/internal/translator/claude/openai/responses/init.go +++ b/internal/translator/claude/openai/responses/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - OPENAI_RESPONSE, - CLAUDE, + OpenaiResponse, + Claude, ConvertOpenAIResponsesRequestToClaude, interfaces.TranslateResponse{ Stream: ConvertClaudeResponseToOpenAIResponses, diff --git a/internal/translator/codex/claude/init.go b/internal/translator/codex/claude/init.go index 65b2c3ee..82ff78ad 100644 --- a/internal/translator/codex/claude/init.go +++ b/internal/translator/codex/claude/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - CLAUDE, - CODEX, + Claude, + Codex, ConvertClaudeRequestToCodex, interfaces.TranslateResponse{ Stream: ConvertCodexResponseToClaude, diff --git a/internal/translator/codex/gemini-cli/init.go b/internal/translator/codex/gemini-cli/init.go index 77c3c705..ac470655 100644 --- a/internal/translator/codex/gemini-cli/init.go +++ b/internal/translator/codex/gemini-cli/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - GEMINICLI, - CODEX, + GeminiCLI, + Codex, ConvertGeminiCLIRequestToCodex, interfaces.TranslateResponse{ Stream: ConvertCodexResponseToGeminiCLI, diff --git a/internal/translator/codex/gemini/init.go b/internal/translator/codex/gemini/init.go index 180345a4..96f68a98 100644 --- a/internal/translator/codex/gemini/init.go +++ b/internal/translator/codex/gemini/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - GEMINI, - CODEX, + Gemini, + Codex, ConvertGeminiRequestToCodex, interfaces.TranslateResponse{ Stream: ConvertCodexResponseToGemini, diff --git a/internal/translator/codex/openai/chat-completions/init.go b/internal/translator/codex/openai/chat-completions/init.go index 24f840bd..8f782fda 100644 --- a/internal/translator/codex/openai/chat-completions/init.go +++ b/internal/translator/codex/openai/chat-completions/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - OPENAI, - CODEX, + OpenAI, + Codex, ConvertOpenAIRequestToCodex, interfaces.TranslateResponse{ Stream: ConvertCodexResponseToOpenAI, diff --git a/internal/translator/codex/openai/responses/init.go b/internal/translator/codex/openai/responses/init.go index 2b5e343a..cab759f2 100644 --- a/internal/translator/codex/openai/responses/init.go +++ b/internal/translator/codex/openai/responses/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - OPENAI_RESPONSE, - CODEX, + OpenaiResponse, + Codex, ConvertOpenAIResponsesRequestToCodex, interfaces.TranslateResponse{ Stream: ConvertCodexResponseToOpenAIResponses, diff --git a/internal/translator/gemini-cli/claude/init.go b/internal/translator/gemini-cli/claude/init.go index 7b7c9587..7899d710 100644 --- a/internal/translator/gemini-cli/claude/init.go +++ b/internal/translator/gemini-cli/claude/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - CLAUDE, - GEMINICLI, + Claude, + GeminiCLI, ConvertClaudeRequestToCLI, interfaces.TranslateResponse{ Stream: ConvertGeminiCLIResponseToClaude, diff --git a/internal/translator/gemini-cli/gemini/init.go b/internal/translator/gemini-cli/gemini/init.go index 9d371882..2a372ea6 100644 --- a/internal/translator/gemini-cli/gemini/init.go +++ b/internal/translator/gemini-cli/gemini/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - GEMINI, - GEMINICLI, + Gemini, + GeminiCLI, ConvertGeminiRequestToGeminiCLI, interfaces.TranslateResponse{ Stream: ConvertGeminiCliRequestToGemini, diff --git a/internal/translator/gemini-cli/openai/chat-completions/init.go b/internal/translator/gemini-cli/openai/chat-completions/init.go index b1d11f71..3bd76c51 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/init.go +++ b/internal/translator/gemini-cli/openai/chat-completions/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - OPENAI, - GEMINICLI, + OpenAI, + GeminiCLI, ConvertOpenAIRequestToGeminiCLI, interfaces.TranslateResponse{ Stream: ConvertCliResponseToOpenAI, diff --git a/internal/translator/gemini-cli/openai/responses/init.go b/internal/translator/gemini-cli/openai/responses/init.go index e6d7c1d9..b25d6708 100644 --- a/internal/translator/gemini-cli/openai/responses/init.go +++ b/internal/translator/gemini-cli/openai/responses/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - OPENAI_RESPONSE, - GEMINICLI, + OpenaiResponse, + GeminiCLI, ConvertOpenAIResponsesRequestToGeminiCLI, interfaces.TranslateResponse{ Stream: ConvertGeminiCLIResponseToOpenAIResponses, diff --git a/internal/translator/gemini-web/openai/chat-completions/init.go b/internal/translator/gemini-web/openai/chat-completions/init.go index d6da3693..7e8dc53e 100644 --- a/internal/translator/gemini-web/openai/chat-completions/init.go +++ b/internal/translator/gemini-web/openai/chat-completions/init.go @@ -9,8 +9,8 @@ import ( func init() { translator.Register( - OPENAI, - GEMINIWEB, + OpenAI, + GeminiWeb, geminiChat.ConvertOpenAIRequestToGemini, interfaces.TranslateResponse{ Stream: geminiChat.ConvertGeminiResponseToOpenAI, diff --git a/internal/translator/gemini-web/openai/responses/init.go b/internal/translator/gemini-web/openai/responses/init.go index c6f9600a..84cdec72 100644 --- a/internal/translator/gemini-web/openai/responses/init.go +++ b/internal/translator/gemini-web/openai/responses/init.go @@ -9,8 +9,8 @@ import ( func init() { translator.Register( - OPENAI_RESPONSE, - GEMINIWEB, + OpenaiResponse, + GeminiWeb, geminiResponses.ConvertOpenAIResponsesRequestToGemini, interfaces.TranslateResponse{ Stream: geminiResponses.ConvertGeminiResponseToOpenAIResponses, diff --git a/internal/translator/gemini/claude/init.go b/internal/translator/gemini/claude/init.go index 09ea45c7..89b663b9 100644 --- a/internal/translator/gemini/claude/init.go +++ b/internal/translator/gemini/claude/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - CLAUDE, - GEMINI, + Claude, + Gemini, ConvertClaudeRequestToGemini, interfaces.TranslateResponse{ Stream: ConvertGeminiResponseToClaude, diff --git a/internal/translator/gemini/gemini-cli/init.go b/internal/translator/gemini/gemini-cli/init.go index b0ee9b50..d30713cc 100644 --- a/internal/translator/gemini/gemini-cli/init.go +++ b/internal/translator/gemini/gemini-cli/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - GEMINICLI, - GEMINI, + GeminiCLI, + Gemini, ConvertGeminiCLIRequestToGemini, interfaces.TranslateResponse{ Stream: ConvertGeminiResponseToGeminiCLI, diff --git a/internal/translator/gemini/gemini/init.go b/internal/translator/gemini/gemini/init.go index abdb22c3..6a95fef3 100644 --- a/internal/translator/gemini/gemini/init.go +++ b/internal/translator/gemini/gemini/init.go @@ -10,8 +10,8 @@ import ( // The request converter ensures missing or invalid roles are normalized to valid values. func init() { translator.Register( - GEMINI, - GEMINI, + Gemini, + Gemini, ConvertGeminiRequestToGemini, interfaces.TranslateResponse{ Stream: PassthroughGeminiResponseStream, diff --git a/internal/translator/gemini/openai/chat-completions/init.go b/internal/translator/gemini/openai/chat-completions/init.go index 10afb54a..800e07db 100644 --- a/internal/translator/gemini/openai/chat-completions/init.go +++ b/internal/translator/gemini/openai/chat-completions/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - OPENAI, - GEMINI, + OpenAI, + Gemini, ConvertOpenAIRequestToGemini, interfaces.TranslateResponse{ Stream: ConvertGeminiResponseToOpenAI, diff --git a/internal/translator/gemini/openai/responses/init.go b/internal/translator/gemini/openai/responses/init.go index 2db565a9..b53cac3d 100644 --- a/internal/translator/gemini/openai/responses/init.go +++ b/internal/translator/gemini/openai/responses/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - OPENAI_RESPONSE, - GEMINI, + OpenaiResponse, + Gemini, ConvertOpenAIResponsesRequestToGemini, interfaces.TranslateResponse{ Stream: ConvertGeminiResponseToOpenAIResponses, diff --git a/internal/translator/openai/claude/init.go b/internal/translator/openai/claude/init.go index 1b694c34..e72227f1 100644 --- a/internal/translator/openai/claude/init.go +++ b/internal/translator/openai/claude/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - CLAUDE, - OPENAI, + Claude, + OpenAI, ConvertClaudeRequestToOpenAI, interfaces.TranslateResponse{ Stream: ConvertOpenAIResponseToClaude, diff --git a/internal/translator/openai/gemini-cli/init.go b/internal/translator/openai/gemini-cli/init.go index b30aa3cd..24262c36 100644 --- a/internal/translator/openai/gemini-cli/init.go +++ b/internal/translator/openai/gemini-cli/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - GEMINICLI, - OPENAI, + GeminiCLI, + OpenAI, ConvertGeminiCLIRequestToOpenAI, interfaces.TranslateResponse{ Stream: ConvertOpenAIResponseToGeminiCLI, diff --git a/internal/translator/openai/gemini/init.go b/internal/translator/openai/gemini/init.go index a8ae2df5..04c0704a 100644 --- a/internal/translator/openai/gemini/init.go +++ b/internal/translator/openai/gemini/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - GEMINI, - OPENAI, + Gemini, + OpenAI, ConvertGeminiRequestToOpenAI, interfaces.TranslateResponse{ Stream: ConvertOpenAIResponseToGemini, diff --git a/internal/translator/openai/openai/chat-completions/init.go b/internal/translator/openai/openai/chat-completions/init.go index 067fab14..90fa3dcd 100644 --- a/internal/translator/openai/openai/chat-completions/init.go +++ b/internal/translator/openai/openai/chat-completions/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - OPENAI, - OPENAI, + OpenAI, + OpenAI, ConvertOpenAIRequestToOpenAI, interfaces.TranslateResponse{ Stream: ConvertOpenAIResponseToOpenAI, diff --git a/internal/translator/openai/openai/responses/init.go b/internal/translator/openai/openai/responses/init.go index 40c3f693..e6f60e0e 100644 --- a/internal/translator/openai/openai/responses/init.go +++ b/internal/translator/openai/openai/responses/init.go @@ -8,8 +8,8 @@ import ( func init() { translator.Register( - OPENAI_RESPONSE, - OPENAI, + OpenaiResponse, + OpenAI, ConvertOpenAIResponsesRequestToOpenAIChatCompletions, interfaces.TranslateResponse{ Stream: ConvertOpenAIChatCompletionsResponseToOpenAIResponses, diff --git a/internal/util/translator.go b/internal/util/translator.go index 6744eab0..329f9e94 100644 --- a/internal/util/translator.go +++ b/internal/util/translator.go @@ -289,9 +289,6 @@ func sanitizeTypeFields(jsonStr string) string { break } else if typeStr == "number" || typeStr == "integer" { preferredType = typeStr - if preferredType == "" { - preferredType = typeStr - } } else if preferredType == "" { preferredType = typeStr } @@ -323,6 +320,8 @@ func walkForTypeFields(value gjson.Result, path string, paths *[]string) { walkForTypeFields(val, childPath, paths) return true }) + default: + } } @@ -367,5 +366,7 @@ func findNestedSchemaPaths(value gjson.Result, path string, fieldsToFind []strin findNestedSchemaPaths(val, childPath, fieldsToFind, paths) return true }) + default: + } } diff --git a/internal/util/util.go b/internal/util/util.go index 909b21ae..78a3d0b0 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,6 +1,11 @@ package util import ( + "io/fs" + "os" + "path/filepath" + "strings" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" log "github.com/sirupsen/logrus" ) @@ -21,3 +26,38 @@ func SetLogLevel(cfg *config.Config) { log.Infof("log level changed from %s to %s (debug=%t)", currentLevel, newLevel, cfg.Debug) } } + +// CountAuthFiles returns the number of JSON auth files located under the provided directory. +// The function resolves leading tildes to the user's home directory and performs a case-insensitive +// match on the ".json" suffix so that files saved with uppercase extensions are also counted. +func CountAuthFiles(authDir string) int { + if authDir == "" { + return 0 + } + if strings.HasPrefix(authDir, "~") { + home, err := os.UserHomeDir() + if err != nil { + log.Debugf("countAuthFiles: failed to resolve home directory: %v", err) + return 0 + } + authDir = filepath.Join(home, authDir[1:]) + } + count := 0 + walkErr := filepath.WalkDir(authDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + log.Debugf("countAuthFiles: error accessing %s: %v", path, err) + return nil + } + if d.IsDir() { + return nil + } + if strings.HasSuffix(strings.ToLower(d.Name()), ".json") { + count++ + } + return nil + }) + if walkErr != nil { + log.Debugf("countAuthFiles: walk error: %v", walkErr) + } + return count +} diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index 1d651c67..a0fa4128 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -174,21 +174,19 @@ func (w *Watcher) handleEvent(event fsnotify.Event) { } // Handle auth directory changes incrementally (.json only) - if isAuthJSON { - log.Infof("auth file changed (%s): %s, processing incrementally", event.Op.String(), filepath.Base(event.Name)) - if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write { + log.Infof("auth file changed (%s): %s, processing incrementally", event.Op.String(), filepath.Base(event.Name)) + if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write { + w.addOrUpdateClient(event.Name) + } else if event.Op&fsnotify.Remove == fsnotify.Remove { + // Atomic replace on some platforms may surface as Remove+Create for the target path. + // Wait briefly; if the file exists again, treat as update instead of removal. + time.Sleep(replaceCheckDelay) + if _, statErr := os.Stat(event.Name); statErr == nil { + // File exists after a short delay; handle as an update. w.addOrUpdateClient(event.Name) - } else if event.Op&fsnotify.Remove == fsnotify.Remove { - // Atomic replace on some platforms may surface as Remove+Create for the target path. - // Wait briefly; if the file exists again, treat as update instead of removal. - time.Sleep(replaceCheckDelay) - if _, statErr := os.Stat(event.Name); statErr == nil { - // File exists after a short delay; handle as an update. - w.addOrUpdateClient(event.Name) - return - } - w.removeClient(event.Name) + return } + w.removeClient(event.Name) } } @@ -301,7 +299,7 @@ func (w *Watcher) reloadClients() { log.Debugf("created %d new API key clients", 0) // Load file-based clients - successfulAuthCount := w.loadFileClients(cfg) + authFileCount := w.loadFileClients(cfg) log.Debugf("loaded %d new file-based clients", 0) // no legacy file-based clients to unregister @@ -317,7 +315,7 @@ func (w *Watcher) reloadClients() { return nil } if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") { - if data, err := util.ReadAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay); err == nil && len(data) > 0 { + if data, errReadAuthFileWithRetry := util.ReadAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay); errReadAuthFileWithRetry == nil && len(data) > 0 { sum := sha256.Sum256(data) w.lastAuthHashes[path] = hex.EncodeToString(sum[:]) } @@ -326,12 +324,12 @@ func (w *Watcher) reloadClients() { }) w.clientsMutex.Unlock() - totalNewClients := successfulAuthCount + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount + totalNewClients := authFileCount + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount log.Infof("full client reload complete - old: %d clients, new: %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)", 0, totalNewClients, - successfulAuthCount, + authFileCount, glAPIKeyCount, claudeAPIKeyCount, codexAPIKeyCount, @@ -572,7 +570,7 @@ func (w *Watcher) loadFileClients(cfg *config.Config) int { log.Debugf("error accessing path %s: %v", path, err) return err } - if !info.IsDir() && strings.HasSuffix(info.Name(), ".json") { + if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") { authFileCount++ misc.LogCredentialSeparator() log.Debugf("processing auth file %d: %s", authFileCount, filepath.Base(path)) @@ -587,8 +585,8 @@ func (w *Watcher) loadFileClients(cfg *config.Config) int { if errWalk != nil { log.Errorf("error walking auth directory: %v", errWalk) } - log.Debugf("auth directory scan complete - found %d .json files, %d successful authentications", authFileCount, successfulAuthCount) - return successfulAuthCount + log.Debugf("auth directory scan complete - found %d .json files, %d readable", authFileCount, successfulAuthCount) + return authFileCount } func BuildAPIKeyClients(cfg *config.Config) (int, int, int, int) { diff --git a/sdk/cliproxy/auth/filestore.go b/sdk/cliproxy/auth/filestore.go index 194c209a..ac303aec 100644 --- a/sdk/cliproxy/auth/filestore.go +++ b/sdk/cliproxy/auth/filestore.go @@ -73,7 +73,7 @@ func (s *FileStore) Save(ctx context.Context, auth *Auth) error { if err != nil { return fmt.Errorf("auth filestore: marshal metadata failed: %w", err) } - if existing, err := os.ReadFile(path); err == nil { + if existing, errReadFile := os.ReadFile(path); errReadFile == nil { if jsonEqual(existing, raw) { return nil } @@ -108,8 +108,8 @@ func deepEqualJSON(a, b any) bool { return false } for key, subA := range valA { - subB, ok := valB[key] - if !ok || !deepEqualJSON(subA, subB) { + subB, ok1 := valB[key] + if !ok1 || !deepEqualJSON(subA, subB) { return false } } diff --git a/sdk/cliproxy/auth/manager.go b/sdk/cliproxy/auth/manager.go index 05f857ba..8548e3fc 100644 --- a/sdk/cliproxy/auth/manager.go +++ b/sdk/cliproxy/auth/manager.go @@ -795,7 +795,7 @@ func authLastRefreshTimestamp(a *Auth) (time.Time, bool) { func lookupMetadataTime(meta map[string]any, keys ...string) (time.Time, bool) { for _, key := range keys { if val, ok := meta[key]; ok { - if ts, ok := parseTimeValue(val); ok { + if ts, ok1 := parseTimeValue(val); ok1 { return ts, true } } diff --git a/sdk/cliproxy/auth/types.go b/sdk/cliproxy/auth/types.go index 017c6802..912bc36f 100644 --- a/sdk/cliproxy/auth/types.go +++ b/sdk/cliproxy/auth/types.go @@ -84,6 +84,24 @@ func (a *Auth) AccountInfo() (bool, string) { if a == nil { return false, "" } + if strings.ToLower(a.Provider) == "gemini-web" { + if a.Metadata != nil { + if v, ok := a.Metadata["secure_1psid"].(string); ok && v != "" { + return true, v + } + if v, ok := a.Metadata["__Secure-1PSID"].(string); ok && v != "" { + return true, v + } + } + if a.Attributes != nil { + if v := a.Attributes["secure_1psid"]; v != "" { + return true, v + } + if v := a.Attributes["api_key"]; v != "" { + return true, v + } + } + } if a.Metadata != nil { if v, ok := a.Metadata["email"].(string); ok { return false, v @@ -125,7 +143,7 @@ func expirationFromMap(meta map[string]any) (time.Time, bool) { } for _, key := range expireKeys { if v, ok := meta[key]; ok { - if ts, ok := parseTimeValue(v); ok { + if ts, ok1 := parseTimeValue(v); ok1 { return ts, true } } @@ -134,7 +152,7 @@ func expirationFromMap(meta map[string]any) (time.Time, bool) { if nested, ok := meta[nestedKey]; ok { switch val := nested.(type) { case map[string]any: - if ts, ok := expirationFromMap(val); ok { + if ts, ok1 := expirationFromMap(val); ok1 { return ts, true } case map[string]string: @@ -142,7 +160,7 @@ func expirationFromMap(meta map[string]any) (time.Time, bool) { for k, v := range val { temp[k] = v } - if ts, ok := expirationFromMap(temp); ok { + if ts, ok1 := expirationFromMap(temp); ok1 { return ts, true } } diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index 79ce3ffb..c42c4c39 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -15,6 +15,7 @@ import ( "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/runtime/executor" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" @@ -172,10 +173,11 @@ func (s *Service) Run(ctx context.Context) error { log.Infof("core auth auto-refresh started (interval=%s)", interval) } - totalNewClients := tokenResult.SuccessfulAuthed + apiKeyResult.GeminiKeyCount + apiKeyResult.ClaudeKeyCount + apiKeyResult.CodexKeyCount + apiKeyResult.OpenAICompatCount + authFileCount := util.CountAuthFiles(s.cfg.AuthDir) + totalNewClients := authFileCount + apiKeyResult.GeminiKeyCount + apiKeyResult.ClaudeKeyCount + apiKeyResult.CodexKeyCount + apiKeyResult.OpenAICompatCount log.Infof("full client load complete - %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)", totalNewClients, - tokenResult.SuccessfulAuthed, + authFileCount, apiKeyResult.GeminiKeyCount, apiKeyResult.ClaudeKeyCount, apiKeyResult.CodexKeyCount, @@ -292,19 +294,19 @@ func (s *Service) syncCoreAuthFromAuths(ctx context.Context, auths []*coreauth.A // Ensure executors registered per provider: prefer stateless where available. switch strings.ToLower(a.Provider) { case "gemini": - s.coreManager.RegisterExecutor(executor.NewGeminiExecutor()) + s.coreManager.RegisterExecutor(executor.NewGeminiExecutor(s.cfg)) case "gemini-cli": - s.coreManager.RegisterExecutor(executor.NewGeminiCLIExecutor()) + s.coreManager.RegisterExecutor(executor.NewGeminiCLIExecutor(s.cfg)) case "gemini-web": s.coreManager.RegisterExecutor(executor.NewGeminiWebExecutor(s.cfg)) case "claude": - s.coreManager.RegisterExecutor(executor.NewClaudeExecutor()) + s.coreManager.RegisterExecutor(executor.NewClaudeExecutor(s.cfg)) case "codex": - s.coreManager.RegisterExecutor(executor.NewCodexExecutor()) + s.coreManager.RegisterExecutor(executor.NewCodexExecutor(s.cfg)) case "qwen": - s.coreManager.RegisterExecutor(executor.NewQwenExecutor()) + s.coreManager.RegisterExecutor(executor.NewQwenExecutor(s.cfg)) default: - s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor("openai-compatibility")) + s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor("openai-compatibility", s.cfg)) } // Preserve existing temporal fields @@ -316,9 +318,9 @@ func (s *Service) syncCoreAuthFromAuths(ctx context.Context, auths []*coreauth.A // Ensure model registry reflects core auth identity s.registerModelsForAuth(a) if _, ok := s.coreManager.GetByID(a.ID); ok { - s.coreManager.Update(ctx, a) + _, _ = s.coreManager.Update(ctx, a) } else { - s.coreManager.Register(ctx, a) + _, _ = s.coreManager.Register(ctx, a) } } // Disable removed auths @@ -333,7 +335,7 @@ func (s *Service) syncCoreAuthFromAuths(ctx context.Context, auths []*coreauth.A stored.Status = coreauth.StatusDisabled // Unregister from model registry when disabled GlobalModelRegistry().UnregisterClient(stored.ID) - s.coreManager.Update(ctx, stored) + _, _ = s.coreManager.Update(ctx, stored) } }