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. // HandlerType returns the identifier for this handler implementation.
func (h *ClaudeCodeAPIHandler) HandlerType() string { func (h *ClaudeCodeAPIHandler) HandlerType() string {
return CLAUDE return Claude
} }
// Models returns a list of models supported by this handler. // 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. // HandlerType returns the type of this handler.
func (h *GeminiCLIAPIHandler) HandlerType() string { func (h *GeminiCLIAPIHandler) HandlerType() string {
return GEMINICLI return GeminiCLI
} }
// Models returns a list of models supported by this handler. // 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. // HandlerType returns the identifier for this handler implementation.
func (h *GeminiAPIHandler) HandlerType() string { func (h *GeminiAPIHandler) HandlerType() string {
return GEMINI return Gemini
} }
// Models returns the Gemini-compatible model metadata supported by this handler. // 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)}) c.JSON(500, gin.H{"error": fmt.Sprintf("failed to write file: %v", errWrite)})
return 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()}) c.JSON(500, gin.H{"error": err.Error()})
return return
} }

View File

@@ -44,7 +44,7 @@ func NewOpenAIAPIHandler(apiHandlers *handlers.BaseAPIHandler) *OpenAIAPIHandler
// HandlerType returns the identifier for this handler implementation. // HandlerType returns the identifier for this handler implementation.
func (h *OpenAIAPIHandler) HandlerType() string { func (h *OpenAIAPIHandler) HandlerType() string {
return OPENAI return OpenAI
} }
// Models returns the OpenAI-compatible model metadata supported by this handler. // 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. // HandlerType returns the identifier for this handler implementation.
func (h *OpenAIResponsesAPIHandler) HandlerType() string { func (h *OpenAIResponsesAPIHandler) HandlerType() string {
return OPENAI_RESPONSE return OpenaiResponse
} }
// Models returns the OpenAIResponses-compatible model metadata supported by this handler. // Models returns the OpenAIResponses-compatible model metadata supported by this handler.
@@ -161,6 +161,7 @@ func (h *OpenAIResponsesAPIHandler) forwardResponsesStream(c *gin.Context, flush
return return
case chunk, ok := <-data: case chunk, ok := <-data:
if !ok { if !ok {
_, _ = c.Writer.Write([]byte("\n"))
flusher.Flush() flusher.Flush()
cancel(nil) cancel(nil)
return return

View File

@@ -439,38 +439,14 @@ func (s *Server) UpdateClients(cfg *config.Config) {
s.mgmt.SetAuthManager(s.handlers.AuthManager) s.mgmt.SetAuthManager(s.handlers.AuthManager)
} }
// Count types from AuthManager state + config // Count client sources from configuration and auth directory
authFiles := 0 authFiles := util.CountAuthFiles(cfg.AuthDir)
glAPIKeyCount := 0 glAPIKeyCount := len(cfg.GlAPIKey)
claudeAPIKeyCount := 0 claudeAPIKeyCount := len(cfg.ClaudeKey)
codexAPIKeyCount := 0 codexAPIKeyCount := len(cfg.CodexKey)
openAICompatCount := 0 openAICompatCount := 0
for i := range cfg.OpenAICompatibility {
if s.handlers != nil && s.handlers.AuthManager != nil { openAICompatCount += len(cfg.OpenAICompatibility[i].APIKeys)
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 total := authFiles + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount

View File

@@ -96,7 +96,7 @@ func (c *GeminiClient) Init(timeoutSec float64, autoClose bool, closeDelaySec fl
tr := &http.Transport{} tr := &http.Transport{}
if c.Proxy != "" { 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) tr.Proxy = http.ProxyURL(pu)
} }
} }
@@ -348,7 +348,9 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
if err != nil { if err != nil {
return empty, &TimeoutError{GeminiError{Msg: "Generate content request timed out."}} return empty, &TimeoutError{GeminiError{Msg: "Generate content request timed out."}}
} }
defer resp.Body.Close() defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode == 429 { if resp.StatusCode == 429 {
// Surface 429 as TemporarilyBlocked to match Python behavior // 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."} return empty, &APIError{Msg: "Invalid response data received."}
} }
var responseJSON []any 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) c.Close(0)
return empty, &APIError{Msg: "Invalid response data received."} return empty, &APIError{Msg: "Invalid response data received."}
} }
@@ -388,7 +390,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
continue continue
} }
var mainPart []any var mainPart []any
if err := json.Unmarshal([]byte(s), &mainPart); err != nil { if err = json.Unmarshal([]byte(s), &mainPart); err != nil {
continue continue
} }
if len(mainPart) > 4 && mainPart[4] != nil { if len(mainPart) > 4 && mainPart[4] != nil {
@@ -406,7 +408,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
continue continue
} }
var top []any var top []any
if err := json.Unmarshal([]byte(line), &top); err != nil { if err = json.Unmarshal([]byte(line), &top); err != nil {
continue continue
} }
lastTop = top lastTop = top
@@ -420,7 +422,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
continue continue
} }
var mainPart []any var mainPart []any
if err := json.Unmarshal([]byte(s), &mainPart); err != nil { if err = json.Unmarshal([]byte(s), &mainPart); err != nil {
continue continue
} }
if len(mainPart) > 4 && mainPart[4] != nil { 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 len(bodyArr) > 1 {
if metaArr, ok := bodyArr[1].([]any); ok { if metaArr, ok := bodyArr[1].([]any); ok {
for _, v := range metaArr { for _, v := range metaArr {
if s, ok := v.(string); ok { if s, isOk := v.(string); isOk {
metadata = append(metadata, s) 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+`) reGen := regexp.MustCompile(`http://googleusercontent\.com/image_generation_content/\d+`)
for ci, candAny := range candContainer { for ci, candAny := range candContainer {
cArr, ok := candAny.([]any) cArr, isOk := candAny.([]any)
if !ok { if !isOk {
continue continue
} }
// text: cArr[1][0] // text: cArr[1][0]
var text string var text string
if len(cArr) > 1 { 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) text, _ = sArr[0].(string)
} }
} }
if reCard.MatchString(text) { if reCard.MatchString(text) {
// candidate[22] and candidate[22][0] or text // candidate[22] and candidate[22][0] or text
if len(cArr) > 22 { if len(cArr) > 22 {
if arr, ok := cArr[22].([]any); ok && len(arr) > 0 { if arr, isOk1 := cArr[22].([]any); isOk1 && len(arr) > 0 {
if s, ok := arr[0].(string); ok { if s, isOk2 := arr[0].(string); isOk2 {
text = s text = s
} }
} }
@@ -507,9 +509,9 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
// thoughts: candidate[37][0][0] // thoughts: candidate[37][0][0]
var thoughts *string var thoughts *string
if len(cArr) > 37 { if len(cArr) > 37 {
if a, ok := cArr[37].([]any); ok && len(a) > 0 { if a, ok1 := cArr[37].([]any); ok1 && len(a) > 0 {
if b, ok := a[0].([]any); ok && len(b) > 0 { if b1, ok2 := a[0].([]any); ok2 && len(b1) > 0 {
if s, ok := b[0].(string); ok { if s, ok3 := b1[0].(string); ok3 {
ss := decodeHTML(s) ss := decodeHTML(s)
thoughts = &ss thoughts = &ss
} }
@@ -518,34 +520,34 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
} }
// web images: candidate[12][1] // web images: candidate[12][1]
webImages := []WebImage{} var webImages []WebImage
var imgSection any var imgSection any
if len(cArr) > 12 { if len(cArr) > 12 {
imgSection = cArr[12] imgSection = cArr[12]
} }
if arr, ok := imgSection.([]any); ok && len(arr) > 1 { if arr, ok1 := imgSection.([]any); ok1 && len(arr) > 1 {
if imagesArr, ok := arr[1].([]any); ok { if imagesArr, ok2 := arr[1].([]any); ok2 {
for _, wiAny := range imagesArr { for _, wiAny := range imagesArr {
wiArr, ok := wiAny.([]any) wiArr, ok3 := wiAny.([]any)
if !ok { if !ok3 {
continue continue
} }
// url: wiArr[0][0][0], title: wiArr[7][0], alt: wiArr[0][4] // url: wiArr[0][0][0], title: wiArr[7][0], alt: wiArr[0][4]
var urlStr, title, alt string var urlStr, title, alt string
if len(wiArr) > 0 { if len(wiArr) > 0 {
if a, ok := wiArr[0].([]any); ok && len(a) > 0 { if a, ok5 := wiArr[0].([]any); ok5 && len(a) > 0 {
if b, ok := a[0].([]any); ok && len(b) > 0 { if b1, ok6 := a[0].([]any); ok6 && len(b1) > 0 {
urlStr, _ = b[0].(string) urlStr, _ = b1[0].(string)
} }
if len(a) > 4 { if len(a) > 4 {
if s, ok := a[4].(string); ok { if s, ok6 := a[4].(string); ok6 {
alt = s alt = s
} }
} }
} }
} }
if len(wiArr) > 7 { 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) title, _ = a[0].(string)
} }
} }
@@ -555,10 +557,10 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
} }
// generated images // generated images
genImages := []GeneratedImage{} var genImages []GeneratedImage
hasGen := false hasGen := false
if arr, ok := imgSection.([]any); ok && len(arr) > 7 { if arr, ok1 := imgSection.([]any); ok1 && len(arr) > 7 {
if a, ok := arr[7].([]any); ok && len(a) > 0 && a[0] != nil { if a, ok2 := arr[7].([]any); ok2 && len(a) > 0 && a[0] != nil {
hasGen = true hasGen = true
} }
} }
@@ -567,23 +569,23 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
var imgBody []any var imgBody []any
for pi := bodyIndex; pi < len(responseJSON); pi++ { for pi := bodyIndex; pi < len(responseJSON); pi++ {
part := responseJSON[pi] part := responseJSON[pi]
arr, ok := part.([]any) arr, ok1 := part.([]any)
if !ok || len(arr) < 3 { if !ok1 || len(arr) < 3 {
continue continue
} }
s, ok := arr[2].(string) s, ok1 := arr[2].(string)
if !ok { if !ok1 {
continue continue
} }
var mp []any var mp []any
if err := json.Unmarshal([]byte(s), &mp); err != nil { if err = json.Unmarshal([]byte(s), &mp); err != nil {
continue continue
} }
if len(mp) > 4 { if len(mp) > 4 {
if tt, ok := mp[4].([]any); ok && len(tt) > ci { if tt, ok2 := mp[4].([]any); ok2 && len(tt) > ci {
if sec, ok := tt[ci].([]any); ok && len(sec) > 12 { if sec, ok3 := tt[ci].([]any); ok3 && len(sec) > 12 {
if ss, ok := sec[12].([]any); ok && len(ss) > 7 { if ss, ok4 := sec[12].([]any); ok4 && len(ss) > 7 {
if first, ok := ss[7].([]any); ok && len(first) > 0 && first[0] != nil { if first, ok5 := ss[7].([]any); ok5 && len(first) > 0 && first[0] != nil {
imgBody = mp imgBody = mp
break break
} }
@@ -597,34 +599,34 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
} }
imgCand := imgBody[4].([]any)[ci].([]any) imgCand := imgBody[4].([]any)[ci].([]any)
if len(imgCand) > 1 { if len(imgCand) > 1 {
if a, ok := imgCand[1].([]any); ok && len(a) > 0 { if a, ok1 := imgCand[1].([]any); ok1 && len(a) > 0 {
if s, ok := a[0].(string); ok { if s, ok2 := a[0].(string); ok2 {
text = strings.TrimSpace(reGen.ReplaceAllString(s, "")) text = strings.TrimSpace(reGen.ReplaceAllString(s, ""))
} }
} }
} }
// images list at imgCand[12][7][0] // images list at imgCand[12][7][0]
if len(imgCand) > 12 { if len(imgCand) > 12 {
if s1, ok := imgCand[12].([]any); ok && len(s1) > 7 { if s1, ok1 := imgCand[12].([]any); ok1 && len(s1) > 7 {
if s2, ok := s1[7].([]any); ok && len(s2) > 0 { if s2, ok2 := s1[7].([]any); ok2 && len(s2) > 0 {
if s3, ok := s2[0].([]any); ok { if s3, ok3 := s2[0].([]any); ok3 {
for ii, giAny := range s3 { for ii, giAny := range s3 {
ga, ok := giAny.([]any) ga, ok4 := giAny.([]any)
if !ok || len(ga) < 4 { if !ok4 || len(ga) < 4 {
continue continue
} }
// url: ga[0][3][3] // url: ga[0][3][3]
var urlStr, title, alt string var urlStr, title, alt string
if a, ok := ga[0].([]any); ok && len(a) > 3 { if a, ok5 := ga[0].([]any); ok5 && len(a) > 3 {
if b, ok := a[3].([]any); ok && len(b) > 3 { if b1, ok6 := a[3].([]any); ok6 && len(b1) > 3 {
urlStr, _ = b[3].(string) urlStr, _ = b1[3].(string)
} }
} }
// title from ga[3][6] // title from ga[3][6]
if len(ga) > 3 { if len(ga) > 3 {
if a, ok := ga[3].([]any); ok { if a, ok5 := ga[3].([]any); ok5 {
if len(a) > 6 { 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) title = fmt.Sprintf("[Generated Image %.0f]", v)
} else { } else {
title = "[Generated Image]" title = "[Generated Image]"
@@ -634,13 +636,13 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model,
} }
// alt from ga[3][5][ii] fallback // alt from ga[3][5][ii] fallback
if len(a) > 5 { if len(a) > 5 {
if tt, ok := a[5].([]any); ok { if tt, ok6 := a[5].([]any); ok6 {
if ii < len(tt) { if ii < len(tt) {
if s, ok := tt[ii].(string); ok { if s, ok7 := tt[ii].(string); ok7 {
alt = s alt = s
} }
} else if len(tt) > 0 { } else if len(tt) > 0 {
if s, ok := tt[0].(string); ok { if s, ok7 := tt[0].(string); ok7 {
alt = s alt = s
} }
} }
@@ -709,14 +711,6 @@ func extractErrorCode(top []any) (int, bool) {
return int(f), true 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 // StartChat returns a ChatSession attached to the client
func (c *GeminiClient) StartChat(model Model, gem *Gem, metadata []string) *ChatSession { 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)} 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() _ = resp.Body.Close()
}() }()
if resp.StatusCode != http.StatusOK { 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") { 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) 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 { if err != nil {
return nil, err return nil, err
} }
defer db.Close() defer func() {
_ = db.Close()
}()
out := map[string][]string{} out := map[string][]string{}
err = db.View(func(tx *bolt.Tx) error { err = db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("account_meta")) b := tx.Bucket([]byte("account_meta"))
@@ -138,24 +140,26 @@ func SaveConvStore(path string, data map[string][]string) error {
if err != nil { if err != nil {
return err return err
} }
defer db.Close() defer func() {
_ = db.Close()
}()
return db.Update(func(tx *bolt.Tx) error { return db.Update(func(tx *bolt.Tx) error {
// Recreate bucket to reflect the given snapshot exactly. // Recreate bucket to reflect the given snapshot exactly.
if b := tx.Bucket([]byte("account_meta")); b != nil { 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 return err
} }
} }
b, err := tx.CreateBucket([]byte("account_meta")) b, errCreateBucket := tx.CreateBucket([]byte("account_meta"))
if err != nil { if errCreateBucket != nil {
return err return errCreateBucket
} }
for k, v := range data { for k, v := range data {
enc, e := json.Marshal(v) enc, e := json.Marshal(v)
if e != nil { if e != nil {
return e return e
} }
if e := b.Put([]byte(k), enc); e != nil { if e = b.Put([]byte(k), enc); e != nil {
return e return e
} }
} }
@@ -177,7 +181,9 @@ func LoadConvData(path string) (map[string]ConversationRecord, map[string]string
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
defer db.Close() defer func() {
_ = db.Close()
}()
items := map[string]ConversationRecord{} items := map[string]ConversationRecord{}
index := map[string]string{} index := map[string]string{}
err = db.View(func(tx *bolt.Tx) error { 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 { if err != nil {
return err return err
} }
defer db.Close() defer func() {
_ = db.Close()
}()
return db.Update(func(tx *bolt.Tx) error { return db.Update(func(tx *bolt.Tx) error {
// Recreate items bucket // Recreate items bucket
if b := tx.Bucket([]byte("conv_items")); b != nil { 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 return err
} }
} }
bi, err := tx.CreateBucket([]byte("conv_items")) bi, errCreateBucket := tx.CreateBucket([]byte("conv_items"))
if err != nil { if errCreateBucket != nil {
return err return errCreateBucket
} }
for k, rec := range items { for k, rec := range items {
enc, e := json.Marshal(rec) enc, e := json.Marshal(rec)
if e != nil { if e != nil {
return e return e
} }
if e := bi.Put([]byte(k), enc); e != nil { if e = bi.Put([]byte(k), enc); e != nil {
return e return e
} }
} }
// Recreate index bucket // Recreate index bucket
if b := tx.Bucket([]byte("conv_index")); b != nil { 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 return err
} }
} }
bx, err := tx.CreateBucket([]byte("conv_index")) bx, errCreateBucket := tx.CreateBucket([]byte("conv_index"))
if err != nil { if errCreateBucket != nil {
return err return errCreateBucket
} }
for k, v := range index { for k, v := range index {
if e := bx.Put([]byte(k), []byte(v)); e != nil { 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 useHint = false
chunkSize = maxChars 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 // Split into rune-safe chunks
chunks := ChunkByRunes(text, chunkSize) chunks := ChunkByRunes(text, chunkSize)

View File

@@ -1,11 +1,11 @@
package constant package constant
const ( const (
GEMINI = "gemini" Gemini = "gemini"
GEMINICLI = "gemini-cli" GeminiCLI = "gemini-cli"
GEMINIWEB = "gemini-web" GeminiWeb = "gemini-web"
CODEX = "codex" Codex = "codex"
CLAUDE = "claude" Claude = "claude"
OPENAI = "openai" OpenAI = "openai"
OPENAI_RESPONSE = "openai-response" OpenaiResponse = "openai-response"
) )

View File

@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" 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. // 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. // 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" } 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) 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)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
@@ -62,12 +66,14 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
appendAPIResponseChunk(ctx, e.cfg, b)
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)}
} }
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
appendAPIResponseChunk(ctx, e.cfg, data)
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param) out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param)
return cliproxyexecutor.Response{Payload: []byte(out)}, nil 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)) body, _ = sjson.SetRawBytes(body, "system", []byte(misc.ClaudeCodeInstructions))
url := fmt.Sprintf("%s/v1/messages?beta=true", baseURL) 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)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -107,6 +114,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
appendAPIResponseChunk(ctx, e.cfg, b)
return nil, statusErr{code: resp.StatusCode, msg: string(b)} return nil, statusErr{code: resp.StatusCode, msg: string(b)}
} }
out := make(chan cliproxyexecutor.StreamChunk) out := make(chan cliproxyexecutor.StreamChunk)
@@ -119,6 +127,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() 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) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), &param)
for i := range chunks { for i := range chunks {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" 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). // 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. // 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" } 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" url := strings.TrimSuffix(baseURL, "/") + "/responses"
recordAPIRequest(ctx, e.cfg, body)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
@@ -83,12 +87,14 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
appendAPIResponseChunk(ctx, e.cfg, b)
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)}
} }
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
appendAPIResponseChunk(ctx, e.cfg, data)
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param) out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param)
return cliproxyexecutor.Response{Payload: []byte(out)}, nil 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" url := strings.TrimSuffix(baseURL, "/") + "/responses"
recordAPIRequest(ctx, e.cfg, body)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -153,6 +160,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
appendAPIResponseChunk(ctx, e.cfg, b)
return nil, statusErr{code: resp.StatusCode, msg: string(b)} return nil, statusErr{code: resp.StatusCode, msg: string(b)}
} }
out := make(chan cliproxyexecutor.StreamChunk) out := make(chan cliproxyexecutor.StreamChunk)
@@ -165,6 +173,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() 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) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), &param)
for i := range chunks { for i := range chunks {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}

View File

@@ -11,6 +11,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" 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. // 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" } 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) url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
} }
recordAPIRequest(ctx, e.cfg, payload)
reqHTTP, errReq := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) reqHTTP, errReq := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
if errReq != nil { if errReq != nil {
return cliproxyexecutor.Response{}, errReq return cliproxyexecutor.Response{}, errReq
@@ -105,6 +111,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
} }
data, _ := io.ReadAll(resp.Body) data, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close() _ = resp.Body.Close()
appendAPIResponseChunk(ctx, e.cfg, data)
if resp.StatusCode >= 200 && resp.StatusCode < 300 { if resp.StatusCode >= 200 && resp.StatusCode < 300 {
var param any var param any
out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, bytes.Clone(opts.OriginalRequest), payload, data, &param) 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)} 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) url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
} }
recordAPIRequest(ctx, e.cfg, payload)
reqHTTP, errReq := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) reqHTTP, errReq := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
if errReq != nil { if errReq != nil {
return nil, errReq return nil, errReq
@@ -177,6 +188,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
data, _ := io.ReadAll(resp.Body) data, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close() _ = resp.Body.Close()
appendAPIResponseChunk(ctx, e.cfg, data)
lastStatus = resp.StatusCode lastStatus = resp.StatusCode
lastBody = data lastBody = data
if resp.StatusCode == 429 { if resp.StatusCode == 429 {
@@ -196,6 +208,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line)
if bytes.HasPrefix(line, dataTag) { if bytes.HasPrefix(line, dataTag) {
segments := sdktranslator.TranslateStream(respCtx, to, from, attempt, bytes.Clone(opts.OriginalRequest), reqBody, bytes.Clone(line), &param) segments := sdktranslator.TranslateStream(respCtx, to, from, attempt, bytes.Clone(opts.OriginalRequest), reqBody, bytes.Clone(line), &param)
for i := range segments { for i := range segments {
@@ -219,6 +232,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
out <- cliproxyexecutor.StreamChunk{Err: errRead} out <- cliproxyexecutor.StreamChunk{Err: errRead}
return return
} }
appendAPIResponseChunk(ctx, e.cfg, data)
var param any var param any
segments := sdktranslator.TranslateStream(respCtx, to, from, attempt, bytes.Clone(opts.OriginalRequest), reqBody, data, &param) segments := sdktranslator.TranslateStream(respCtx, to, from, attempt, bytes.Clone(opts.OriginalRequest), reqBody, data, &param)
for i := range segments { 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 { if raw, err := json.Marshal(tok); err == nil {
var tokenMap map[string]any 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 { for k, v := range tokenMap {
merged[k] = v merged[k] = v
} }

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"net/http" "net/http"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" 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. // 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. // 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" } 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) url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
} }
recordAPIRequest(ctx, e.cfg, body)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
@@ -73,12 +77,14 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
appendAPIResponseChunk(ctx, e.cfg, b)
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)}
} }
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
appendAPIResponseChunk(ctx, e.cfg, data)
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param) out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param)
return cliproxyexecutor.Response{Payload: []byte(out)}, nil return cliproxyexecutor.Response{Payload: []byte(out)}, nil
@@ -101,6 +107,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
} else { } else {
url = url + fmt.Sprintf("?$alt=%s", opts.Alt) url = url + fmt.Sprintf("?$alt=%s", opts.Alt)
} }
recordAPIRequest(ctx, e.cfg, body)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -123,6 +130,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
appendAPIResponseChunk(ctx, e.cfg, b)
return nil, statusErr{code: resp.StatusCode, msg: string(b)} return nil, statusErr{code: resp.StatusCode, msg: string(b)}
} }
out := make(chan cliproxyexecutor.StreamChunk) out := make(chan cliproxyexecutor.StreamChunk)
@@ -135,6 +143,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() 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) lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), &param)
for i := range lines { for i := range lines {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])} out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}

