mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 21:03:19 +00:00
- Add OAuth handler with PKCE flow and local callback server - Automatic project discovery via loadCodeAssist/onboardUser endpoints - Store credentials with projectId for API calls - Encode token+projectId as JSON for provider to decode - Register as 'google-cloud-code-assist' OAuth provider
321 lines
9.7 KiB
Markdown
321 lines
9.7 KiB
Markdown
# Google Cloud Code Assist Provider Implementation Plan
|
|
|
|
## Overview
|
|
Add support for Gemini CLI / Antigravity authentication, which uses Google's Cloud Code Assist API (`cloudcode-pa.googleapis.com`) to access Gemini and Claude models through a unified gateway.
|
|
|
|
## References
|
|
- Antigravity API Spec: https://github.com/NoeFabris/opencode-antigravity-auth/blob/main/docs/ANTIGRAVITY_API_SPEC.md
|
|
- Gemini CLI Auth: https://github.com/jenslys/opencode-gemini-auth
|
|
- Antigravity Auth: https://github.com/NoeFabris/opencode-antigravity-auth
|
|
|
|
## Key Differences from Standard Google Provider
|
|
1. **Endpoint**: `cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse`
|
|
2. **Auth**: OAuth token (not API key)
|
|
3. **Request format**: Wrapped in `{ project, model, request: {...} }`
|
|
4. **Response format**: Wrapped in `{ response: {...} }` (needs unwrapping)
|
|
5. **Headers**: Requires `User-Agent`, `X-Goog-Api-Client`, `Client-Metadata`
|
|
|
|
## OAuth Details
|
|
- **Client ID**: `681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com`
|
|
- **Client Secret**: `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`
|
|
- **Redirect URI**: `http://localhost:8085/oauth2callback`
|
|
- **Scopes**: `cloud-platform`, `userinfo.email`, `userinfo.profile`
|
|
- **Token URL**: `https://oauth2.googleapis.com/token`
|
|
- **Auth URL**: `https://accounts.google.com/o/oauth2/v2/auth`
|
|
|
|
## Available Models
|
|
| Model ID | Type | Context | Output | Reasoning |
|
|
|----------|------|---------|--------|-----------|
|
|
| gemini-3-pro-high | Gemini | 1M | 64K | Yes |
|
|
| gemini-3-pro-low | Gemini | 1M | 64K | Yes |
|
|
| gemini-3-flash | Gemini | 1M | 64K | No |
|
|
| claude-sonnet-4-5 | Claude | 200K | 64K | No |
|
|
| claude-sonnet-4-5-thinking | Claude | 200K | 64K | Yes |
|
|
| claude-opus-4-5-thinking | Claude | 200K | 64K | Yes |
|
|
| gpt-oss-120b-medium | GPT-OSS | 128K | 32K | No |
|
|
|
|
All models support: text, image, pdf input; text output; cost is $0 (uses Google account quota)
|
|
|
|
---
|
|
|
|
## Implementation Steps
|
|
|
|
### Phase 1: AI Provider (COMPLETED)
|
|
|
|
Steps 1-8 completed. The provider is implemented in:
|
|
- `packages/ai/src/providers/google-cloud-code-assist.ts`
|
|
- `packages/ai/src/providers/google-shared.ts`
|
|
- Models added to `packages/ai/scripts/generate-models.ts`
|
|
|
|
---
|
|
|
|
### Step 1: Update types.ts
|
|
File: `packages/ai/src/types.ts`
|
|
|
|
Add new API type:
|
|
```typescript
|
|
export type Api = "openai-completions" | "openai-responses" | "anthropic-messages" | "google-generative-ai" | "google-cloud-code-assist";
|
|
```
|
|
|
|
### Step 2: Create google-shared.ts
|
|
File: `packages/ai/src/providers/google-shared.ts`
|
|
|
|
Extract from `google.ts`:
|
|
- `convertMessages()` - convert internal messages to Gemini Content[] format
|
|
- `convertTools()` - convert tools to Gemini function declarations
|
|
- `mapToolChoice()` - map tool choice to Gemini enum
|
|
- `mapStopReason()` - map Gemini finish reason to our stop reason
|
|
- Shared types and imports
|
|
|
|
Make functions generic to work with both `google-generative-ai` and `google-cloud-code-assist` API types.
|
|
|
|
### Step 3: Update google.ts
|
|
File: `packages/ai/src/providers/google.ts`
|
|
|
|
- Import shared functions from `google-shared.ts`
|
|
- Remove extracted functions
|
|
- Keep: `createClient()`, `buildParams()`, `streamGoogle()`
|
|
|
|
### Step 4: Create google-cloud-code-assist.ts
|
|
File: `packages/ai/src/providers/google-cloud-code-assist.ts`
|
|
|
|
Implement:
|
|
```typescript
|
|
export interface GoogleCloudCodeAssistOptions extends StreamOptions {
|
|
toolChoice?: "auto" | "none" | "any";
|
|
thinking?: {
|
|
enabled: boolean;
|
|
budgetTokens?: number;
|
|
};
|
|
projectId?: string; // Google Cloud project ID
|
|
}
|
|
|
|
export const streamGoogleCloudCodeAssist: StreamFunction<"google-cloud-code-assist"> = (
|
|
model: Model<"google-cloud-code-assist">,
|
|
context: Context,
|
|
options?: GoogleCloudCodeAssistOptions,
|
|
): AssistantMessageEventStream => {
|
|
// Implementation
|
|
};
|
|
```
|
|
|
|
Key implementation details:
|
|
1. **Build request body**:
|
|
```json
|
|
{
|
|
"project": "{projectId}",
|
|
"model": "{modelId}",
|
|
"request": {
|
|
"contents": [...],
|
|
"systemInstruction": { "parts": [{ "text": "..." }] },
|
|
"generationConfig": { ... },
|
|
"tools": [...]
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **Headers**:
|
|
```
|
|
Authorization: Bearer {accessToken}
|
|
Content-Type: application/json
|
|
Accept: text/event-stream
|
|
User-Agent: google-api-nodejs-client/9.15.1
|
|
X-Goog-Api-Client: gl-node/22.17.0
|
|
Client-Metadata: ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI
|
|
```
|
|
|
|
3. **Endpoint**: `https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse`
|
|
|
|
4. **Parse SSE response**:
|
|
- Each line: `data: {"response": {...}, "traceId": "..."}`
|
|
- Extract `response` object, which has same structure as standard Gemini response
|
|
- Handle thinking parts with `thought: true` and `thoughtSignature`
|
|
|
|
5. **Use shared functions** for message/tool conversion and stop reason mapping
|
|
|
|
### Step 5: Update stream.ts
|
|
File: `packages/ai/src/stream.ts`
|
|
|
|
Add case for new provider:
|
|
```typescript
|
|
import { streamGoogleCloudCodeAssist } from "./providers/google-cloud-code-assist.js";
|
|
|
|
// In the switch/case or if/else chain:
|
|
case "google-cloud-code-assist":
|
|
return streamGoogleCloudCodeAssist(model, context, {
|
|
...options,
|
|
// map reasoning to thinking config
|
|
});
|
|
```
|
|
|
|
### Step 6: Update models.ts
|
|
File: `packages/ai/src/models.ts`
|
|
|
|
Add to `xhighSupportedModels` if applicable (check if any models support xhigh).
|
|
|
|
### Step 7: Add models to generate-models.ts
|
|
File: `packages/ai/scripts/generate-models.ts`
|
|
|
|
Add hardcoded models:
|
|
```typescript
|
|
const googleCloudCodeAssistModels: Model<"google-cloud-code-assist">[] = [
|
|
{
|
|
id: "gemini-3-pro-high",
|
|
provider: "google-cloud-code-assist",
|
|
api: "google-cloud-code-assist",
|
|
name: "Gemini 3 Pro High",
|
|
contextWindow: 1048576,
|
|
maxOutputTokens: 65535,
|
|
pricing: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
input: ["text", "image", "pdf"],
|
|
output: ["text"],
|
|
reasoning: true,
|
|
},
|
|
// ... other models
|
|
];
|
|
```
|
|
|
|
### Step 8: Update index.ts exports
|
|
File: `packages/ai/src/index.ts`
|
|
|
|
Export new provider:
|
|
```typescript
|
|
export { streamGoogleCloudCodeAssist, type GoogleCloudCodeAssistOptions } from "./providers/google-cloud-code-assist.js";
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 2: OAuth Flow in coding-agent
|
|
|
|
### Step 9: Create google-cloud.ts OAuth handler
|
|
File: `packages/coding-agent/src/core/oauth/google-cloud.ts`
|
|
|
|
Implement:
|
|
```typescript
|
|
import { createHash, randomBytes } from "crypto";
|
|
import { createServer } from "http";
|
|
import { type OAuthCredentials, saveOAuthCredentials } from "./storage.js";
|
|
|
|
const CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
const CLIENT_SECRET = "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";
|
|
const REDIRECT_URI = "http://localhost:8085/oauth2callback";
|
|
const SCOPES = [
|
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
"https://www.googleapis.com/auth/userinfo.profile",
|
|
];
|
|
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
|
|
export async function loginGoogleCloud(
|
|
onAuth: (info: { url: string; instructions?: string }) => void,
|
|
onProgress?: (message: string) => void,
|
|
): Promise<OAuthCredentials & { projectId?: string }> {
|
|
// 1. Generate PKCE
|
|
// 2. Start local server on port 8085
|
|
// 3. Build auth URL and call onAuth
|
|
// 4. Wait for callback with code
|
|
// 5. Exchange code for tokens
|
|
// 6. Discover/provision project via loadCodeAssist endpoint
|
|
// 7. Return credentials with projectId
|
|
}
|
|
|
|
export async function refreshGoogleCloudToken(refreshToken: string): Promise<OAuthCredentials> {
|
|
// Refresh token flow
|
|
}
|
|
|
|
// Project discovery
|
|
async function discoverProject(accessToken: string): Promise<string> {
|
|
// Call /v1internal:loadCodeAssist to get project ID
|
|
// Or /v1internal:onboardUser if needed
|
|
}
|
|
```
|
|
|
|
### Step 10: Update oauth/index.ts
|
|
File: `packages/coding-agent/src/core/oauth/index.ts`
|
|
|
|
- Add `"google-cloud"` to `SupportedOAuthProvider`
|
|
- Add to `getOAuthProviders()` list
|
|
- Add case in `login()` function
|
|
- Add case in `refreshToken()` function
|
|
|
|
### Step 11: Update oauth/storage.ts
|
|
File: `packages/coding-agent/src/core/oauth/storage.ts`
|
|
|
|
Extend `OAuthCredentials` to include optional `projectId`:
|
|
```typescript
|
|
export interface OAuthCredentials {
|
|
type: "oauth";
|
|
refresh: string;
|
|
access: string;
|
|
expires: number;
|
|
enterpriseUrl?: string;
|
|
projectId?: string; // For Google Cloud
|
|
}
|
|
```
|
|
|
|
### Step 12: Update model-config.ts
|
|
File: `packages/coding-agent/src/core/model-config.ts`
|
|
|
|
Add logic to get API key for `google-cloud-code-assist` provider:
|
|
- Check for OAuth token via `getOAuthToken("google-cloud")`
|
|
- Return the access token as the "API key"
|
|
|
|
---
|
|
|
|
## Phase 3: Testing
|
|
|
|
### Manual Testing
|
|
1. Run `pi` and use `/login` to authenticate with Google
|
|
2. Select a google-cloud-code-assist model
|
|
3. Send a message and verify streaming works
|
|
4. Test tool calling
|
|
5. Test thinking models
|
|
|
|
### Verification Points
|
|
- [ ] OAuth flow completes successfully
|
|
- [ ] Token refresh works
|
|
- [ ] Streaming text works
|
|
- [ ] Thinking blocks are parsed correctly
|
|
- [ ] Tool calls work
|
|
- [ ] Tool results are sent correctly
|
|
- [ ] Error handling works (rate limits, auth errors)
|
|
|
|
---
|
|
|
|
## Notes
|
|
|
|
### systemInstruction Format
|
|
Must be object with parts, NOT plain string:
|
|
```json
|
|
{
|
|
"systemInstruction": {
|
|
"parts": [{ "text": "You are a helpful assistant." }]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Tool Name Rules
|
|
- Must start with letter or underscore
|
|
- Allowed: a-zA-Z0-9, underscores, dots, colons, dashes
|
|
- Max 64 chars
|
|
- No slashes or spaces
|
|
|
|
### Thinking Config
|
|
```json
|
|
{
|
|
"generationConfig": {
|
|
"thinkingConfig": {
|
|
"thinkingBudget": 8000,
|
|
"includeThoughts": true
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Response Unwrapping
|
|
SSE lines come as:
|
|
```
|
|
data: {"response": {...}, "traceId": "..."}
|
|
```
|
|
Need to extract `response` object which matches standard Gemini format.
|