Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions cmd/capi/antigravity.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,34 @@ func isAntigravityAccount(account OpenAIAccount) bool {
return isAntigravitySource(account.Source)
}

// isAntigravityChannel reports whether a channel is a first-class Antigravity
// account pool (provider == "antigravity"), which routes through the Google
// OAuth + Cloud Code path rather than the codex path.
func isAntigravityChannel(channel Channel) bool {
return isAntigravitySource(channel.Provider)
}

// ensureAntigravityChannelLocked keeps an Antigravity channel's provider stable
// and registers its model catalog. It mirrors ensureCodexChannelLocked but for
// the Antigravity provider, so account import / periodic upkeep never rewrites
// it to codex. Callers must hold s.mu.
func (s *Server) ensureAntigravityChannelLocked(channel *Channel) bool {
if channel == nil {
return false
}
changed := false
if !strings.EqualFold(strings.TrimSpace(channel.Provider), "antigravity") {
channel.Provider = "antigravity"
changed = true
}
for _, modelID := range antigravityModelIDs() {
if s.ensureImportedModelLocked(modelID) {
changed = true
}
}
return changed
}

// checkAntigravityAccount validates an Antigravity account for the batch/health
// check. It refreshes the Google token when needed and warms the project cache;
// only a failed token refresh ejects the account, so a transient loadCodeAssist
Expand Down
26 changes: 26 additions & 0 deletions cmd/capi/antigravity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"encoding/json"
"strings"
"testing"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -153,3 +154,28 @@ func TestParseDataURL(t *testing.T) {
t.Fatalf("non-base64 data URL should not parse")
}
}

func TestAntigravityChannelIsNotCodex(t *testing.T) {
// A first-class Antigravity pool must route through the antigravity path,
// never the codex path, even once it holds accounts.
channel := Channel{Provider: "antigravity", OpenAIAccounts: []OpenAIAccount{{ID: "a", AccessToken: "x", Source: "antigravity"}}}
if !isAntigravityChannel(channel) {
t.Fatalf("expected antigravity channel")
}
if isCodexChannel(channel) {
t.Fatalf("antigravity channel must not be treated as codex")
}
codex := Channel{Provider: "codex", OpenAIAccounts: []OpenAIAccount{{ID: "b", AccessToken: "x"}}}
if isAntigravityChannel(codex) || !isCodexChannel(codex) {
t.Fatalf("codex channel classification regressed")
}
}