View File

@@ -10,7 +10,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
geminiwebapi "github.com/router-for-me/CLIProxyAPI/v6/internal/client/gemini-web" geminiwebapi "github.com/router-for-me/CLIProxyAPI/v6/internal/client/gemini-web"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
@@ -192,8 +191,8 @@ func (s *geminiWebState) onCookiesRefreshed() {
func (s *geminiWebState) tokenSnapshot() *gemini.GeminiWebTokenStorage { func (s *geminiWebState) tokenSnapshot() *gemini.GeminiWebTokenStorage {
s.tokenMu.Lock() s.tokenMu.Lock()
defer s.tokenMu.Unlock() defer s.tokenMu.Unlock()
copy := *s.token c := *s.token
return &copy return &c
} }
func (s *geminiWebState) ShouldRefresh(now time.Time, _ *cliproxyauth.Auth) bool { 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) res.translatedRaw = bytes.Clone(rawJSON)
if handler, ok := ctx.Value("handler").(interfaces.APIHandler); ok && handler != nil { if handler, ok := ctx.Value("handler").(interfaces.APIHandler); ok && handler != nil {
res.handlerType = handler.HandlerType() res.handlerType = handler.HandlerType()
res.translatedRaw = translator.Request(res.handlerType, constant.GEMINIWEB, modelName, res.translatedRaw, stream) 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)
}
} }
recordAPIRequest(ctx, s.cfg, res.translatedRaw)
messages, files, mimes, msgFileIdx, err := geminiwebapi.ParseMessagesAndFiles(res.translatedRaw) messages, files, mimes, msgFileIdx, err := geminiwebapi.ParseMessagesAndFiles(res.translatedRaw)
if err != nil { if err != nil {
@@ -336,7 +331,7 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
} }
res.uploaded = uploaded res.uploaded = uploaded
if err := s.ensureClient(); err != nil { if err = s.ensureClient(); err != nil {
return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: err} return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: err}
} }
chat := s.client.StartChat(model, s.getConfiguredGem(), meta) 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) { func (s *geminiWebState) addAPIResponseData(ctx context.Context, line []byte) {
if s.cfg == nil || !s.cfg.RequestLog { appendAPIResponseChunk(ctx, s.cfg, line)
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)
}
} }
func (s *geminiWebState) convertToTarget(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []byte { func (s *geminiWebState) convertToTarget(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []byte {
if prep == nil || prep.handlerType == "" { if prep == nil || prep.handlerType == "" {
return gemBytes return gemBytes
} }
if !translator.NeedConvert(prep.handlerType, constant.GEMINIWEB) { if !translator.NeedConvert(prep.handlerType, constant.GeminiWeb) {
return gemBytes return gemBytes
} }
var param any var param any
out := translator.ResponseNonStream(prep.handlerType, constant.GEMINIWEB, ctx, modelName, prep.originalRaw, prep.translatedRaw, gemBytes, &param) out := translator.ResponseNonStream(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, gemBytes, &param)
if prep.handlerType == constant.OPENAI && out != "" { if prep.handlerType == constant.OpenAI && out != "" {
newID := fmt.Sprintf("chatcmpl-%x", time.Now().UnixNano()) newID := fmt.Sprintf("chatcmpl-%x", time.Now().UnixNano())
if v := gjson.Parse(out).Get("id"); v.Exists() { if v := gjson.Parse(out).Get("id"); v.Exists() {
out, _ = sjson.Set(out, "id", newID) 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 == "" { if prep == nil || prep.handlerType == "" {
return []string{string(gemBytes)} return []string{string(gemBytes)}
} }
if !translator.NeedConvert(prep.handlerType, constant.GEMINIWEB) { if !translator.NeedConvert(prep.handlerType, constant.GeminiWeb) {
return []string{string(gemBytes)} return []string{string(gemBytes)}
} }
var param any 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 { func (s *geminiWebState) doneStream(ctx context.Context, modelName string, prep *geminiWebPrepared) []string {
if prep == nil || prep.handlerType == "" { if prep == nil || prep.handlerType == "" {
return nil return nil
} }
if !translator.NeedConvert(prep.handlerType, constant.GEMINIWEB) { if !translator.NeedConvert(prep.handlerType, constant.GeminiWeb) {
return nil return nil
} }
var param any 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 { func (s *geminiWebState) useReusableContext() bool {

View File

@@ -5,12 +5,14 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "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" "io"
"net/http" "net/http"
"strings" "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. // 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. // using per-auth credentials (API key) and per-auth HTTP transport (proxy) from context.
type OpenAICompatExecutor struct { type OpenAICompatExecutor struct {
provider string provider string
cfg *config.Config
} }
// NewOpenAICompatExecutor creates an executor bound to a provider key (e.g., "openrouter"). // NewOpenAICompatExecutor creates an executor bound to a provider key (e.g., "openrouter").
func NewOpenAICompatExecutor(provider string) *OpenAICompatExecutor { func NewOpenAICompatExecutor(provider string, cfg *config.Config) *OpenAICompatExecutor {
return &OpenAICompatExecutor{provider: provider} return &OpenAICompatExecutor{provider: provider, cfg: cfg}
} }
// Identifier implements cliproxyauth.ProviderExecutor. // 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) translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), opts.Stream)
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
recordAPIRequest(ctx, e.cfg, translated)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated))
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
@@ -64,12 +68,14 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
appendAPIResponseChunk(ctx, e.cfg, b)
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)}
} }
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
appendAPIResponseChunk(ctx, e.cfg, body)
// Translate response back to source format when needed // Translate response back to source format when needed
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, body, &param) 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) translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
recordAPIRequest(ctx, e.cfg, translated)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -107,6 +114,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
appendAPIResponseChunk(ctx, e.cfg, b)
return nil, statusErr{code: resp.StatusCode, msg: string(b)} return nil, statusErr{code: resp.StatusCode, msg: string(b)}
} }
out := make(chan cliproxyexecutor.StreamChunk) out := make(chan cliproxyexecutor.StreamChunk)
@@ -119,6 +127,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() line := scanner.Bytes()
appendAPIResponseChunk(ctx, e.cfg, line)
if len(line) == 0 { if len(line) == 0 {
continue continue
} }
@@ -129,7 +138,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
} }
} }
if err := scanner.Err(); err != nil { if err = scanner.Err(); err != nil {
out <- cliproxyexecutor.StreamChunk{Err: err} out <- cliproxyexecutor.StreamChunk{Err: err}
} }
}() }()

