Skip to content

Commit 77e4659

Browse files
committed
feat(providers): add Google Antigravity OAuth subscription provider
Adds a native Google Antigravity OAuth channel (`ChannelAuth::AntigravityOAuth`, auth.toml key "google-antigravity") alongside the existing xAI / ChatGPT / Copilot subscription providers, exposing Google-native models over Google REST. - neenee-core: new `AntigravityOAuth` variant wired into `is_oauth`, `oauth_provider_id`, and `default_login_method` (browser flow). - neenee-providers: `GOOGLE_ANTIGRAVITY` OAuthConfig (public client id, offline-access consent scope, loopback callback) with an `antigravity-oauth` template spec; `OAuth::google_antigravity()` constructor. - neenee-agent: OAuth bearer resolution collapses the three per-provider match arms into a single `oauth_provider_id()`-driven lookup, so new OAuth channels no longer need bespoke code in catalog / discovery. - neenee-cli TUI: new `antigravity-oauth` provider template and Antigravity labels in the custom-auth / oauth-pending views.
1 parent ec387f1 commit 77e4659

9 files changed

Lines changed: 98 additions & 71 deletions

File tree

crates/neenee-agent/src/catalog.rs

Lines changed: 18 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -60,41 +60,18 @@ fn user_channel_to_channel(uc: &UserChannelConfig, fallback_model: &str) -> Chan
6060
// OAuth channels resolve their bearer from auth.toml. ChatGPT also yields
6161
// the chatgpt_account_id (carried on the Responses transport); xAI has none.
6262
// Activate/switch refreshes the token first (handlers_provider).
63-
let (api_key, account_id) = match uc.auth {
64-
neenee_core::ChannelAuth::ChatGptOAuth => {
65-
let store = neenee_providers::oauth::AuthStore::load();
66-
let tokens = store.get("chatgpt");
67-
(
68-
tokens.map(|t| t.access.clone()).unwrap_or_default(),
69-
tokens.and_then(|t| t.account_id.clone()),
70-
)
71-
}
72-
neenee_core::ChannelAuth::CopilotOAuth => {
73-
// Copilot's bearer is the GitHub OAuth access token; there is no
74-
// account id (unlike ChatGPT's chatgpt_account_id claim).
75-
let store = neenee_providers::oauth::AuthStore::load();
76-
(
77-
store
78-
.get("copilot")
79-
.map(|tokens| tokens.access.clone())
80-
.unwrap_or_default(),
81-
None,
82-
)
83-
}
84-
neenee_core::ChannelAuth::XaiOAuth => {
85-
let store = neenee_providers::oauth::AuthStore::load();
86-
(
87-
store
88-
.get("xai")
89-
.map(|tokens| tokens.access.clone())
90-
.unwrap_or_default(),
91-
None,
92-
)
93-
}
94-
neenee_core::ChannelAuth::ApiKey => (
63+
let (api_key, account_id) = if let Some(oauth_id) = uc.auth.oauth_provider_id() {
64+
let store = neenee_providers::oauth::AuthStore::load();
65+
let tokens = store.get(oauth_id);
66+
(
67+
tokens.map(|t| t.access.clone()).unwrap_or_default(),
68+
tokens.and_then(|t| t.account_id.clone()),
69+
)
70+
} else {
71+
(
9572
env_or_config(uc.api_key_env.as_deref(), uc.api_key.clone()).unwrap_or_default(),
9673
None,
97-
),
74+
)
9875
};
9976
let model = uc
10077
.model
@@ -657,29 +634,14 @@ pub async fn discover_provider_models(config: &mut Config) -> DiscoveryOutcome {
657634
// OAuth auth modes; API-key channels keep using the stored key.
658635
let resolved_bearer: SecretString;
659636
let no_key = SecretString::default();
660-
let api_key: &SecretString = match channel.auth {
661-
neenee_core::ChannelAuth::ApiKey => channel.api_key.as_ref().unwrap_or(&no_key),
662-
neenee_core::ChannelAuth::CopilotOAuth => {
663-
resolved_bearer = neenee_providers::oauth::AuthStore::load()
664-
.get("copilot")
665-
.map(|tokens| tokens.access.clone())
666-
.unwrap_or_default();
667-
&resolved_bearer
668-
}
669-
neenee_core::ChannelAuth::ChatGptOAuth => {
670-
resolved_bearer = neenee_providers::oauth::AuthStore::load()
671-
.get("chatgpt")
672-
.map(|tokens| tokens.access.clone())
673-
.unwrap_or_default();
674-
&resolved_bearer
675-
}
676-
neenee_core::ChannelAuth::XaiOAuth => {
677-
resolved_bearer = neenee_providers::oauth::AuthStore::load()
678-
.get("xai")
679-
.map(|tokens| tokens.access.clone())
680-
.unwrap_or_default();
681-
&resolved_bearer
682-
}
637+
let api_key: &SecretString = if let Some(oauth_id) = channel.auth.oauth_provider_id() {
638+
resolved_bearer = neenee_providers::oauth::AuthStore::load()
639+
.get(oauth_id)
640+
.map(|tokens| tokens.access.clone())
641+
.unwrap_or_default();
642+
&resolved_bearer
643+
} else {
644+
channel.api_key.as_ref().unwrap_or(&no_key)
683645
};
684646
let user_agent = channel.user_agent.as_deref();
685647
let protocol = neenee_providers::DiscoveryProtocol::from_template_protocol(spec.protocol);

crates/neenee-cli/src/tui/app.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2157,6 +2157,7 @@ impl App {
21572157
let default_name = match self.custom_auth {
21582158
neenee_core::ChannelAuth::ChatGptOAuth => "ChatGPT",
21592159
neenee_core::ChannelAuth::CopilotOAuth => "Copilot",
2160+
neenee_core::ChannelAuth::AntigravityOAuth => "Google Antigravity",
21602161
_ => "xAI",
21612162
};
21622163
self.custom_name = default_name.to_string();

crates/neenee-cli/src/tui/event_loop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2090,6 +2090,7 @@ pub(super) async fn run_app_loop(
20902090
neenee_core::ChannelAuth::ChatGptOAuth => "ChatGPT",
20912091
neenee_core::ChannelAuth::CopilotOAuth => "Copilot",
20922092
neenee_core::ChannelAuth::XaiOAuth => "xAI",
2093+
neenee_core::ChannelAuth::AntigravityOAuth => "Google Antigravity",
20932094
neenee_core::ChannelAuth::ApiKey => "OAuth",
20942095
};
20952096
Some(view::draw_oauth_pending(

crates/neenee-cli/src/tui/providers.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,19 @@ pub const PROVIDER_TEMPLATES: &[ProviderTemplate] = &[
215215
user_agent: None,
216216
auth: neenee_core::ChannelAuth::CopilotOAuth,
217217
},
218+
ProviderTemplate {
219+
id: "antigravity-oauth",
220+
label: "Google Antigravity OAuth",
221+
description: "Gemini 3.x / Antigravity models via Google OAuth subscription",
222+
protocol: "google",
223+
models: neenee_providers::GOOGLE_BUILTIN_MODELS,
224+
needs_url: false,
225+
url_hint: "https://generativelanguage.googleapis.com/v1beta",
226+
needs_model: false,
227+
default_url: Some("https://generativelanguage.googleapis.com/v1beta"),
228+
user_agent: None,
229+
auth: neenee_core::ChannelAuth::AntigravityOAuth,
230+
},
218231
ProviderTemplate {
219232
id: "kimi-code",
220233
label: "Kimi Code",
@@ -789,6 +802,7 @@ mod tests {
789802
"xAI OAuth",
790803
"ChatGPT OAuth",
791804
"Copilot OAuth",
805+
"Google Antigravity OAuth",
792806
"Kimi Code",
793807
"ZAI Code",
794808
"OpenCode Go",

crates/neenee-core/src/channelauth.rs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,12 @@ pub enum ChannelAuth {
2929
ChatGptOAuth,
3030
/// GitHub Copilot subscription: resolve the live OAuth access token from
3131
/// `auth.toml` (key `"copilot"`) and route inference to the Copilot
32-
/// Responses backend (`https://api.githubcopilot.com/responses`). The token
33-
/// is the GitHub OAuth access token from the RFC 8628 device flow; it does
34-
/// not expire on a schedule, so the refresh path is a no-op until the user
35-
/// revokes the app. Copilot-specific request headers
36-
/// (`x-initiator`, `Openai-Intent`, `X-GitHub-Api-Version`) are injected by
37-
/// the Responses provider when it detects this auth mode.
32+
/// Responses backend (`https://api.githubcopilot.com/responses`).
3833
CopilotOAuth,
34+
/// Google Antigravity subscription: resolve the live OAuth access token from
35+
/// `auth.toml` (key `"google-antigravity"`), refreshed at activate/switch
36+
/// time. Connects Google Antigravity models over native Google REST.
37+
AntigravityOAuth,
3938
}
4039

4140
impl ChannelAuth {
@@ -44,7 +43,10 @@ impl ChannelAuth {
4443
pub fn is_oauth(self) -> bool {
4544
matches!(
4645
self,
47-
ChannelAuth::XaiOAuth | ChannelAuth::ChatGptOAuth | ChannelAuth::CopilotOAuth
46+
ChannelAuth::XaiOAuth
47+
| ChannelAuth::ChatGptOAuth
48+
| ChannelAuth::CopilotOAuth
49+
| ChannelAuth::AntigravityOAuth
4850
)
4951
}
5052

@@ -55,23 +57,20 @@ impl ChannelAuth {
5557
ChannelAuth::XaiOAuth => Some("xai"),
5658
ChannelAuth::ChatGptOAuth => Some("chatgpt"),
5759
ChannelAuth::CopilotOAuth => Some("copilot"),
60+
ChannelAuth::AntigravityOAuth => Some("google-antigravity"),
5861
ChannelAuth::ApiKey => None,
5962
}
6063
}
6164

62-
/// The default login flow for this OAuth provider. **Device flow is the
63-
/// default** for every subscription provider: it works headless (SSH, VPS,
64-
/// Docker) and needs no registered browser callback URL, so it cannot hit
65-
/// "redirect_uri is not associated with this application". The browser flow
66-
/// remains available as an opt-in only when the provider's callback is
67-
/// registered and the user is on a desktop with a reachable loopback port.
65+
/// The default login flow for this OAuth provider.
6866
///
6967
/// Returns `None` for API-key channels (no OAuth login to run).
7068
pub fn default_login_method(self) -> Option<LoginMethod> {
7169
match self {
7270
ChannelAuth::XaiOAuth | ChannelAuth::ChatGptOAuth | ChannelAuth::CopilotOAuth => {
7371
Some(LoginMethod::Device)
7472
}
73+
ChannelAuth::AntigravityOAuth => Some(LoginMethod::Browser),
7574
ChannelAuth::ApiKey => None,
7675
}
7776
}

crates/neenee-providers/src/oauth/config.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,34 @@ pub const COPILOT: OAuthConfig = OAuthConfig {
170170
device_redirect_uri: "",
171171
};
172172

173+
/// Google Antigravity OAuth client config. Public Client ID registered for Google Antigravity / Cloud SDK.
174+
pub const GOOGLE_ANTIGRAVITY: OAuthConfig = OAuthConfig {
175+
provider_id: "google-antigravity",
176+
client_id: "1070200057404-36h00infrjh2h81p4g0t47a98v1a21qg.apps.googleusercontent.com",
177+
authorize_url: "https://accounts.google.com/o/oauth2/v2/auth",
178+
token_url: "https://oauth2.googleapis.com/token",
179+
device_authorization_url: "https://oauth2.googleapis.com/device/code",
180+
grant_type_device: "urn:ietf:params:oauth:grant-type:device_code",
181+
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email openid profile offline_access",
182+
extra_authorize_params: &[("access_type", "offline"), ("prompt", "consent")],
183+
oauth_host: "127.0.0.1",
184+
oauth_port: 51121,
185+
oauth_path: "/oauth/callback",
186+
redirect_host: "127.0.0.1",
187+
send_nonce: false,
188+
device_flow: DeviceFlow::Rfc8628,
189+
device_token_url: "https://oauth2.googleapis.com/token",
190+
device_redirect_uri: "",
191+
};
192+
173193
/// Resolve a config by its `auth.toml` provider-id key (`"xai"` / `"chatgpt"`
174-
/// / `"copilot"`).
194+
/// / `"copilot"` / `"google-antigravity"`).
175195
pub fn config_by_provider_id(id: &str) -> Option<&'static OAuthConfig> {
176196
match id {
177197
"xai" => Some(&XAI),
178198
"chatgpt" => Some(&CHATGPT),
179199
"copilot" => Some(&COPILOT),
200+
"google-antigravity" | "antigravity" => Some(&GOOGLE_ANTIGRAVITY),
180201
_ => None,
181202
}
182203
}
@@ -216,6 +237,14 @@ mod tests {
216237
config_by_provider_id("copilot").unwrap().provider_id,
217238
"copilot"
218239
);
240+
assert_eq!(
241+
config_by_provider_id("google-antigravity").unwrap().provider_id,
242+
"google-antigravity"
243+
);
244+
assert_eq!(
245+
config_by_provider_id("antigravity").unwrap().provider_id,
246+
"google-antigravity"
247+
);
219248
assert!(config_by_provider_id("nope").is_none());
220249
}
221250

crates/neenee-providers/src/oauth/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub use chatgpt_device::{
3131
request_device_code as request_chatgpt_device_code,
3232
verification_url as chatgpt_verification_url,
3333
};
34-
pub use config::{CHATGPT, COPILOT, OAuthConfig, XAI, config_by_provider_id};
34+
pub use config::{CHATGPT, COPILOT, GOOGLE_ANTIGRAVITY, OAuthConfig, XAI, config_by_provider_id};
3535
pub use device::{DeviceCodeResponse, poll_device_code, request_device_code};
3636
pub use pkce::{PkceCodes, new_nonce, new_state};
3737
pub use store::{AuthStore, TokenSet};
@@ -128,6 +128,11 @@ impl OAuth {
128128
Self::new(COPILOT)
129129
}
130130

131+
/// Convenience constructor for the Google Antigravity subscription provider.
132+
pub fn google_antigravity() -> Self {
133+
Self::new(GOOGLE_ANTIGRAVITY)
134+
}
135+
131136
/// The provider config this OAuth instance authenticates against.
132137
pub fn config(&self) -> &OAuthConfig {
133138
&self.config
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//! The `antigravity-oauth` provider template: Google-native models served
2+
//! via Google Antigravity OAuth subscription.
3+
4+
use super::google::{GOOGLE_BUILTIN_MODELS, MODELS};
5+
use super::ProviderTemplateSpec;
6+
7+
pub(crate) const TEMPLATE_SPEC: ProviderTemplateSpec = ProviderTemplateSpec {
8+
id: "antigravity-oauth",
9+
baselines: MODELS,
10+
protocol: "google",
11+
discovery: true,
12+
fitting: false,
13+
models: GOOGLE_BUILTIN_MODELS,
14+
};

crates/neenee-providers/src/registry/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::{
1616
};
1717

1818
mod anthropic;
19+
mod antigravity_oauth;
1920
mod antigravity_sub2api;
2021
mod chatgpt;
2122
mod copilot;
@@ -169,6 +170,7 @@ pub const PROVIDER_TEMPLATE_SPECS: &[ProviderTemplateSpec] = &[
169170
anthropic::SUB2API_TEMPLATE_SPEC,
170171
openai_sub2api::TEMPLATE_SPEC,
171172
antigravity_sub2api::TEMPLATE_SPEC,
173+
antigravity_oauth::TEMPLATE_SPEC,
172174
];
173175

174176
/// Look up a template spec by its stable id. Exact match only.

0 commit comments

Comments
 (0)