luispater commited on
Commit
d53a4c0
·
unverified ·
1 Parent(s): 712a84b

**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 CHANGED
@@ -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)
internal/cmd/antigravity_login.go ADDED
@@ -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
+ }
internal/cmd/auth_manager.go CHANGED
@@ -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
  }
internal/runtime/executor/antigravity_executor.go ADDED
@@ -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, &param)
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), &param)
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]"), &param)
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, &registry.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
+ }
internal/translator/gemini-cli/gemini/{gemini_gemini-cli_request.go → gemini-cli_gemini_response.go} RENAMED
@@ -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
- // ConvertGeminiCliRequestToGemini parses and transforms a Gemini CLI API request into Gemini API format.
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 ConvertGeminiCliRequestToGemini(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string {
 
 
 
 
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
- // ConvertGeminiCliRequestToGeminiNonStream converts a non-streaming Gemini CLI request to a non-streaming Gemini response.
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 ConvertGeminiCliRequestToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
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
internal/translator/gemini-cli/gemini/init.go CHANGED
@@ -12,8 +12,8 @@ func init() {
12
  GeminiCLI,
13
  ConvertGeminiRequestToGeminiCLI,
14
  interfaces.TranslateResponse{
15
- Stream: ConvertGeminiCliRequestToGemini,
16
- NonStream: ConvertGeminiCliRequestToGeminiNonStream,
17
  TokenCount: GeminiTokenCount,
18
  },
19
  )
 
12
  GeminiCLI,
13
  ConvertGeminiRequestToGeminiCLI,
14
  interfaces.TranslateResponse{
15
+ Stream: ConvertGeminiCliResponseToGemini,
16
+ NonStream: ConvertGeminiCliResponseToGeminiNonStream,
17
  TokenCount: GeminiTokenCount,
18
  },
19
  )
sdk/auth/antigravity.go ADDED
@@ -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
+ }
sdk/auth/refresh_registry.go CHANGED
@@ -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) {
sdk/cliproxy/service.go CHANGED
@@ -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 {
sdk/translator/formats.go CHANGED
@@ -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
  )