Skip to content

Commit cb1ea0b

Browse files
authored
Merge pull request #101 from link-assistant/issue-100-a059c6e915ea
fix(google): Route OAuth requests through Cloud Code API for subscription support
2 parents ecffb85 + bf038b2 commit cb1ea0b

7 files changed

Lines changed: 1511 additions & 5 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@link-assistant/agent': minor
3+
---
4+
5+
feat(google): Route OAuth requests through Cloud Code API for proper subscription support
6+
7+
When Google OAuth is active, the agent now routes requests through Google's Cloud Code API
8+
(`cloudcode-pa.googleapis.com/v1internal`) instead of the standard Generative Language API
9+
(`generativelanguage.googleapis.com`).
10+
11+
This is the same approach used by the official Gemini CLI and properly supports:
12+
13+
- Google AI Pro/Ultra subscription users
14+
- The `cloud-platform` OAuth scope (which is all the OAuth client has registered)
15+
- Automatic subscription tier handling (FREE, STANDARD, etc.)
16+
17+
The key insight is that the Gemini CLI doesn't call `generativelanguage.googleapis.com` directly.
18+
Instead, it uses Google's Cloud Code API which:
19+
20+
1. Accepts `cloud-platform` OAuth tokens
21+
2. Handles subscription tier validation
22+
3. Proxies requests to the Generative Language API internally
23+
24+
This fix includes:
25+
26+
- URL transformation from Generative Language API to Cloud Code API
27+
- Request body transformation to Cloud Code API format (wrapping with model/project fields)
28+
- Response body transformation to unwrap Cloud Code API responses
29+
- Streaming response support with proper SSE chunk transformation
30+
- Fallback to API key authentication if Cloud Code API fails
31+
32+
Fixes #100
33+
34+
References:
35+
36+
- [Gemini CLI server.ts](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/server.ts)
37+
- [Gemini CLI oauth2.ts](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts)
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
# Case Study: Issue #100 - Google OAuth Scope Mismatch
2+
3+
## Issue Reference
4+
5+
- **Issue URL**: https://github.com/link-assistant/agent/issues/100
6+
- **Title**: We should try all available auth credentials we have for Google
7+
- **Labels**: bug
8+
- **Date Reported**: December 23, 2025
9+
10+
## Timeline of Events
11+
12+
1. **User installs agent v0.6.3**: `bun install -g @link-assistant/agent@latest`
13+
2. **User authenticates with Google OAuth**: Uses "Google AI Pro/Ultra (OAuth - Manual Code Entry)" method
14+
3. **Login appears successful**: Terminal shows "Login successful"
15+
4. **API request fails**: When using `echo "hi" | agent --model google/gemini-3-pro`, the request fails with error code 403
16+
17+
## Error Analysis
18+
19+
### Error Response Details
20+
21+
```json
22+
{
23+
"error": {
24+
"code": 403,
25+
"message": "Request had insufficient authentication scopes.",
26+
"status": "PERMISSION_DENIED",
27+
"details": [
28+
{
29+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
30+
"reason": "ACCESS_TOKEN_SCOPE_INSUFFICIENT",
31+
"domain": "googleapis.com",
32+
"metadata": {
33+
"method": "google.ai.generativelanguage.v1beta.GenerativeService.StreamGenerateContent",
34+
"service": "generativelanguage.googleapis.com"
35+
}
36+
}
37+
]
38+
}
39+
}
40+
```
41+
42+
### WWW-Authenticate Header
43+
44+
```
45+
Bearer realm="https://accounts.google.com/",
46+
error="insufficient_scope",
47+
scope="https://www.googleapis.com/auth/generative-language
48+
https://www.googleapis.com/auth/generative-language.tuning
49+
https://www.googleapis.com/auth/generative-language.tuning.readonly
50+
https://www.googleapis.com/auth/generative-language.retriever
51+
https://www.googleapis.com/auth/generative-language.retriever.readonly"
52+
```
53+
54+
## Root Cause Analysis
55+
56+
### Current Implementation (src/auth/plugins.ts)
57+
58+
The Google OAuth plugin uses these scopes:
59+
60+
```typescript
61+
const GOOGLE_OAUTH_SCOPES = [
62+
'https://www.googleapis.com/auth/cloud-platform',
63+
'https://www.googleapis.com/auth/userinfo.email',
64+
'https://www.googleapis.com/auth/userinfo.profile',
65+
];
66+
```
67+
68+
### What Our API Requires
69+
70+
We make API calls to `generativelanguage.googleapis.com` which requires specific scopes:
71+
72+
- `https://www.googleapis.com/auth/generative-language`
73+
- `https://www.googleapis.com/auth/generative-language.tuning`
74+
- `https://www.googleapis.com/auth/generative-language.retriever`
75+
76+
### The Real Difference: API Endpoint
77+
78+
**Our Implementation**:
79+
80+
- Uses `generativelanguage.googleapis.com` (standard Generative Language API)
81+
- This API requires `generative-language.*` scopes
82+
- OAuth client doesn't have these scopes registered → **SCOPE MISMATCH**
83+
84+
**Gemini CLI Implementation**:
85+
86+
- Uses `https://cloudcode-pa.googleapis.com/v1internal` (Cloud Code API)
87+
- This is a **different API endpoint** that wraps the Generative Language API
88+
- The Cloud Code API accepts `cloud-platform` scope!
89+
- Same OAuth client, same scopes, but **different API that works with those scopes**
90+
91+
### Gemini CLI Architecture (Key Discovery)
92+
93+
```
94+
User OAuth (cloud-platform scope)
95+
96+
Gemini CLI → Cloud Code API (cloudcode-pa.googleapis.com/v1internal)
97+
98+
Cloud Code Server → Generative Language API (internal)
99+
```
100+
101+
The Gemini CLI doesn't call `generativelanguage.googleapis.com` directly. It calls Google's **Cloud Code API** which:
102+
103+
1. Accepts `cloud-platform` OAuth tokens
104+
2. Handles subscription validation (FREE tier, STANDARD tier)
105+
3. Proxies requests to the Generative Language API internally
106+
107+
### Code Evidence from Gemini CLI
108+
109+
From `/tmp/gemini-cli/packages/core/src/code_assist/server.ts`:
110+
111+
```typescript
112+
export const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
113+
export const CODE_ASSIST_API_VERSION = 'v1internal';
114+
115+
export class CodeAssistServer implements ContentGenerator {
116+
// Makes requests to cloudcode-pa.googleapis.com, not generativelanguage.googleapis.com
117+
async generateContentStream(req, userPromptId) {
118+
return this.requestStreamingPost('streamGenerateContent', ...);
119+
}
120+
121+
getMethodUrl(method: string): string {
122+
return `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${method}`;
123+
}
124+
}
125+
```
126+
127+
## Key Findings
128+
129+
### Authentication Methods for Google AI
130+
131+
| Method | Required Scope | Works? |
132+
| --------------------------- | ----------------------------------- | ------------ |
133+
| API Key | None (uses `x-goog-api-key` header) | Yes |
134+
| OAuth (cloud-platform) | `cloud-platform` | Inconsistent |
135+
| OAuth (generative-language) | `generative-language.*` | Should work |
136+
137+
### The Issue Title's Insight
138+
139+
The issue title "We should try all available auth credentials we have for Google" suggests that:
140+
141+
1. Users may have both OAuth tokens and API keys stored
142+
2. When OAuth fails due to scope issues, the system should fall back to API key authentication
143+
3. Currently, the system does not attempt alternative credentials
144+
145+
## Current Auth Storage
146+
147+
From `src/auth/index.ts`:
148+
149+
```typescript
150+
export namespace Auth {
151+
export const Oauth = z.object({
152+
type: z.literal('oauth'),
153+
refresh: z.string(),
154+
access: z.string(),
155+
expires: z.number(),
156+
enterpriseUrl: z.string().optional(),
157+
});
158+
159+
export const Api = z.object({
160+
type: z.literal('api'),
161+
key: z.string(),
162+
});
163+
164+
// Auth is stored per-provider in auth.json
165+
}
166+
```
167+
168+
Auth credentials are stored by provider ID, meaning:
169+
170+
- `google` -> OAuth credentials (with limited scope)
171+
- API keys from environment variables (`GOOGLE_GENERATIVE_AI_API_KEY` or `GEMINI_API_KEY`)
172+
173+
## Proposed Solutions
174+
175+
### Solution 1: Use Cloud Code API (Recommended - Matches Gemini CLI)
176+
177+
**The proper fix**: Use the same API endpoint as Gemini CLI.
178+
179+
When Google OAuth is active, route API calls through:
180+
181+
- `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent`
182+
183+
Instead of:
184+
185+
- `https://generativelanguage.googleapis.com/v1beta/models/...:streamGenerateContent`
186+
187+
**Pros**:
188+
189+
- Exact same approach as official Gemini CLI
190+
- Works with existing `cloud-platform` scope
191+
- Supports subscription tiers (FREE, STANDARD, etc.)
192+
- No need for users to set API keys
193+
194+
**Cons**:
195+
196+
- Requires implementing Cloud Code API request/response translation
197+
- More complex implementation
198+
199+
### Solution 2: Add Generative Language Scopes to OAuth
200+
201+
Update `GOOGLE_OAUTH_SCOPES` in `src/auth/plugins.ts`:
202+
203+
```typescript
204+
const GOOGLE_OAUTH_SCOPES = [
205+
'https://www.googleapis.com/auth/cloud-platform',
206+
'https://www.googleapis.com/auth/userinfo.email',
207+
'https://www.googleapis.com/auth/userinfo.profile',
208+
'https://www.googleapis.com/auth/generative-language',
209+
];
210+
```
211+
212+
**Pros**: Simple fix
213+
**Cons**:
214+
215+
- Won't work - scopes aren't registered for the OAuth client (causes `403: restricted_client`)
216+
- See issue #93
217+
218+
### Solution 3: Credential Fallback Mechanism (Current Implementation)
219+
220+
Implement a fallback strategy in `src/auth/plugins.ts`:
221+
222+
1. Try OAuth credentials first
223+
2. If OAuth fails with scope error (403), try API key
224+
3. If API key fails, report the original error
225+
226+
**Pros**: Works as workaround
227+
**Cons**:
228+
229+
- Requires users to have an API key set
230+
- Doesn't leverage subscription benefits properly
231+
- Not the same experience as Gemini CLI
232+
233+
## Implementation Recommendation
234+
235+
**Proper Fix**: Solution 1 - Use Cloud Code API
236+
237+
This requires implementing a `CodeAssistServer`-like client that:
238+
239+
1. Calls `https://cloudcode-pa.googleapis.com/v1internal` endpoints
240+
2. Translates between our request format and Cloud Code API format
241+
3. Uses the `google-auth-library` OAuth client for Bearer token authentication
242+
243+
The Gemini CLI has already implemented this in:
244+
245+
- `/packages/core/src/code_assist/server.ts` - API client
246+
- `/packages/core/src/code_assist/converter.ts` - Request/response translation
247+
248+
**Temporary Fix**: Solution 3 - Credential Fallback (Already Implemented)
249+
250+
Until Solution 1 is implemented, users can set an API key as a fallback.
251+
252+
## Implementation
253+
254+
### Changes Made
255+
256+
#### Key Insight: OAuth Client Scope Limitations
257+
258+
The generative-language scopes cannot simply be added to the OAuth flow because:
259+
260+
1. The Gemini CLI OAuth client (`681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j`) only has certain scopes registered
261+
2. Attempting to request unregistered scopes causes a `403: restricted_client` error (see issue #93)
262+
3. Therefore, the solution is NOT to add more scopes, but to fall back to API key authentication
263+
264+
#### Implemented Solution: Fallback to API Key on OAuth Scope Errors (`src/auth/plugins.ts`)
265+
266+
In the Google OAuth loader, added logic to:
267+
268+
1. Detect OAuth scope errors (HTTP 403 with `insufficient_scope` in `www-authenticate` header)
269+
2. Fall back to API key authentication if available (`GOOGLE_GENERATIVE_AI_API_KEY` or `GEMINI_API_KEY`)
270+
3. Log helpful warnings and hints for users
271+
272+
```typescript
273+
const isScopeError = (response: Response): boolean => {
274+
if (response.status !== 403) return false;
275+
const wwwAuth = response.headers.get('www-authenticate') || '';
276+
return (
277+
wwwAuth.includes('insufficient_scope') ||
278+
wwwAuth.includes('ACCESS_TOKEN_SCOPE_INSUFFICIENT')
279+
);
280+
};
281+
282+
// In the fetch handler:
283+
if (isScopeError(oauthResponse)) {
284+
const fallbackApiKey = getFallbackApiKey();
285+
if (fallbackApiKey) {
286+
log.warn('oauth scope error, falling back to api key authentication');
287+
// Use API key instead of OAuth
288+
}
289+
}
290+
```
291+
292+
### How to Test
293+
294+
1. **Users with OAuth + API key**: Set `GOOGLE_GENERATIVE_AI_API_KEY` or `GEMINI_API_KEY` environment variable. When OAuth fails with scope error, it will automatically fall back to the API key.
295+
296+
2. **Manual verification**: Check the logs for messages like:
297+
- `using google oauth credentials` - OAuth being used
298+
- `oauth scope error, falling back to api key authentication` - Fallback triggered
299+
- `oauth scope error and no api key fallback available` - No fallback available (user needs to set API key)
300+
301+
## References
302+
303+
- [Google OAuth Scopes Documentation](https://ai.google.dev/gemini-api/docs/oauth)
304+
- [Gemini CLI OAuth Implementation](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts)
305+
- [Issue #51 - PaLM API 403 Insufficient Scopes](https://github.com/google/generative-ai-python/issues/51)
306+
- [Google Developer Forum - ACCESS_TOKEN_SCOPE_INSUFFICIENT](https://discuss.google.dev/t/googlegenerativeaierror-access-token-scope-insufficient/170693)

0 commit comments

Comments
 (0)