refactor: standardize constant naming and improve file-based auth handling

- Renamed constants from uppercase to CamelCase for consistency.
- Replaced redundant file-based auth handling logic with the new `util.CountAuthFiles` helper.
- Fixed various error-handling inconsistencies and enhanced robustness in file operations.
- Streamlined auth client reload logic in server and watcher components.
- Applied minor code readability improvements across multiple packages.
This commit is contained in:
Luis Pater
2025-09-22 02:56:45 +08:00
parent 4999fce7f4
commit d9ad65622a
51 changed files with 341 additions and 270 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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

View File

@@ -439,39 +439,15 @@ 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)
}
}
total := authFiles + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
log.Infof("server clients and configuration updated: %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)",

View File

@@ -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)}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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"
)

View File

@@ -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, &param)
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), &param)
for i := range chunks {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}

View File

@@ -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, &param)
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), &param)
for i := range chunks {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}

View File

@@ -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, &param)
@@ -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), &param)
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, &param)
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
}

View File

@@ -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, &param)
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), &param)
for i := range lines {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}

View File

@@ -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 &copy
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, &param)
if prep.handlerType == constant.OPENAI && out != "" {
out := translator.ResponseNonStream(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, gemBytes, &param)
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, &param)
return translator.Response(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, gemBytes, &param)
}
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]"), &param)
return translator.Response(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, []byte("[DONE]"), &param)
}
func (s *geminiWebState) useReusableContext() bool {

View File

@@ -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, &param)
@@ -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}
}
}()

View File

@@ -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, &param)
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), &param)
for i := range chunks {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
GEMINICLI,
CLAUDE,
GeminiCLI,
Claude,
ConvertGeminiCLIRequestToClaude,
interfaces.TranslateResponse{
Stream: ConvertClaudeResponseToGeminiCLI,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
GEMINI,
CLAUDE,
Gemini,
Claude,
ConvertGeminiRequestToClaude,
interfaces.TranslateResponse{
Stream: ConvertClaudeResponseToGemini,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
OPENAI,
CLAUDE,
OpenAI,
Claude,
ConvertOpenAIRequestToClaude,
interfaces.TranslateResponse{
Stream: ConvertClaudeResponseToOpenAI,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
OPENAI_RESPONSE,
CLAUDE,
OpenaiResponse,
Claude,
ConvertOpenAIResponsesRequestToClaude,
interfaces.TranslateResponse{
Stream: ConvertClaudeResponseToOpenAIResponses,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
CLAUDE,
CODEX,
Claude,
Codex,
ConvertClaudeRequestToCodex,
interfaces.TranslateResponse{
Stream: ConvertCodexResponseToClaude,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
GEMINICLI,
CODEX,
GeminiCLI,
Codex,
ConvertGeminiCLIRequestToCodex,
interfaces.TranslateResponse{
Stream: ConvertCodexResponseToGeminiCLI,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
GEMINI,
CODEX,
Gemini,
Codex,
ConvertGeminiRequestToCodex,
interfaces.TranslateResponse{
Stream: ConvertCodexResponseToGemini,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
OPENAI,
CODEX,
OpenAI,
Codex,
ConvertOpenAIRequestToCodex,
interfaces.TranslateResponse{
Stream: ConvertCodexResponseToOpenAI,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
OPENAI_RESPONSE,
CODEX,
OpenaiResponse,
Codex,
ConvertOpenAIResponsesRequestToCodex,
interfaces.TranslateResponse{
Stream: ConvertCodexResponseToOpenAIResponses,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
CLAUDE,
GEMINICLI,
Claude,
GeminiCLI,
ConvertClaudeRequestToCLI,
interfaces.TranslateResponse{
Stream: ConvertGeminiCLIResponseToClaude,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
GEMINI,
GEMINICLI,
Gemini,
GeminiCLI,
ConvertGeminiRequestToGeminiCLI,
interfaces.TranslateResponse{
Stream: ConvertGeminiCliRequestToGemini,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
OPENAI,
GEMINICLI,
OpenAI,
GeminiCLI,
ConvertOpenAIRequestToGeminiCLI,
interfaces.TranslateResponse{
Stream: ConvertCliResponseToOpenAI,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
OPENAI_RESPONSE,
GEMINICLI,
OpenaiResponse,
GeminiCLI,
ConvertOpenAIResponsesRequestToGeminiCLI,
interfaces.TranslateResponse{
Stream: ConvertGeminiCLIResponseToOpenAIResponses,

View File

@@ -9,8 +9,8 @@ import (
func init() {
translator.Register(
OPENAI,
GEMINIWEB,
OpenAI,
GeminiWeb,
geminiChat.ConvertOpenAIRequestToGemini,
interfaces.TranslateResponse{
Stream: geminiChat.ConvertGeminiResponseToOpenAI,

View File

@@ -9,8 +9,8 @@ import (
func init() {
translator.Register(
OPENAI_RESPONSE,
GEMINIWEB,
OpenaiResponse,
GeminiWeb,
geminiResponses.ConvertOpenAIResponsesRequestToGemini,
interfaces.TranslateResponse{
Stream: geminiResponses.ConvertGeminiResponseToOpenAIResponses,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
CLAUDE,
GEMINI,
Claude,
Gemini,
ConvertClaudeRequestToGemini,
interfaces.TranslateResponse{
Stream: ConvertGeminiResponseToClaude,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
GEMINICLI,
GEMINI,
GeminiCLI,
Gemini,
ConvertGeminiCLIRequestToGemini,
interfaces.TranslateResponse{
Stream: ConvertGeminiResponseToGeminiCLI,

View File

@@ -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,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
OPENAI,
GEMINI,
OpenAI,
Gemini,
ConvertOpenAIRequestToGemini,
interfaces.TranslateResponse{
Stream: ConvertGeminiResponseToOpenAI,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
OPENAI_RESPONSE,
GEMINI,
OpenaiResponse,
Gemini,
ConvertOpenAIResponsesRequestToGemini,
interfaces.TranslateResponse{
Stream: ConvertGeminiResponseToOpenAIResponses,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
CLAUDE,
OPENAI,
Claude,
OpenAI,
ConvertClaudeRequestToOpenAI,
interfaces.TranslateResponse{
Stream: ConvertOpenAIResponseToClaude,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
GEMINICLI,
OPENAI,
GeminiCLI,
OpenAI,
ConvertGeminiCLIRequestToOpenAI,
interfaces.TranslateResponse{
Stream: ConvertOpenAIResponseToGeminiCLI,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
GEMINI,
OPENAI,
Gemini,
OpenAI,
ConvertGeminiRequestToOpenAI,
interfaces.TranslateResponse{
Stream: ConvertOpenAIResponseToGemini,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
OPENAI,
OPENAI,
OpenAI,
OpenAI,
ConvertOpenAIRequestToOpenAI,
interfaces.TranslateResponse{
Stream: ConvertOpenAIResponseToOpenAI,

View File

@@ -8,8 +8,8 @@ import (
func init() {
translator.Register(
OPENAI_RESPONSE,
OPENAI,
OpenaiResponse,
OpenAI,
ConvertOpenAIResponsesRequestToOpenAIChatCompletions,
interfaces.TranslateResponse{
Stream: ConvertOpenAIChatCompletionsResponseToOpenAIResponses,

View File

@@ -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:
}
}

View File

@@ -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
}

View File

@@ -174,7 +174,6 @@ 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 {
w.addOrUpdateClient(event.Name)
@@ -189,7 +188,6 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
}
w.removeClient(event.Name)
}
}
}
// reloadConfig reloads the configuration and triggers a full reload
@@ -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) {

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}