View File

@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" 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. // 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. // 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" } 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) body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions" url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
recordAPIRequest(ctx, e.cfg, body)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
@@ -58,12 +62,14 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
appendAPIResponseChunk(ctx, e.cfg, b)
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)}
} }
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return cliproxyexecutor.Response{}, err return cliproxyexecutor.Response{}, err
} }
appendAPIResponseChunk(ctx, e.cfg, data)
var param any var param any
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param) out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, &param)
return cliproxyexecutor.Response{Payload: []byte(out)}, nil 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" url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
recordAPIRequest(ctx, e.cfg, body)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -109,6 +116,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
b, _ := io.ReadAll(resp.Body) b, _ := io.ReadAll(resp.Body)
appendAPIResponseChunk(ctx, e.cfg, b)
return nil, statusErr{code: resp.StatusCode, msg: string(b)} return nil, statusErr{code: resp.StatusCode, msg: string(b)}
} }
out := make(chan cliproxyexecutor.StreamChunk) out := make(chan cliproxyexecutor.StreamChunk)
@@ -121,6 +129,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
var param any var param any
for scanner.Scan() { for scanner.Scan() {
line := scanner.Bytes() 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) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), &param)
for i := range chunks { for i := range chunks {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,8 +10,8 @@ import (
// The request converter ensures missing or invalid roles are normalized to valid values. // The request converter ensures missing or invalid roles are normalized to valid values.
func init() { func init() {
translator.Register( translator.Register(
GEMINI, Gemini,
GEMINI, Gemini,
ConvertGeminiRequestToGemini, ConvertGeminiRequestToGemini,
interfaces.TranslateResponse{ interfaces.TranslateResponse{
Stream: PassthroughGeminiResponseStream, Stream: PassthroughGeminiResponseStream,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -289,9 +289,6 @@ func sanitizeTypeFields(jsonStr string) string {
break break
} else if typeStr == "number" || typeStr == "integer" { } else if typeStr == "number" || typeStr == "integer" {
preferredType = typeStr preferredType = typeStr
if preferredType == "" {
preferredType = typeStr
}
} else if preferredType == "" { } else if preferredType == "" {
preferredType = typeStr preferredType = typeStr
} }
@@ -323,6 +320,8 @@ func walkForTypeFields(value gjson.Result, path string, paths *[]string) {
walkForTypeFields(val, childPath, paths) walkForTypeFields(val, childPath, paths)
return true return true
}) })
default:
} }
} }
@@ -367,5 +366,7 @@ func findNestedSchemaPaths(value gjson.Result, path string, fieldsToFind []strin
findNestedSchemaPaths(val, childPath, fieldsToFind, paths) findNestedSchemaPaths(val, childPath, fieldsToFind, paths)
return true return true
}) })
default:
} }
} }