func TestAntigravityClientSecretDecodes(t *testing.T) {
if antigravityOAuthClientSecret == "" {
t.Fatalf("client secret failed to decode")
}
if strings.ContainsAny(antigravityOAuthClientSecret, "\x00") {
t.Fatalf("client secret decoded to garbage")
}
}
26 changes: 25 additions & 1 deletion cmd/capi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3200,6 +3200,11 @@ func (s *Server) importOpenAIAccounts(c *gin.Context) {
}
}
for _, account := range accounts {
// Accounts imported into a first-class Antigravity pool are Antigravity
// accounts regardless of the file's declared source.
if isAntigravityChannel(*channel) && !isAntigravitySource(account.Source) {
account.Source = "antigravity"
}
if strings.TrimSpace(account.AccessToken) == "" && strings.TrimSpace(account.SessionToken) != "" {
account.AccessToken, account.ExpiresAt, _ = fetchChatGPTAccessTokenViaSessionCookie(account.SessionToken)
}
Expand Down Expand Up @@ -6934,6 +6939,9 @@ func chatCompletionUsageFromCodex(usage gin.H, messages []ChatMessage) gin.H {
}

func (s *Server) fetchUpstreamModelIDs(channel Channel, upstreamKey string) ([]string, error) {
if isAntigravityChannel(channel) {
return antigravityModelIDs(), nil
}
if isCodexChannel(channel) {
return codexChannelModelIDs(), nil
}
Expand Down Expand Up @@ -7934,6 +7942,9 @@ func (s *Server) shouldUseCompatibleProvider(channel Channel) bool {
if strings.EqualFold(s.providerMode, "compatible") {
return true
}
if isAntigravityChannel(channel) && len(channel.OpenAIAccounts) > 0 {
return true
}
if isCodexChannel(channel) && len(channel.OpenAIAccounts) > 0 {
return true
}
Expand Down Expand Up @@ -9177,13 +9188,20 @@ func codexChannelModelIDs() []string {
}

func isCodexChannel(channel Channel) bool {
if isAntigravityChannel(channel) {
return false
}
return strings.EqualFold(strings.TrimSpace(channel.Provider), "codex") || len(channel.OpenAIAccounts) > 0
}

func (s *Server) ensureCodexChannelLocked(channel *Channel) bool {
if channel == nil {
return false
}
// An Antigravity account pool is its own provider; never rewrite it to codex.
if isAntigravityChannel(*channel) {
return s.ensureAntigravityChannelLocked(channel)
}
changed := false
if !strings.EqualFold(strings.TrimSpace(channel.Provider), "codex") {
channel.Provider = "codex"
Expand Down Expand Up @@ -9268,6 +9286,8 @@ func providerLabel(provider string) string {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "codex":
return "Codex"
case "antigravity", "agy", "google-antigravity":
return "Antigravity"
case "cpa", "cliproxyapi", "cli-proxy-api":
return "CLIProxyAPI"
case "openai":
Expand Down Expand Up @@ -10572,7 +10592,11 @@ func (s *Server) normalizeStateCollections() bool {
s.state.Channels[i].Models = []string{}
changed = true
}
if isCodexChannel(s.state.Channels[i]) {
if isAntigravityChannel(s.state.Channels[i]) {
if s.ensureAntigravityChannelLocked(&s.state.Channels[i]) {
changed = true
}
} else if isCodexChannel(s.state.Channels[i]) {
if s.ensureCodexChannelLocked(&s.state.Channels[i]) {
changed = true
}
Expand Down
16 changes: 8 additions & 8 deletions dist/assets/index-B2BgeCFL.js → dist/assets/index-B9-tgVyd.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>CAPI</title>
<script type="module" crossorigin src="/assets/index-B2BgeCFL.js"></script>
<script type="module" crossorigin src="/assets/index-B9-tgVyd.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-699x73P2.css">
</head>
<body>
Expand Down
15 changes: 14 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ const navItems = [
const providerOptions = [
{ value: "kiro", label: "Kiro / Amazon Q" },
{ value: "codex", label: "Codex / ChatGPT OAuth" },
{ value: "antigravity", label: "Antigravity / Google" },
{ value: "cpa", label: "CPA / CLIProxyAPI" },
{ value: "openai", label: "OpenAI" },
{ value: "anthropic", label: "Anthropic / Claude" },
Expand All @@ -466,6 +467,8 @@ const defaultCodexModels = "gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, g
const defaultOpenAIModels = "gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4, gpt-image-2";
const defaultCPAModels = "gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4, claude-sonnet-4, gemini-3.1-pro";
const defaultKiroModels = "claude-sonnet-4.5, claude-sonnet-4, claude-haiku-4.5, claude-opus-4.5";
const defaultAntigravityBaseURL = "https://daily-cloudcode-pa.googleapis.com";
const defaultAntigravityModels = "claude-opus-4-6-thinking, claude-sonnet-4-6, gemini-3.8-flash-high, gemini-3.7-flash-high, gemini-3.6-flash-high, gemini-3-flash, gemini-pro-agent, gemini-3.1-pro-low, gpt-oss-120b-medium, gemini-3.1-flash-lite, gemini-3.5-flash-lite, gemini-3.1-flash-image";

const channelTemplates = [
{
Expand All @@ -489,6 +492,13 @@ const channelTemplates = [
baseUrl: defaultCodexBaseURL,
models: defaultCodexModels.split(",").map((model) => model.trim())
},
{
provider: "antigravity",
label: "Antigravity 账号池",
name: "Antigravity 账号池",
baseUrl: defaultAntigravityBaseURL,
models: defaultAntigravityModels.split(",").map((model) => model.trim())
},
{
provider: "cpa",
label: "CPA / CLIProxyAPI",
Expand Down Expand Up @@ -563,6 +573,7 @@ function accountErrorLabel(code?: string) {
function defaultBaseURLForProvider(provider: string) {
if (provider === "openai") return defaultOpenAIBaseURL;
if (provider === "codex") return defaultCodexBaseURL;
if (provider === "antigravity") return defaultAntigravityBaseURL;
if (provider === "cpa" || provider === "cliproxyapi") return defaultCPABaseURL;
if (provider === "kiro") return defaultKiroBaseURL;
return "";
Expand Down Expand Up @@ -733,6 +744,7 @@ function ProviderIcon({ provider }: { provider: string }) {
);
}
const mark = provider === "codex" ? "C"
: provider === "antigravity" ? "G"
: provider === "cpa" || provider === "cliproxyapi" ? "CPA"
: provider === "anthropic" ? "A"
: provider === "google" ? "✦"
Expand Down Expand Up @@ -3054,7 +3066,7 @@ function DrawingView({
onStartOAuth: (channelId: string) => Promise<{ authorizeUrl: string; state: string; redirectUri: string }>;
onCompleteOAuth: (channelId: string, payload: { callbackUrl?: string; code?: string; state?: string }) => Promise<unknown>;
}) {
const drawingChannels = channels.filter((channel) => channel.provider === "codex" || channel.provider === "openai" || arrayOf(channel.models).some((model) => model.includes("image")));
const drawingChannels = channels.filter((channel) => channel.provider === "codex" || channel.provider === "antigravity" || channel.provider === "openai" || arrayOf(channel.models).some((model) => model.includes("image")));
const [busy, setBusy] = useState("");
const [accountVisibleCounts, setAccountVisibleCounts] = useState<Record<string, number>>({});
const [accountFilters, setAccountFilters] = useState<Record<string, "all" | "attention" | "invalid" | "error" | "refreshable">>({});
Expand Down Expand Up @@ -3151,6 +3163,7 @@ function DrawingView({
<div className="panel-toolbar">
<span className="muted-inline">账号有两种来源,任选其一即可。</span>
<button className="primary-button" onClick={() => onCreate(channelCreateFromTemplate("codex"))}>新增账号池渠道</button>
<button className="secondary-button" onClick={() => onCreate(channelCreateFromTemplate("antigravity"))}>新增 Antigravity 账号池</button>
</div>
<div className="source-guide">
<div className="source-guide-item">
Expand Down
Loading