Spaces:
Paused
**feat(auth, executor, cmd): add Antigravity provider integration**
Browse files- Implemented OAuth login flow for the Antigravity provider in `auth/antigravity.go`.
- Added `AntigravityExecutor` for handling requests and streaming via Antigravity APIs.
- Created `antigravity_login.go` command for triggering Antigravity authentication.
- Introduced OpenAI-to-Antigravity translation logic in `translator/antigravity/openai/chat-completions`.
**refactor(translator, executor): update Gemini CLI response translation and add Antigravity payload customization**
- Renamed Gemini CLI translation methods to align with response handling (`ConvertGeminiCliResponseToGemini` and `ConvertGeminiCliResponseToGeminiNonStream`).
- Updated `init.go` to reflect these method changes.
- Introduced `geminiToAntigravity` function to embed metadata (`model`, `userAgent`, `project`, etc.) into Antigravity payloads.
- Added random project, request, and session ID generators for enhanced tracking.
- Streamlined `buildRequest` to use `geminiToAntigravity` transformation before request execution.
- cmd/server/main.go +5 -0
- internal/cmd/antigravity_login.go +38 -0
- internal/cmd/auth_manager.go +1 -0
- internal/runtime/executor/antigravity_executor.go +560 -0
- internal/translator/gemini-cli/gemini/{gemini_gemini-cli_request.go → gemini-cli_gemini_response.go} +9 -4
- internal/translator/gemini-cli/gemini/init.go +2 -2
- sdk/auth/antigravity.go +289 -0
- sdk/auth/refresh_registry.go +1 -0
- sdk/cliproxy/service.go +6 -0
- sdk/translator/formats.go +1 -0
|
@@ -61,6 +61,7 @@ func main() {
|
|
| 61 |
var iflowLogin bool
|
| 62 |
var iflowCookie bool
|
| 63 |
var noBrowser bool
|
|
|
|
| 64 |
var projectID string
|
| 65 |
var vertexImport string
|
| 66 |
var configPath string
|
|
@@ -74,6 +75,7 @@ func main() {
|
|
| 74 |
flag.BoolVar(&iflowLogin, "iflow-login", false, "Login to iFlow using OAuth")
|
| 75 |
flag.BoolVar(&iflowCookie, "iflow-cookie", false, "Login to iFlow using Cookie")
|
| 76 |
flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
|
|
|
|
| 77 |
flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
|
| 78 |
flag.StringVar(&configPath, "config", DefaultConfigPath, "Configure File Path")
|
| 79 |
flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file")
|
|
@@ -431,6 +433,9 @@ func main() {
|
|
| 431 |
} else if login {
|
| 432 |
// Handle Google/Gemini login
|
| 433 |
cmd.DoLogin(cfg, projectID, options)
|
|
|
|
|
|
|
|
|
|
| 434 |
} else if codexLogin {
|
| 435 |
// Handle Codex login
|
| 436 |
cmd.DoCodexLogin(cfg, options)
|
|
|
|
| 61 |
var iflowLogin bool
|
| 62 |
var iflowCookie bool
|
| 63 |
var noBrowser bool
|
| 64 |
+
var antigravityLogin bool
|
| 65 |
var projectID string
|
| 66 |
var vertexImport string
|
| 67 |
var configPath string
|
|
|
|
| 75 |
flag.BoolVar(&iflowLogin, "iflow-login", false, "Login to iFlow using OAuth")
|
| 76 |
flag.BoolVar(&iflowCookie, "iflow-cookie", false, "Login to iFlow using Cookie")
|
| 77 |
flag.BoolVar(&noBrowser, "no-browser", false, "Don't open browser automatically for OAuth")
|
| 78 |
+
flag.BoolVar(&antigravityLogin, "antigravity-login", false, "Login to Antigravity using OAuth")
|
| 79 |
flag.StringVar(&projectID, "project_id", "", "Project ID (Gemini only, not required)")
|
| 80 |
flag.StringVar(&configPath, "config", DefaultConfigPath, "Configure File Path")
|
| 81 |
flag.StringVar(&vertexImport, "vertex-import", "", "Import Vertex service account key JSON file")
|
|
|
|
| 433 |
} else if login {
|
| 434 |
// Handle Google/Gemini login
|
| 435 |
cmd.DoLogin(cfg, projectID, options)
|
| 436 |
+
} else if antigravityLogin {
|
| 437 |
+
// Handle Antigravity login
|
| 438 |
+
cmd.DoAntigravityLogin(cfg, options)
|
| 439 |
} else if codexLogin {
|
| 440 |
// Handle Codex login
|
| 441 |
cmd.DoCodexLogin(cfg, options)
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package cmd
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"fmt"
|
| 6 |
+
|
| 7 |
+
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
| 8 |
+
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
|
| 9 |
+
log "github.com/sirupsen/logrus"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
// DoAntigravityLogin triggers the OAuth flow for the antigravity provider and saves tokens.
|
| 13 |
+
func DoAntigravityLogin(cfg *config.Config, options *LoginOptions) {
|
| 14 |
+
if options == nil {
|
| 15 |
+
options = &LoginOptions{}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
manager := newAuthManager()
|
| 19 |
+
authOpts := &sdkAuth.LoginOptions{
|
| 20 |
+
NoBrowser: options.NoBrowser,
|
| 21 |
+
Metadata: map[string]string{},
|
| 22 |
+
Prompt: options.Prompt,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
record, savedPath, err := manager.Login(context.Background(), "antigravity", cfg, authOpts)
|
| 26 |
+
if err != nil {
|
| 27 |
+
log.Errorf("Antigravity authentication failed: %v", err)
|
| 28 |
+
return
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
if savedPath != "" {
|
| 32 |
+
fmt.Printf("Authentication saved to %s\n", savedPath)
|
| 33 |
+
}
|
| 34 |
+
if record != nil && record.Label != "" {
|
| 35 |
+
fmt.Printf("Authenticated as %s\n", record.Label)
|
| 36 |
+
}
|
| 37 |
+
fmt.Println("Antigravity authentication successful!")
|
| 38 |
+
}
|
|
@@ -18,6 +18,7 @@ func newAuthManager() *sdkAuth.Manager {
|
|
| 18 |
sdkAuth.NewClaudeAuthenticator(),
|
| 19 |
sdkAuth.NewQwenAuthenticator(),
|
| 20 |
sdkAuth.NewIFlowAuthenticator(),
|
|
|
|
| 21 |
)
|
| 22 |
return manager
|
| 23 |
}
|
|
|
|
| 18 |
sdkAuth.NewClaudeAuthenticator(),
|
| 19 |
sdkAuth.NewQwenAuthenticator(),
|
| 20 |
sdkAuth.NewIFlowAuthenticator(),
|
| 21 |
+
sdkAuth.NewAntigravityAuthenticator(),
|
| 22 |
)
|
| 23 |
return manager
|
| 24 |
}
|
|
@@ -0,0 +1,560 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package executor
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"bytes"
|
| 6 |
+
"context"
|
| 7 |
+
"encoding/json"
|
| 8 |
+
"fmt"
|
| 9 |
+
"io"
|
| 10 |
+
"math/rand"
|
| 11 |
+
"net/http"
|
| 12 |
+
"net/url"
|
| 13 |
+
"strconv"
|
| 14 |
+
"strings"
|
| 15 |
+
"time"
|
| 16 |
+
|
| 17 |
+
"github.com/google/uuid"
|
| 18 |
+
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
| 19 |
+
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
| 20 |
+
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
| 21 |
+
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
| 22 |
+
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
| 23 |
+
log "github.com/sirupsen/logrus"
|
| 24 |
+
"github.com/tidwall/gjson"
|
| 25 |
+
"github.com/tidwall/sjson"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
const (
|
| 29 |
+
antigravityBaseURL = "https://daily-cloudcode-pa.sandbox.googleapis.com"
|
| 30 |
+
antigravityStreamPath = "/v1internal:streamGenerateContent"
|
| 31 |
+
antigravityGeneratePath = "/v1internal:generateContent"
|
| 32 |
+
antigravityModelsPath = "/v1internal:fetchAvailableModels"
|
| 33 |
+
antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
| 34 |
+
antigravityClientSecret = "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"
|
| 35 |
+
defaultAntigravityAgent = "antigravity/1.11.3 windows/amd64"
|
| 36 |
+
antigravityAuthType = "antigravity"
|
| 37 |
+
refreshSkew = 5 * time.Minute
|
| 38 |
+
streamScannerBuffer int = 20_971_520
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
var randSource = rand.New(rand.NewSource(time.Now().UnixNano()))
|
| 42 |
+
|
| 43 |
+
// AntigravityExecutor proxies requests to the antigravity upstream.
|
| 44 |
+
type AntigravityExecutor struct {
|
| 45 |
+
cfg *config.Config
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// NewAntigravityExecutor constructs a new executor instance.
|
| 49 |
+
func NewAntigravityExecutor(cfg *config.Config) *AntigravityExecutor {
|
| 50 |
+
return &AntigravityExecutor{cfg: cfg}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// Identifier implements ProviderExecutor.
|
| 54 |
+
func (e *AntigravityExecutor) Identifier() string { return antigravityAuthType }
|
| 55 |
+
|
| 56 |
+
// PrepareRequest implements ProviderExecutor.
|
| 57 |
+
func (e *AntigravityExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) error { return nil }
|
| 58 |
+
|
| 59 |
+
// Execute handles non-streaming requests via the antigravity generate endpoint.
|
| 60 |
+
func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
| 61 |
+
token, updatedAuth, errToken := e.ensureAccessToken(ctx, auth)
|
| 62 |
+
if errToken != nil {
|
| 63 |
+
return resp, errToken
|
| 64 |
+
}
|
| 65 |
+
if updatedAuth != nil {
|
| 66 |
+
auth = updatedAuth
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
|
| 70 |
+
defer reporter.trackFailure(ctx, &err)
|
| 71 |
+
|
| 72 |
+
from := opts.SourceFormat
|
| 73 |
+
to := sdktranslator.FromString("gemini-cli")
|
| 74 |
+
translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
| 75 |
+
|
| 76 |
+
httpReq, errReq := e.buildRequest(ctx, auth, token, req.Model, translated, false, opts.Alt)
|
| 77 |
+
if errReq != nil {
|
| 78 |
+
return resp, errReq
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
| 82 |
+
httpResp, errDo := httpClient.Do(httpReq)
|
| 83 |
+
if errDo != nil {
|
| 84 |
+
recordAPIResponseError(ctx, e.cfg, errDo)
|
| 85 |
+
return resp, errDo
|
| 86 |
+
}
|
| 87 |
+
defer func() {
|
| 88 |
+
if errClose := httpResp.Body.Close(); errClose != nil {
|
| 89 |
+
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
| 90 |
+
}
|
| 91 |
+
}()
|
| 92 |
+
|
| 93 |
+
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
| 94 |
+
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
| 95 |
+
if errRead != nil {
|
| 96 |
+
recordAPIResponseError(ctx, e.cfg, errRead)
|
| 97 |
+
return resp, errRead
|
| 98 |
+
}
|
| 99 |
+
appendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
| 100 |
+
|
| 101 |
+
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
| 102 |
+
log.Debugf("antigravity executor: upstream error status: %d, body: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), bodyBytes))
|
| 103 |
+
err = statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)}
|
| 104 |
+
return resp, err
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
var param any
|
| 108 |
+
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, bodyBytes, ¶m)
|
| 109 |
+
resp = cliproxyexecutor.Response{Payload: []byte(converted)}
|
| 110 |
+
reporter.ensurePublished(ctx)
|
| 111 |
+
return resp, nil
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// ExecuteStream handles streaming requests via the antigravity upstream.
|
| 115 |
+
func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
| 116 |
+
ctx = context.WithValue(ctx, "alt", "")
|
| 117 |
+
|
| 118 |
+
token, updatedAuth, errToken := e.ensureAccessToken(ctx, auth)
|
| 119 |
+
if errToken != nil {
|
| 120 |
+
return nil, errToken
|
| 121 |
+
}
|
| 122 |
+
if updatedAuth != nil {
|
| 123 |
+
auth = updatedAuth
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
|
| 127 |
+
defer reporter.trackFailure(ctx, &err)
|
| 128 |
+
|
| 129 |
+
from := opts.SourceFormat
|
| 130 |
+
to := sdktranslator.FromString("gemini-cli")
|
| 131 |
+
translated := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
| 132 |
+
|
| 133 |
+
httpReq, errReq := e.buildRequest(ctx, auth, token, req.Model, translated, true, opts.Alt)
|
| 134 |
+
if errReq != nil {
|
| 135 |
+
return nil, errReq
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
| 139 |
+
httpResp, errDo := httpClient.Do(httpReq)
|
| 140 |
+
if errDo != nil {
|
| 141 |
+
recordAPIResponseError(ctx, e.cfg, errDo)
|
| 142 |
+
return nil, errDo
|
| 143 |
+
}
|
| 144 |
+
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
| 145 |
+
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
| 146 |
+
bodyBytes, _ := io.ReadAll(httpResp.Body)
|
| 147 |
+
appendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
| 148 |
+
if errClose := httpResp.Body.Close(); errClose != nil {
|
| 149 |
+
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
| 150 |
+
}
|
| 151 |
+
err = statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)}
|
| 152 |
+
return nil, err
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
out := make(chan cliproxyexecutor.StreamChunk)
|
| 156 |
+
stream = out
|
| 157 |
+
go func() {
|
| 158 |
+
defer close(out)
|
| 159 |
+
defer func() {
|
| 160 |
+
if errClose := httpResp.Body.Close(); errClose != nil {
|
| 161 |
+
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
| 162 |
+
}
|
| 163 |
+
}()
|
| 164 |
+
scanner := bufio.NewScanner(httpResp.Body)
|
| 165 |
+
scanner.Buffer(nil, streamScannerBuffer)
|
| 166 |
+
var param any
|
| 167 |
+
for scanner.Scan() {
|
| 168 |
+
line := scanner.Bytes()
|
| 169 |
+
appendAPIResponseChunk(ctx, e.cfg, line)
|
| 170 |
+
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, bytes.Clone(line), ¶m)
|
| 171 |
+
for i := range chunks {
|
| 172 |
+
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
tail := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translated, []byte("[DONE]"), ¶m)
|
| 176 |
+
for i := range tail {
|
| 177 |
+
out <- cliproxyexecutor.StreamChunk{Payload: []byte(tail[i])}
|
| 178 |
+
}
|
| 179 |
+
if errScan := scanner.Err(); errScan != nil {
|
| 180 |
+
recordAPIResponseError(ctx, e.cfg, errScan)
|
| 181 |
+
reporter.publishFailure(ctx)
|
| 182 |
+
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
| 183 |
+
} else {
|
| 184 |
+
reporter.ensurePublished(ctx)
|
| 185 |
+
}
|
| 186 |
+
}()
|
| 187 |
+
return stream, nil
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// Refresh refreshes the OAuth token using the refresh token.
|
| 191 |
+
func (e *AntigravityExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
| 192 |
+
if auth == nil {
|
| 193 |
+
return auth, nil
|
| 194 |
+
}
|
| 195 |
+
updated, errRefresh := e.refreshToken(ctx, auth.Clone())
|
| 196 |
+
if errRefresh != nil {
|
| 197 |
+
return nil, errRefresh
|
| 198 |
+
}
|
| 199 |
+
return updated, nil
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// CountTokens is not supported for the antigravity provider.
|
| 203 |
+
func (e *AntigravityExecutor) CountTokens(context.Context, *cliproxyauth.Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
|
| 204 |
+
return cliproxyexecutor.Response{}, statusErr{code: http.StatusNotImplemented, msg: "count tokens not supported"}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// FetchAntigravityModels retrieves available models using the supplied auth.
|
| 208 |
+
func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *config.Config) []*registry.ModelInfo {
|
| 209 |
+
exec := &AntigravityExecutor{cfg: cfg}
|
| 210 |
+
token, updatedAuth, errToken := exec.ensureAccessToken(ctx, auth)
|
| 211 |
+
if errToken != nil || token == "" {
|
| 212 |
+
return nil
|
| 213 |
+
}
|
| 214 |
+
if updatedAuth != nil {
|
| 215 |
+
auth = updatedAuth
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
modelsURL := buildBaseURL(auth) + antigravityModelsPath
|
| 219 |
+
httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, modelsURL, bytes.NewReader([]byte(`{}`)))
|
| 220 |
+
if errReq != nil {
|
| 221 |
+
return nil
|
| 222 |
+
}
|
| 223 |
+
httpReq.Header.Set("Content-Type", "application/json")
|
| 224 |
+
httpReq.Header.Set("Authorization", "Bearer "+token)
|
| 225 |
+
httpReq.Header.Set("User-Agent", resolveUserAgent(auth))
|
| 226 |
+
if host := resolveHost(auth); host != "" {
|
| 227 |
+
httpReq.Host = host
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
httpClient := newProxyAwareHTTPClient(ctx, cfg, auth, 0)
|
| 231 |
+
httpResp, errDo := httpClient.Do(httpReq)
|
| 232 |
+
if errDo != nil {
|
| 233 |
+
return nil
|
| 234 |
+
}
|
| 235 |
+
defer func() {
|
| 236 |
+
if errClose := httpResp.Body.Close(); errClose != nil {
|
| 237 |
+
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
| 238 |
+
}
|
| 239 |
+
}()
|
| 240 |
+
|
| 241 |
+
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
| 242 |
+
if errRead != nil {
|
| 243 |
+
return nil
|
| 244 |
+
}
|
| 245 |
+
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
| 246 |
+
return nil
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
result := gjson.GetBytes(bodyBytes, "models")
|
| 250 |
+
if !result.Exists() {
|
| 251 |
+
return nil
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
now := time.Now().Unix()
|
| 255 |
+
models := make([]*registry.ModelInfo, 0, len(result.Map()))
|
| 256 |
+
for id := range result.Map() {
|
| 257 |
+
models = append(models, ®istry.ModelInfo{
|
| 258 |
+
ID: id,
|
| 259 |
+
Object: "model",
|
| 260 |
+
Created: now,
|
| 261 |
+
OwnedBy: antigravityAuthType,
|
| 262 |
+
Type: antigravityAuthType,
|
| 263 |
+
})
|
| 264 |
+
}
|
| 265 |
+
return models
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
func (e *AntigravityExecutor) ensureAccessToken(ctx context.Context, auth *cliproxyauth.Auth) (string, *cliproxyauth.Auth, error) {
|
| 269 |
+
if auth == nil {
|
| 270 |
+
return "", nil, statusErr{code: http.StatusUnauthorized, msg: "missing auth"}
|
| 271 |
+
}
|
| 272 |
+
accessToken := metaStringValue(auth.Metadata, "access_token")
|
| 273 |
+
expiry := tokenExpiry(auth.Metadata)
|
| 274 |
+
if accessToken != "" && expiry.After(time.Now().Add(refreshSkew)) {
|
| 275 |
+
return accessToken, nil, nil
|
| 276 |
+
}
|
| 277 |
+
updated, errRefresh := e.refreshToken(ctx, auth.Clone())
|
| 278 |
+
if errRefresh != nil {
|
| 279 |
+
return "", nil, errRefresh
|
| 280 |
+
}
|
| 281 |
+
return metaStringValue(updated.Metadata, "access_token"), updated, nil
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
func (e *AntigravityExecutor) refreshToken(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
| 285 |
+
if auth == nil {
|
| 286 |
+
return nil, statusErr{code: http.StatusUnauthorized, msg: "missing auth"}
|
| 287 |
+
}
|
| 288 |
+
refreshToken := metaStringValue(auth.Metadata, "refresh_token")
|
| 289 |
+
if refreshToken == "" {
|
| 290 |
+
return auth, statusErr{code: http.StatusUnauthorized, msg: "missing refresh token"}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
form := url.Values{}
|
| 294 |
+
form.Set("client_id", antigravityClientID)
|
| 295 |
+
form.Set("client_secret", antigravityClientSecret)
|
| 296 |
+
form.Set("grant_type", "refresh_token")
|
| 297 |
+
form.Set("refresh_token", refreshToken)
|
| 298 |
+
|
| 299 |
+
httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, "https://oauth2.googleapis.com/token", strings.NewReader(form.Encode()))
|
| 300 |
+
if errReq != nil {
|
| 301 |
+
return auth, errReq
|
| 302 |
+
}
|
| 303 |
+
httpReq.Header.Set("Host", "oauth2.googleapis.com")
|
| 304 |
+
httpReq.Header.Set("User-Agent", defaultAntigravityAgent)
|
| 305 |
+
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
| 306 |
+
|
| 307 |
+
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
| 308 |
+
httpResp, errDo := httpClient.Do(httpReq)
|
| 309 |
+
if errDo != nil {
|
| 310 |
+
return auth, errDo
|
| 311 |
+
}
|
| 312 |
+
defer func() {
|
| 313 |
+
if errClose := httpResp.Body.Close(); errClose != nil {
|
| 314 |
+
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
| 315 |
+
}
|
| 316 |
+
}()
|
| 317 |
+
|
| 318 |
+
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
| 319 |
+
if errRead != nil {
|
| 320 |
+
return auth, errRead
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
| 324 |
+
return auth, statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
var tokenResp struct {
|
| 328 |
+
AccessToken string `json:"access_token"`
|
| 329 |
+
RefreshToken string `json:"refresh_token"`
|
| 330 |
+
ExpiresIn int64 `json:"expires_in"`
|
| 331 |
+
TokenType string `json:"token_type"`
|
| 332 |
+
}
|
| 333 |
+
if errUnmarshal := json.Unmarshal(bodyBytes, &tokenResp); errUnmarshal != nil {
|
| 334 |
+
return auth, errUnmarshal
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
if auth.Metadata == nil {
|
| 338 |
+
auth.Metadata = make(map[string]any)
|
| 339 |
+
}
|
| 340 |
+
auth.Metadata["access_token"] = tokenResp.AccessToken
|
| 341 |
+
if tokenResp.RefreshToken != "" {
|
| 342 |
+
auth.Metadata["refresh_token"] = tokenResp.RefreshToken
|
| 343 |
+
}
|
| 344 |
+
auth.Metadata["expires_in"] = tokenResp.ExpiresIn
|
| 345 |
+
auth.Metadata["timestamp"] = time.Now().UnixMilli()
|
| 346 |
+
auth.Metadata["expired"] = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second).Format(time.RFC3339)
|
| 347 |
+
auth.Metadata["type"] = antigravityAuthType
|
| 348 |
+
return auth, nil
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyauth.Auth, token, modelName string, payload []byte, stream bool, alt string) (*http.Request, error) {
|
| 352 |
+
if token == "" {
|
| 353 |
+
return nil, statusErr{code: http.StatusUnauthorized, msg: "missing access token"}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
base := buildBaseURL(auth)
|
| 357 |
+
path := antigravityGeneratePath
|
| 358 |
+
if stream {
|
| 359 |
+
path = antigravityStreamPath
|
| 360 |
+
}
|
| 361 |
+
var requestURL strings.Builder
|
| 362 |
+
requestURL.WriteString(base)
|
| 363 |
+
requestURL.WriteString(path)
|
| 364 |
+
if stream {
|
| 365 |
+
if alt != "" {
|
| 366 |
+
requestURL.WriteString("?$alt=")
|
| 367 |
+
requestURL.WriteString(url.QueryEscape(alt))
|
| 368 |
+
} else {
|
| 369 |
+
requestURL.WriteString("?alt=sse")
|
| 370 |
+
}
|
| 371 |
+
} else if alt != "" {
|
| 372 |
+
requestURL.WriteString("?$alt=")
|
| 373 |
+
requestURL.WriteString(url.QueryEscape(alt))
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
payload = geminiToAntigravity(modelName, payload)
|
| 377 |
+
httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(payload))
|
| 378 |
+
if errReq != nil {
|
| 379 |
+
return nil, errReq
|
| 380 |
+
}
|
| 381 |
+
httpReq.Header.Set("Content-Type", "application/json")
|
| 382 |
+
httpReq.Header.Set("Authorization", "Bearer "+token)
|
| 383 |
+
httpReq.Header.Set("User-Agent", resolveUserAgent(auth))
|
| 384 |
+
if stream {
|
| 385 |
+
httpReq.Header.Set("Accept", "text/event-stream")
|
| 386 |
+
} else {
|
| 387 |
+
httpReq.Header.Set("Accept", "application/json")
|
| 388 |
+
}
|
| 389 |
+
if host := resolveHost(auth); host != "" {
|
| 390 |
+
httpReq.Host = host
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
var authID, authLabel, authType, authValue string
|
| 394 |
+
if auth != nil {
|
| 395 |
+
authID = auth.ID
|
| 396 |
+
authLabel = auth.Label
|
| 397 |
+
authType, authValue = auth.AccountInfo()
|
| 398 |
+
}
|
| 399 |
+
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
| 400 |
+
URL: requestURL.String(),
|
| 401 |
+
Method: http.MethodPost,
|
| 402 |
+
Headers: httpReq.Header.Clone(),
|
| 403 |
+
Body: payload,
|
| 404 |
+
Provider: e.Identifier(),
|
| 405 |
+
AuthID: authID,
|
| 406 |
+
AuthLabel: authLabel,
|
| 407 |
+
AuthType: authType,
|
| 408 |
+
AuthValue: authValue,
|
| 409 |
+
})
|
| 410 |
+
|
| 411 |
+
return httpReq, nil
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
func tokenExpiry(metadata map[string]any) time.Time {
|
| 415 |
+
if metadata == nil {
|
| 416 |
+
return time.Time{}
|
| 417 |
+
}
|
| 418 |
+
if expStr, ok := metadata["expired"].(string); ok {
|
| 419 |
+
expStr = strings.TrimSpace(expStr)
|
| 420 |
+
if expStr != "" {
|
| 421 |
+
if parsed, errParse := time.Parse(time.RFC3339, expStr); errParse == nil {
|
| 422 |
+
return parsed
|
| 423 |
+
}
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
expiresIn, hasExpires := int64Value(metadata["expires_in"])
|
| 427 |
+
tsMs, hasTimestamp := int64Value(metadata["timestamp"])
|
| 428 |
+
if hasExpires && hasTimestamp {
|
| 429 |
+
return time.Unix(0, tsMs*int64(time.Millisecond)).Add(time.Duration(expiresIn) * time.Second)
|
| 430 |
+
}
|
| 431 |
+
return time.Time{}
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
func metaStringValue(metadata map[string]any, key string) string {
|
| 435 |
+
if metadata == nil {
|
| 436 |
+
return ""
|
| 437 |
+
}
|
| 438 |
+
if v, ok := metadata[key]; ok {
|
| 439 |
+
switch typed := v.(type) {
|
| 440 |
+
case string:
|
| 441 |
+
return strings.TrimSpace(typed)
|
| 442 |
+
case []byte:
|
| 443 |
+
return strings.TrimSpace(string(typed))
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
+
return ""
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
func int64Value(value any) (int64, bool) {
|
| 450 |
+
switch typed := value.(type) {
|
| 451 |
+
case int:
|
| 452 |
+
return int64(typed), true
|
| 453 |
+
case int64:
|
| 454 |
+
return typed, true
|
| 455 |
+
case float64:
|
| 456 |
+
return int64(typed), true
|
| 457 |
+
case json.Number:
|
| 458 |
+
if i, errParse := typed.Int64(); errParse == nil {
|
| 459 |
+
return i, true
|
| 460 |
+
}
|
| 461 |
+
case string:
|
| 462 |
+
if strings.TrimSpace(typed) == "" {
|
| 463 |
+
return 0, false
|
| 464 |
+
}
|
| 465 |
+
if i, errParse := strconv.ParseInt(strings.TrimSpace(typed), 10, 64); errParse == nil {
|
| 466 |
+
return i, true
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
return 0, false
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
func buildBaseURL(auth *cliproxyauth.Auth) string {
|
| 473 |
+
if auth != nil {
|
| 474 |
+
if auth.Attributes != nil {
|
| 475 |
+
if v := strings.TrimSpace(auth.Attributes["base_url"]); v != "" {
|
| 476 |
+
return strings.TrimSuffix(v, "/")
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
if auth.Metadata != nil {
|
| 480 |
+
if v, ok := auth.Metadata["base_url"].(string); ok {
|
| 481 |
+
v = strings.TrimSpace(v)
|
| 482 |
+
if v != "" {
|
| 483 |
+
return strings.TrimSuffix(v, "/")
|
| 484 |
+
}
|
| 485 |
+
}
|
| 486 |
+
}
|
| 487 |
+
}
|
| 488 |
+
return antigravityBaseURL
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
func resolveHost(auth *cliproxyauth.Auth) string {
|
| 492 |
+
base := buildBaseURL(auth)
|
| 493 |
+
parsed, errParse := url.Parse(base)
|
| 494 |
+
if errParse != nil {
|
| 495 |
+
return ""
|
| 496 |
+
}
|
| 497 |
+
if parsed.Host != "" {
|
| 498 |
+
return parsed.Host
|
| 499 |
+
}
|
| 500 |
+
return strings.TrimPrefix(strings.TrimPrefix(base, "https://"), "http://")
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
func resolveUserAgent(auth *cliproxyauth.Auth) string {
|
| 504 |
+
if auth != nil {
|
| 505 |
+
if auth.Attributes != nil {
|
| 506 |
+
if ua := strings.TrimSpace(auth.Attributes["user_agent"]); ua != "" {
|
| 507 |
+
return ua
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
if auth.Metadata != nil {
|
| 511 |
+
if ua, ok := auth.Metadata["user_agent"].(string); ok && strings.TrimSpace(ua) != "" {
|
| 512 |
+
return strings.TrimSpace(ua)
|
| 513 |
+
}
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
return defaultAntigravityAgent
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
func geminiToAntigravity(modelName string, payload []byte) []byte {
|
| 520 |
+
template, _ := sjson.Set(string(payload), "model", modelName)
|
| 521 |
+
template, _ = sjson.Set(template, "userAgent", "antigravity")
|
| 522 |
+
template, _ = sjson.Set(template, "project", generateProjectID())
|
| 523 |
+
template, _ = sjson.Set(template, "requestId", generateRequestID())
|
| 524 |
+
template, _ = sjson.Set(template, "request.sessionId", generateSessionID())
|
| 525 |
+
|
| 526 |
+
template, _ = sjson.Delete(template, "request.safetySettings")
|
| 527 |
+
template, _ = sjson.Set(template, "request.toolConfig.functionCallingConfig.mode", "VALIDATED")
|
| 528 |
+
|
| 529 |
+
gjson.Get(template, "request.contents").ForEach(func(key, content gjson.Result) bool {
|
| 530 |
+
if content.Get("role").String() == "model" {
|
| 531 |
+
content.Get("parts").ForEach(func(partKey, part gjson.Result) bool {
|
| 532 |
+
if part.Get("functionCall").Exists() {
|
| 533 |
+
template, _ = sjson.Set(template, fmt.Sprintf("request.contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator")
|
| 534 |
+
}
|
| 535 |
+
return true
|
| 536 |
+
})
|
| 537 |
+
}
|
| 538 |
+
return true
|
| 539 |
+
})
|
| 540 |
+
|
| 541 |
+
return []byte(template)
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
func generateRequestID() string {
|
| 545 |
+
return "agent-" + uuid.NewString()
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
func generateSessionID() string {
|
| 549 |
+
n := randSource.Int63n(9_000_000_000_000_000_000)
|
| 550 |
+
return "-" + strconv.FormatInt(n, 10)
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
func generateProjectID() string {
|
| 554 |
+
adjectives := []string{"useful", "bright", "swift", "calm", "bold"}
|
| 555 |
+
nouns := []string{"fuze", "wave", "spark", "flow", "core"}
|
| 556 |
+
adj := adjectives[randSource.Intn(len(adjectives))]
|
| 557 |
+
noun := nouns[randSource.Intn(len(nouns))]
|
| 558 |
+
randomPart := strings.ToLower(uuid.NewString())[:5]
|
| 559 |
+
return adj + "-" + noun + "-" + randomPart
|
| 560 |
+
}
|
|
@@ -6,6 +6,7 @@
|
|
| 6 |
package gemini
|
| 7 |
|
| 8 |
import (
|
|
|
|
| 9 |
"context"
|
| 10 |
"fmt"
|
| 11 |
|
|
@@ -13,7 +14,7 @@ import (
|
|
| 13 |
"github.com/tidwall/sjson"
|
| 14 |
)
|
| 15 |
|
| 16 |
-
//
|
| 17 |
// It extracts the model name, system instruction, message contents, and tool declarations
|
| 18 |
// from the raw JSON request and returns them in the format expected by the Gemini API.
|
| 19 |
// The function performs the following transformations:
|
|
@@ -29,7 +30,11 @@ import (
|
|
| 29 |
//
|
| 30 |
// Returns:
|
| 31 |
// - []string: The transformed request data in Gemini API format
|
| 32 |
-
func
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
if alt, ok := ctx.Value("alt").(string); ok {
|
| 34 |
var chunk []byte
|
| 35 |
if alt == "" {
|
|
@@ -56,7 +61,7 @@ func ConvertGeminiCliRequestToGemini(ctx context.Context, _ string, originalRequ
|
|
| 56 |
return []string{}
|
| 57 |
}
|
| 58 |
|
| 59 |
-
//
|
| 60 |
// This function processes the complete Gemini CLI request and transforms it into a single Gemini-compatible
|
| 61 |
// JSON response. It extracts the response data from the request and returns it in the expected format.
|
| 62 |
//
|
|
@@ -68,7 +73,7 @@ func ConvertGeminiCliRequestToGemini(ctx context.Context, _ string, originalRequ
|
|
| 68 |
//
|
| 69 |
// Returns:
|
| 70 |
// - string: A Gemini-compatible JSON response containing the response data
|
| 71 |
-
func
|
| 72 |
responseResult := gjson.GetBytes(rawJSON, "response")
|
| 73 |
if responseResult.Exists() {
|
| 74 |
return responseResult.Raw
|
|
|
|
| 6 |
package gemini
|
| 7 |
|
| 8 |
import (
|
| 9 |
+
"bytes"
|
| 10 |
"context"
|
| 11 |
"fmt"
|
| 12 |
|
|
|
|
| 14 |
"github.com/tidwall/sjson"
|
| 15 |
)
|
| 16 |
|
| 17 |
+
// ConvertGeminiCliResponseToGemini parses and transforms a Gemini CLI API request into Gemini API format.
|
| 18 |
// It extracts the model name, system instruction, message contents, and tool declarations
|
| 19 |
// from the raw JSON request and returns them in the format expected by the Gemini API.
|
| 20 |
// The function performs the following transformations:
|
|
|
|
| 30 |
//
|
| 31 |
// Returns:
|
| 32 |
// - []string: The transformed request data in Gemini API format
|
| 33 |
+
func ConvertGeminiCliResponseToGemini(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string {
|
| 34 |
+
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
| 35 |
+
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
if alt, ok := ctx.Value("alt").(string); ok {
|
| 39 |
var chunk []byte
|
| 40 |
if alt == "" {
|
|
|
|
| 61 |
return []string{}
|
| 62 |
}
|
| 63 |
|
| 64 |
+
// ConvertGeminiCliResponseToGeminiNonStream converts a non-streaming Gemini CLI request to a non-streaming Gemini response.
|
| 65 |
// This function processes the complete Gemini CLI request and transforms it into a single Gemini-compatible
|
| 66 |
// JSON response. It extracts the response data from the request and returns it in the expected format.
|
| 67 |
//
|
|
|
|
| 73 |
//
|
| 74 |
// Returns:
|
| 75 |
// - string: A Gemini-compatible JSON response containing the response data
|
| 76 |
+
func ConvertGeminiCliResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
| 77 |
responseResult := gjson.GetBytes(rawJSON, "response")
|
| 78 |
if responseResult.Exists() {
|
| 79 |
return responseResult.Raw
|
|
@@ -12,8 +12,8 @@ func init() {
|
|
| 12 |
GeminiCLI,
|
| 13 |
ConvertGeminiRequestToGeminiCLI,
|
| 14 |
interfaces.TranslateResponse{
|
| 15 |
-
Stream:
|
| 16 |
-
NonStream:
|
| 17 |
TokenCount: GeminiTokenCount,
|
| 18 |
},
|
| 19 |
)
|
|
|
|
| 12 |
GeminiCLI,
|
| 13 |
ConvertGeminiRequestToGeminiCLI,
|
| 14 |
interfaces.TranslateResponse{
|
| 15 |
+
Stream: ConvertGeminiCliResponseToGemini,
|
| 16 |
+
NonStream: ConvertGeminiCliResponseToGeminiNonStream,
|
| 17 |
TokenCount: GeminiTokenCount,
|
| 18 |
},
|
| 19 |
)
|
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package auth
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"fmt"
|
| 7 |
+
"net"
|
| 8 |
+
"net/http"
|
| 9 |
+
"net/url"
|
| 10 |
+
"strings"
|
| 11 |
+
"time"
|
| 12 |
+
|
| 13 |
+
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
| 14 |
+
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
| 15 |
+
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
| 16 |
+
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
| 17 |
+
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
| 18 |
+
log "github.com/sirupsen/logrus"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
const (
|
| 22 |
+
antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
| 23 |
+
antigravityClientSecret = "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"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
var antigravityScopes = []string{
|
| 27 |
+
"https://www.googleapis.com/auth/cloud-platform",
|
| 28 |
+
"https://www.googleapis.com/auth/userinfo.email",
|
| 29 |
+
"https://www.googleapis.com/auth/userinfo.profile",
|
| 30 |
+
"https://www.googleapis.com/auth/cclog",
|
| 31 |
+
"https://www.googleapis.com/auth/experimentsandconfigs",
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// AntigravityAuthenticator implements OAuth login for the antigravity provider.
|
| 35 |
+
type AntigravityAuthenticator struct{}
|
| 36 |
+
|
| 37 |
+
// NewAntigravityAuthenticator constructs a new authenticator instance.
|
| 38 |
+
func NewAntigravityAuthenticator() Authenticator { return &AntigravityAuthenticator{} }
|
| 39 |
+
|
| 40 |
+
// Provider returns the provider key for antigravity.
|
| 41 |
+
func (AntigravityAuthenticator) Provider() string { return "antigravity" }
|
| 42 |
+
|
| 43 |
+
// RefreshLead instructs the manager to refresh five minutes before expiry.
|
| 44 |
+
func (AntigravityAuthenticator) RefreshLead() *time.Duration {
|
| 45 |
+
lead := 5 * time.Minute
|
| 46 |
+
return &lead
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Login launches a local OAuth flow to obtain antigravity tokens and persists them.
|
| 50 |
+
func (AntigravityAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
| 51 |
+
if cfg == nil {
|
| 52 |
+
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
| 53 |
+
}
|
| 54 |
+
if ctx == nil {
|
| 55 |
+
ctx = context.Background()
|
| 56 |
+
}
|
| 57 |
+
if opts == nil {
|
| 58 |
+
opts = &LoginOptions{}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
state, err := misc.GenerateRandomState()
|
| 62 |
+
if err != nil {
|
| 63 |
+
return nil, fmt.Errorf("antigravity: failed to generate state: %w", err)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
srv, port, cbChan, errServer := startAntigravityCallbackServer()
|
| 67 |
+
if errServer != nil {
|
| 68 |
+
return nil, fmt.Errorf("antigravity: failed to start callback server: %w", errServer)
|
| 69 |
+
}
|
| 70 |
+
defer func() {
|
| 71 |
+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
| 72 |
+
defer cancel()
|
| 73 |
+
_ = srv.Shutdown(shutdownCtx)
|
| 74 |
+
}()
|
| 75 |
+
|
| 76 |
+
redirectURI := fmt.Sprintf("http://localhost:%d/oauth-callback", port)
|
| 77 |
+
authURL := buildAntigravityAuthURL(redirectURI, state)
|
| 78 |
+
|
| 79 |
+
if !opts.NoBrowser {
|
| 80 |
+
fmt.Println("Opening browser for antigravity authentication")
|
| 81 |
+
if !browser.IsAvailable() {
|
| 82 |
+
log.Warn("No browser available; please open the URL manually")
|
| 83 |
+
util.PrintSSHTunnelInstructions(port)
|
| 84 |
+
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
|
| 85 |
+
} else if errOpen := browser.OpenURL(authURL); errOpen != nil {
|
| 86 |
+
log.Warnf("Failed to open browser automatically: %v", errOpen)
|
| 87 |
+
util.PrintSSHTunnelInstructions(port)
|
| 88 |
+
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
|
| 89 |
+
}
|
| 90 |
+
} else {
|
| 91 |
+
util.PrintSSHTunnelInstructions(port)
|
| 92 |
+
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
fmt.Println("Waiting for antigravity authentication callback...")
|
| 96 |
+
|
| 97 |
+
var cbRes callbackResult
|
| 98 |
+
select {
|
| 99 |
+
case res := <-cbChan:
|
| 100 |
+
cbRes = res
|
| 101 |
+
case <-time.After(5 * time.Minute):
|
| 102 |
+
return nil, fmt.Errorf("antigravity: authentication timed out")
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
if cbRes.Error != "" {
|
| 106 |
+
return nil, fmt.Errorf("antigravity: authentication failed: %s", cbRes.Error)
|
| 107 |
+
}
|
| 108 |
+
if cbRes.State != state {
|
| 109 |
+
return nil, fmt.Errorf("antigravity: invalid state")
|
| 110 |
+
}
|
| 111 |
+
if cbRes.Code == "" {
|
| 112 |
+
return nil, fmt.Errorf("antigravity: missing authorization code")
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
tokenResp, errToken := exchangeAntigravityCode(ctx, cbRes.Code, redirectURI)
|
| 116 |
+
if errToken != nil {
|
| 117 |
+
return nil, fmt.Errorf("antigravity: token exchange failed: %w", errToken)
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
email := ""
|
| 121 |
+
if tokenResp.AccessToken != "" {
|
| 122 |
+
if info, errInfo := fetchAntigravityUserInfo(ctx, tokenResp.AccessToken); errInfo == nil && strings.TrimSpace(info.Email) != "" {
|
| 123 |
+
email = strings.TrimSpace(info.Email)
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
now := time.Now()
|
| 128 |
+
metadata := map[string]any{
|
| 129 |
+
"type": "antigravity",
|
| 130 |
+
"access_token": tokenResp.AccessToken,
|
| 131 |
+
"refresh_token": tokenResp.RefreshToken,
|
| 132 |
+
"expires_in": tokenResp.ExpiresIn,
|
| 133 |
+
"timestamp": now.UnixMilli(),
|
| 134 |
+
"expired": now.Add(time.Duration(tokenResp.ExpiresIn) * time.Second).Format(time.RFC3339),
|
| 135 |
+
}
|
| 136 |
+
if email != "" {
|
| 137 |
+
metadata["email"] = email
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
fileName := sanitizeAntigravityFileName(email)
|
| 141 |
+
label := email
|
| 142 |
+
if label == "" {
|
| 143 |
+
label = "antigravity"
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
fmt.Println("Antigravity authentication successful")
|
| 147 |
+
return &coreauth.Auth{
|
| 148 |
+
ID: fileName,
|
| 149 |
+
Provider: "antigravity",
|
| 150 |
+
FileName: fileName,
|
| 151 |
+
Label: label,
|
| 152 |
+
Metadata: metadata,
|
| 153 |
+
}, nil
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
type callbackResult struct {
|
| 157 |
+
Code string
|
| 158 |
+
Error string
|
| 159 |
+
State string
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
func startAntigravityCallbackServer() (*http.Server, int, <-chan callbackResult, error) {
|
| 163 |
+
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
| 164 |
+
if err != nil {
|
| 165 |
+
return nil, 0, nil, err
|
| 166 |
+
}
|
| 167 |
+
port := listener.Addr().(*net.TCPAddr).Port
|
| 168 |
+
resultCh := make(chan callbackResult, 1)
|
| 169 |
+
|
| 170 |
+
mux := http.NewServeMux()
|
| 171 |
+
mux.HandleFunc("/oauth-callback", func(w http.ResponseWriter, r *http.Request) {
|
| 172 |
+
q := r.URL.Query()
|
| 173 |
+
res := callbackResult{
|
| 174 |
+
Code: strings.TrimSpace(q.Get("code")),
|
| 175 |
+
Error: strings.TrimSpace(q.Get("error")),
|
| 176 |
+
State: strings.TrimSpace(q.Get("state")),
|
| 177 |
+
}
|
| 178 |
+
resultCh <- res
|
| 179 |
+
if res.Code != "" && res.Error == "" {
|
| 180 |
+
_, _ = w.Write([]byte("<h1>Login successful</h1><p>You can close this window.</p>"))
|
| 181 |
+
} else {
|
| 182 |
+
_, _ = w.Write([]byte("<h1>Login failed</h1><p>Please check the CLI output.</p>"))
|
| 183 |
+
}
|
| 184 |
+
})
|
| 185 |
+
|
| 186 |
+
srv := &http.Server{Handler: mux}
|
| 187 |
+
go func() {
|
| 188 |
+
if errServe := srv.Serve(listener); errServe != nil && !strings.Contains(errServe.Error(), "Server closed") {
|
| 189 |
+
log.Warnf("antigravity callback server error: %v", errServe)
|
| 190 |
+
}
|
| 191 |
+
}()
|
| 192 |
+
|
| 193 |
+
return srv, port, resultCh, nil
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
type antigravityTokenResponse struct {
|
| 197 |
+
AccessToken string `json:"access_token"`
|
| 198 |
+
RefreshToken string `json:"refresh_token"`
|
| 199 |
+
ExpiresIn int64 `json:"expires_in"`
|
| 200 |
+
TokenType string `json:"token_type"`
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
func exchangeAntigravityCode(ctx context.Context, code, redirectURI string) (*antigravityTokenResponse, error) {
|
| 204 |
+
data := url.Values{}
|
| 205 |
+
data.Set("code", code)
|
| 206 |
+
data.Set("client_id", antigravityClientID)
|
| 207 |
+
data.Set("client_secret", antigravityClientSecret)
|
| 208 |
+
data.Set("redirect_uri", redirectURI)
|
| 209 |
+
data.Set("grant_type", "authorization_code")
|
| 210 |
+
|
| 211 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://oauth2.googleapis.com/token", strings.NewReader(data.Encode()))
|
| 212 |
+
if err != nil {
|
| 213 |
+
return nil, err
|
| 214 |
+
}
|
| 215 |
+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
| 216 |
+
|
| 217 |
+
resp, errDo := http.DefaultClient.Do(req)
|
| 218 |
+
if errDo != nil {
|
| 219 |
+
return nil, errDo
|
| 220 |
+
}
|
| 221 |
+
defer func() {
|
| 222 |
+
if errClose := resp.Body.Close(); errClose != nil {
|
| 223 |
+
log.Errorf("antigravity token exchange: close body error: %v", errClose)
|
| 224 |
+
}
|
| 225 |
+
}()
|
| 226 |
+
|
| 227 |
+
var token antigravityTokenResponse
|
| 228 |
+
if errDecode := json.NewDecoder(resp.Body).Decode(&token); errDecode != nil {
|
| 229 |
+
return nil, errDecode
|
| 230 |
+
}
|
| 231 |
+
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
| 232 |
+
return nil, fmt.Errorf("oauth token exchange failed: status %d", resp.StatusCode)
|
| 233 |
+
}
|
| 234 |
+
return &token, nil
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
type antigravityUserInfo struct {
|
| 238 |
+
Email string `json:"email"`
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
func fetchAntigravityUserInfo(ctx context.Context, accessToken string) (*antigravityUserInfo, error) {
|
| 242 |
+
if strings.TrimSpace(accessToken) == "" {
|
| 243 |
+
return &antigravityUserInfo{}, nil
|
| 244 |
+
}
|
| 245 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", nil)
|
| 246 |
+
if err != nil {
|
| 247 |
+
return nil, err
|
| 248 |
+
}
|
| 249 |
+
req.Header.Set("Authorization", "Bearer "+accessToken)
|
| 250 |
+
|
| 251 |
+
resp, errDo := http.DefaultClient.Do(req)
|
| 252 |
+
if errDo != nil {
|
| 253 |
+
return nil, errDo
|
| 254 |
+
}
|
| 255 |
+
defer func() {
|
| 256 |
+
if errClose := resp.Body.Close(); errClose != nil {
|
| 257 |
+
log.Errorf("antigravity userinfo: close body error: %v", errClose)
|
| 258 |
+
}
|
| 259 |
+
}()
|
| 260 |
+
|
| 261 |
+
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
| 262 |
+
return &antigravityUserInfo{}, nil
|
| 263 |
+
}
|
| 264 |
+
var info antigravityUserInfo
|
| 265 |
+
if errDecode := json.NewDecoder(resp.Body).Decode(&info); errDecode != nil {
|
| 266 |
+
return nil, errDecode
|
| 267 |
+
}
|
| 268 |
+
return &info, nil
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
func buildAntigravityAuthURL(redirectURI, state string) string {
|
| 272 |
+
params := url.Values{}
|
| 273 |
+
params.Set("access_type", "offline")
|
| 274 |
+
params.Set("client_id", antigravityClientID)
|
| 275 |
+
params.Set("prompt", "consent")
|
| 276 |
+
params.Set("redirect_uri", redirectURI)
|
| 277 |
+
params.Set("response_type", "code")
|
| 278 |
+
params.Set("scope", strings.Join(antigravityScopes, " "))
|
| 279 |
+
params.Set("state", state)
|
| 280 |
+
return "https://accounts.google.com/o/oauth2/v2/auth?" + params.Encode()
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
func sanitizeAntigravityFileName(email string) string {
|
| 284 |
+
if strings.TrimSpace(email) == "" {
|
| 285 |
+
return "antigravity.json"
|
| 286 |
+
}
|
| 287 |
+
replacer := strings.NewReplacer("@", "_", ".", "_")
|
| 288 |
+
return fmt.Sprintf("antigravity-%s.json", replacer.Replace(email))
|
| 289 |
+
}
|
|
@@ -13,6 +13,7 @@ func init() {
|
|
| 13 |
registerRefreshLead("iflow", func() Authenticator { return NewIFlowAuthenticator() })
|
| 14 |
registerRefreshLead("gemini", func() Authenticator { return NewGeminiAuthenticator() })
|
| 15 |
registerRefreshLead("gemini-cli", func() Authenticator { return NewGeminiAuthenticator() })
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
func registerRefreshLead(provider string, factory func() Authenticator) {
|
|
|
|
| 13 |
registerRefreshLead("iflow", func() Authenticator { return NewIFlowAuthenticator() })
|
| 14 |
registerRefreshLead("gemini", func() Authenticator { return NewGeminiAuthenticator() })
|
| 15 |
registerRefreshLead("gemini-cli", func() Authenticator { return NewGeminiAuthenticator() })
|
| 16 |
+
registerRefreshLead("antigravity", func() Authenticator { return NewAntigravityAuthenticator() })
|
| 17 |
}
|
| 18 |
|
| 19 |
func registerRefreshLead(provider string, factory func() Authenticator) {
|
|
@@ -333,6 +333,8 @@ func (s *Service) ensureExecutorsForAuth(a *coreauth.Auth) {
|
|
| 333 |
s.coreManager.RegisterExecutor(executor.NewAIStudioExecutor(s.cfg, a.ID, s.wsGateway))
|
| 334 |
}
|
| 335 |
return
|
|
|
|
|
|
|
| 336 |
case "claude":
|
| 337 |
s.coreManager.RegisterExecutor(executor.NewClaudeExecutor(s.cfg))
|
| 338 |
case "codex":
|
|
@@ -634,6 +636,10 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
|
|
| 634 |
models = registry.GetGeminiCLIModels()
|
| 635 |
case "aistudio":
|
| 636 |
models = registry.GetAIStudioModels()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
case "claude":
|
| 638 |
models = registry.GetClaudeModels()
|
| 639 |
if entry := s.resolveConfigClaudeKey(a); entry != nil && len(entry.Models) > 0 {
|
|
|
|
| 333 |
s.coreManager.RegisterExecutor(executor.NewAIStudioExecutor(s.cfg, a.ID, s.wsGateway))
|
| 334 |
}
|
| 335 |
return
|
| 336 |
+
case "antigravity":
|
| 337 |
+
s.coreManager.RegisterExecutor(executor.NewAntigravityExecutor(s.cfg))
|
| 338 |
case "claude":
|
| 339 |
s.coreManager.RegisterExecutor(executor.NewClaudeExecutor(s.cfg))
|
| 340 |
case "codex":
|
|
|
|
| 636 |
models = registry.GetGeminiCLIModels()
|
| 637 |
case "aistudio":
|
| 638 |
models = registry.GetAIStudioModels()
|
| 639 |
+
case "antigravity":
|
| 640 |
+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
| 641 |
+
models = executor.FetchAntigravityModels(ctx, a, s.cfg)
|
| 642 |
+
cancel()
|
| 643 |
case "claude":
|
| 644 |
models = registry.GetClaudeModels()
|
| 645 |
if entry := s.resolveConfigClaudeKey(a); entry != nil && len(entry.Models) > 0 {
|
|
@@ -8,4 +8,5 @@ const (
|
|
| 8 |
FormatGemini Format = "gemini"
|
| 9 |
FormatGeminiCLI Format = "gemini-cli"
|
| 10 |
FormatCodex Format = "codex"
|
|
|
|
| 11 |
)
|
|
|
|
| 8 |
FormatGemini Format = "gemini"
|
| 9 |
FormatGeminiCLI Format = "gemini-cli"
|
| 10 |
FormatCodex Format = "codex"
|
| 11 |
+
FormatAntigravity Format = "antigravity"
|
| 12 |
)
|