diff --git a/internal/api/handlers/management/config_basic.go b/internal/api/handlers/management/config_basic.go index 5843c5b8..ae292982 100644 --- a/internal/api/handlers/management/config_basic.go +++ b/internal/api/handlers/management/config_basic.go @@ -1,16 +1,28 @@ package management import ( + "encoding/json" + "fmt" "io" "net/http" "os" "path/filepath" + "strings" + "time" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) +const ( + latestReleaseURL = "https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest" + latestReleaseUserAgent = "CLIProxyAPI" +) + func (h *Handler) GetConfig(c *gin.Context) { if h == nil || h.cfg == nil { c.JSON(200, gin.H{}) @@ -20,6 +32,66 @@ func (h *Handler) GetConfig(c *gin.Context) { c.JSON(200, &cfgCopy) } +type releaseInfo struct { + TagName string `json:"tag_name"` + Name string `json:"name"` +} + +// GetLatestVersion returns the latest release version from GitHub without downloading assets. +func (h *Handler) GetLatestVersion(c *gin.Context) { + client := &http.Client{Timeout: 10 * time.Second} + proxyURL := "" + if h != nil && h.cfg != nil { + proxyURL = strings.TrimSpace(h.cfg.ProxyURL) + } + if proxyURL != "" { + sdkCfg := &sdkconfig.SDKConfig{ProxyURL: proxyURL} + util.SetProxy(sdkCfg, client) + } + + req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, latestReleaseURL, nil) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "request_create_failed", "message": err.Error()}) + return + } + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("User-Agent", latestReleaseUserAgent) + + resp, err := client.Do(req) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": "request_failed", "message": err.Error()}) + return + } + defer func() { + if errClose := resp.Body.Close(); errClose != nil { + log.WithError(errClose).Debug("failed to close latest version response body") + } + }() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) + c.JSON(http.StatusBadGateway, gin.H{"error": "unexpected_status", "message": fmt.Sprintf("status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))}) + return + } + + var info releaseInfo + if errDecode := json.NewDecoder(resp.Body).Decode(&info); errDecode != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": "decode_failed", "message": errDecode.Error()}) + return + } + + version := strings.TrimSpace(info.TagName) + if version == "" { + version = strings.TrimSpace(info.Name) + } + if version == "" { + c.JSON(http.StatusBadGateway, gin.H{"error": "invalid_response", "message": "missing release version"}) + return + } + + c.JSON(http.StatusOK, gin.H{"latest-version": version}) +} + func WriteConfig(path string, data []byte) error { data = config.NormalizeCommentIndentation(data) f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) diff --git a/internal/api/server.go b/internal/api/server.go index c4d6ad8f..9e1c5848 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -472,6 +472,7 @@ func (s *Server) registerManagementRoutes() { mgmt.GET("/config", s.mgmt.GetConfig) mgmt.GET("/config.yaml", s.mgmt.GetConfigYAML) mgmt.PUT("/config.yaml", s.mgmt.PutConfigYAML) + mgmt.GET("/latest-version", s.mgmt.GetLatestVersion) mgmt.GET("/debug", s.mgmt.GetDebug) mgmt.PUT("/debug", s.mgmt.PutDebug) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index a667a4d9..f83ba789 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -17,6 +17,7 @@ import ( "github.com/google/uuid" "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/util" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" @@ -510,6 +511,24 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau payload = geminiToAntigravity(modelName, payload) payload, _ = sjson.SetBytes(payload, "model", alias2ModelName(modelName)) + + if strings.Contains(modelName, "claude") { + strJSON := string(payload) + paths := make([]string, 0) + util.Walk(gjson.ParseBytes(payload), "", "parametersJsonSchema", &paths) + for _, p := range paths { + strJSON, _ = util.RenameKey(strJSON, p, p[:len(p)-len("parametersJsonSchema")]+"parameters") + } + + strJSON = util.DeleteKey(strJSON, "$schema") + strJSON = util.DeleteKey(strJSON, "maxItems") + strJSON = util.DeleteKey(strJSON, "minItems") + strJSON = util.DeleteKey(strJSON, "minLength") + strJSON = util.DeleteKey(strJSON, "maxLength") + + payload = []byte(strJSON) + } + httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(payload)) if errReq != nil { return nil, errReq diff --git a/internal/util/translator.go b/internal/util/translator.go index 40274aca..5a4bb7e8 100644 --- a/internal/util/translator.go +++ b/internal/util/translator.go @@ -79,6 +79,15 @@ func RenameKey(jsonStr, oldKeyPath, newKeyPath string) (string, error) { return finalJson, nil } +func DeleteKey(jsonStr, keyName string) string { + paths := make([]string, 0) + Walk(gjson.Parse(jsonStr), "", keyName, &paths) + for _, p := range paths { + jsonStr, _ = sjson.Delete(jsonStr, p) + } + return jsonStr +} + // FixJSON converts non-standard JSON that uses single quotes for strings into // RFC 8259-compliant JSON by converting those single-quoted strings to // double-quoted strings with proper escaping.