View File

@@ -1,6 +1,11 @@
package util package util
import ( import (
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
log "github.com/sirupsen/logrus" 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) 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,21 +174,19 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
} }
// Handle auth directory changes incrementally (.json only) // 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))
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 {
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) w.addOrUpdateClient(event.Name)
} else if event.Op&fsnotify.Remove == fsnotify.Remove { return
// 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)
} }
w.removeClient(event.Name)
} }
} }
@@ -301,7 +299,7 @@ func (w *Watcher) reloadClients() {
log.Debugf("created %d new API key clients", 0) log.Debugf("created %d new API key clients", 0)
// Load file-based clients // Load file-based clients
successfulAuthCount := w.loadFileClients(cfg) authFileCount := w.loadFileClients(cfg)
log.Debugf("loaded %d new file-based clients", 0) log.Debugf("loaded %d new file-based clients", 0)
// no legacy file-based clients to unregister // no legacy file-based clients to unregister
@@ -317,7 +315,7 @@ func (w *Watcher) reloadClients() {
return nil return nil
} }
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") { 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) sum := sha256.Sum256(data)
w.lastAuthHashes[path] = hex.EncodeToString(sum[:]) w.lastAuthHashes[path] = hex.EncodeToString(sum[:])
} }
@@ -326,12 +324,12 @@ func (w *Watcher) reloadClients() {
}) })
w.clientsMutex.Unlock() 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)", 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, 0,
totalNewClients, totalNewClients,
successfulAuthCount, authFileCount,
glAPIKeyCount, glAPIKeyCount,
claudeAPIKeyCount, claudeAPIKeyCount,
codexAPIKeyCount, codexAPIKeyCount,
@@ -572,7 +570,7 @@ func (w *Watcher) loadFileClients(cfg *config.Config) int {
log.Debugf("error accessing path %s: %v", path, err) log.Debugf("error accessing path %s: %v", path, err)
return err return err
} }
if !info.IsDir() && strings.HasSuffix(info.Name(), ".json") { if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") {
authFileCount++ authFileCount++
misc.LogCredentialSeparator() misc.LogCredentialSeparator()
log.Debugf("processing auth file %d: %s", authFileCount, filepath.Base(path)) 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 { if errWalk != nil {
log.Errorf("error walking auth directory: %v", errWalk) log.Errorf("error walking auth directory: %v", errWalk)
} }
log.Debugf("auth directory scan complete - found %d .json files, %d successful authentications", authFileCount, successfulAuthCount) log.Debugf("auth directory scan complete - found %d .json files, %d readable", authFileCount, successfulAuthCount)
return successfulAuthCount return authFileCount
} }
func BuildAPIKeyClients(cfg *config.Config) (int, int, int, int) { 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 { if err != nil {
return fmt.Errorf("auth filestore: marshal metadata failed: %w", err) 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) { if jsonEqual(existing, raw) {
return nil return nil
} }
@@ -108,8 +108,8 @@ func deepEqualJSON(a, b any) bool {
return false return false
} }
for key, subA := range valA { for key, subA := range valA {
subB, ok := valB[key] subB, ok1 := valB[key]
if !ok || !deepEqualJSON(subA, subB) { if !ok1 || !deepEqualJSON(subA, subB) {
return false 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) { func lookupMetadataTime(meta map[string]any, keys ...string) (time.Time, bool) {
for _, key := range keys { for _, key := range keys {
if val, ok := meta[key]; ok { if val, ok := meta[key]; ok {
if ts, ok := parseTimeValue(val); ok { if ts, ok1 := parseTimeValue(val); ok1 {
return ts, true return ts, true
} }
} }

View File

@@ -84,6 +84,24 @@ func (a *Auth) AccountInfo() (bool, string) {
if a == nil { if a == nil {
return false, "" 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 a.Metadata != nil {
if v, ok := a.Metadata["email"].(string); ok { if v, ok := a.Metadata["email"].(string); ok {
return false, v return false, v
@@ -125,7 +143,7 @@ func expirationFromMap(meta map[string]any) (time.Time, bool) {
} }
for _, key := range expireKeys { for _, key := range expireKeys {
if v, ok := meta[key]; ok { if v, ok := meta[key]; ok {
if ts, ok := parseTimeValue(v); ok { if ts, ok1 := parseTimeValue(v); ok1 {
return ts, true return ts, true
} }
} }
@@ -134,7 +152,7 @@ func expirationFromMap(meta map[string]any) (time.Time, bool) {
if nested, ok := meta[nestedKey]; ok { if nested, ok := meta[nestedKey]; ok {
switch val := nested.(type) { switch val := nested.(type) {
case map[string]any: case map[string]any:
if ts, ok := expirationFromMap(val); ok { if ts, ok1 := expirationFromMap(val); ok1 {
return ts, true return ts, true
} }
case map[string]string: case map[string]string:
@@ -142,7 +160,7 @@ func expirationFromMap(meta map[string]any) (time.Time, bool) {
for k, v := range val { for k, v := range val {
temp[k] = v temp[k] = v
} }
if ts, ok := expirationFromMap(temp); ok { if ts, ok1 := expirationFromMap(temp); ok1 {
return ts, true 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/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "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/runtime/executor"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus" 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) 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)", 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, totalNewClients,
tokenResult.SuccessfulAuthed, authFileCount,
apiKeyResult.GeminiKeyCount, apiKeyResult.GeminiKeyCount,
apiKeyResult.ClaudeKeyCount, apiKeyResult.ClaudeKeyCount,
apiKeyResult.CodexKeyCount, 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. // Ensure executors registered per provider: prefer stateless where available.
switch strings.ToLower(a.Provider) { switch strings.ToLower(a.Provider) {
case "gemini": case "gemini":
s.coreManager.RegisterExecutor(executor.NewGeminiExecutor()) s.coreManager.RegisterExecutor(executor.NewGeminiExecutor(s.cfg))
case "gemini-cli": case "gemini-cli":
s.coreManager.RegisterExecutor(executor.NewGeminiCLIExecutor()) s.coreManager.RegisterExecutor(executor.NewGeminiCLIExecutor(s.cfg))
case "gemini-web": case "gemini-web":
s.coreManager.RegisterExecutor(executor.NewGeminiWebExecutor(s.cfg)) s.coreManager.RegisterExecutor(executor.NewGeminiWebExecutor(s.cfg))
case "claude": case "claude":
s.coreManager.RegisterExecutor(executor.NewClaudeExecutor()) s.coreManager.RegisterExecutor(executor.NewClaudeExecutor(s.cfg))
case "codex": case "codex":
s.coreManager.RegisterExecutor(executor.NewCodexExecutor()) s.coreManager.RegisterExecutor(executor.NewCodexExecutor(s.cfg))
case "qwen": case "qwen":
s.coreManager.RegisterExecutor(executor.NewQwenExecutor()) s.coreManager.RegisterExecutor(executor.NewQwenExecutor(s.cfg))
default: default:
s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor("openai-compatibility")) s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor("openai-compatibility", s.cfg))
} }
// Preserve existing temporal fields // 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 // Ensure model registry reflects core auth identity
s.registerModelsForAuth(a) s.registerModelsForAuth(a)
if _, ok := s.coreManager.GetByID(a.ID); ok { if _, ok := s.coreManager.GetByID(a.ID); ok {
s.coreManager.Update(ctx, a) _, _ = s.coreManager.Update(ctx, a)
} else { } else {
s.coreManager.Register(ctx, a) _, _ = s.coreManager.Register(ctx, a)
} }
} }
// Disable removed auths // Disable removed auths
@@ -333,7 +335,7 @@ func (s *Service) syncCoreAuthFromAuths(ctx context.Context, auths []*coreauth.A
stored.Status = coreauth.StatusDisabled stored.Status = coreauth.StatusDisabled
// Unregister from model registry when disabled // Unregister from model registry when disabled
GlobalModelRegistry().UnregisterClient(stored.ID) GlobalModelRegistry().UnregisterClient(stored.ID)
s.coreManager.Update(ctx, stored) _, _ = s.coreManager.Update(ctx, stored)
} }
} }