opencode-gemini-auth 1.1.6 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -61
- package/package.json +3 -3
- package/src/gemini/oauth.ts +5 -16
- package/src/plugin/auth.ts +10 -2
- package/src/plugin/project.ts +30 -8
- package/src/plugin/request-helpers.ts +14 -8
- package/src/plugin/types.ts +2 -1
- package/src/plugin.ts +43 -31
- package/src/plugin/cli.ts +0 -15
package/README.md
CHANGED
|
@@ -1,96 +1,173 @@
|
|
|
1
1
|
# Gemini OAuth Plugin for Opencode
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+

|
|
4
|
+

|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
**Authenticate the Opencode CLI with your Google account.** This plugin enables
|
|
7
|
+
you to use your existing Gemini plan and quotas (including the free tier)
|
|
8
|
+
directly within Opencode, bypassing separate API billing.
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
## Prerequisites
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
- [Opencode CLI](https://opencode.ai) installed.
|
|
13
|
+
- A Google account with access to Gemini.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add the plugin to your Opencode configuration file
|
|
18
|
+
(`~/.config/opencode/config.json` or similar):
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"$schema": "https://opencode.ai/config.json",
|
|
23
|
+
"plugin": ["opencode-gemini-auth"]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
1. **Login**: Run the authentication command in your terminal:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
opencode auth login
|
|
15
33
|
```
|
|
16
34
|
|
|
17
|
-
2.
|
|
18
|
-
3.
|
|
35
|
+
2. **Select Provider**: Choose **Google** from the list.
|
|
36
|
+
3. **Authenticate**: Select **OAuth with Google (Gemini CLI)**.
|
|
37
|
+
- A browser window will open for you to approve the access.
|
|
38
|
+
- The plugin spins up a temporary local server to capture the callback.
|
|
39
|
+
- If the local server fails (e.g., port in use or headless environment),
|
|
40
|
+
you can manually copy/paste the callback URL as instructed.
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
browser you'll land on an "Authentication complete" page with no URL
|
|
22
|
-
copy/paste required. If that port is already taken, the CLI automatically
|
|
23
|
-
falls back to the classic copy/paste flow and explains what to do.
|
|
42
|
+
Once authenticated, Opencode will use your Google account for Gemini requests.
|
|
24
43
|
|
|
25
|
-
##
|
|
44
|
+
## Configuration
|
|
26
45
|
|
|
27
|
-
|
|
28
|
-
> OpenCode does NOT auto-update plugins
|
|
46
|
+
### Google Cloud Project
|
|
29
47
|
|
|
30
|
-
|
|
48
|
+
By default, the plugin attempts to provision or find a suitable Google Cloud
|
|
49
|
+
project. To force a specific project, set the `projectId` in your configuration:
|
|
31
50
|
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"provider": {
|
|
54
|
+
"google": {
|
|
55
|
+
"options": {
|
|
56
|
+
"projectId": "your-specific-project-id"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
36
61
|
```
|
|
37
62
|
|
|
38
|
-
|
|
39
|
-
opencode # Reinstalls latest
|
|
40
|
-
```
|
|
63
|
+
### Thinking Models
|
|
41
64
|
|
|
42
|
-
|
|
65
|
+
Configure "thinking" capabilities for Gemini models using the `thinkingConfig`
|
|
66
|
+
option in your `config.json`.
|
|
43
67
|
|
|
44
|
-
|
|
68
|
+
**Gemini 3 (Thinking Level)**
|
|
69
|
+
Use `thinkingLevel` (`"low"`, `"high"`) for Gemini 3 models.
|
|
45
70
|
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"provider": {
|
|
74
|
+
"google": {
|
|
75
|
+
"models": {
|
|
76
|
+
"gemini-3-pro-preview": {
|
|
77
|
+
"options": {
|
|
78
|
+
"thinkingConfig": {
|
|
79
|
+
"thinkingLevel": "high",
|
|
80
|
+
"includeThoughts": true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
50
88
|
```
|
|
51
89
|
|
|
52
|
-
|
|
53
|
-
`
|
|
90
|
+
**Gemini 2.5 (Thinking Budget)**
|
|
91
|
+
Use `thinkingBudget` (token count) for Gemini 2.5 models.
|
|
54
92
|
|
|
55
93
|
```json
|
|
56
94
|
{
|
|
57
|
-
"
|
|
58
|
-
|
|
95
|
+
"provider": {
|
|
96
|
+
"google": {
|
|
97
|
+
"models": {
|
|
98
|
+
"gemini-2.5-flash": {
|
|
99
|
+
"options": {
|
|
100
|
+
"thinkingConfig": {
|
|
101
|
+
"thinkingBudget": 8192,
|
|
102
|
+
"includeThoughts": true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
59
109
|
}
|
|
60
110
|
```
|
|
61
111
|
|
|
62
|
-
|
|
63
|
-
your local clone.
|
|
112
|
+
## Troubleshooting
|
|
64
113
|
|
|
65
|
-
|
|
114
|
+
### Manual Google Cloud Setup
|
|
66
115
|
|
|
67
|
-
If automatic provisioning fails,
|
|
116
|
+
If automatic provisioning fails, you may need to set up the project manually:
|
|
68
117
|
|
|
69
|
-
1. Go to the Google Cloud Console
|
|
70
|
-
2.
|
|
71
|
-
3. Enable the **Gemini for Google Cloud API**
|
|
72
|
-
|
|
118
|
+
1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
|
|
119
|
+
2. Create or select a project.
|
|
120
|
+
3. Enable the **Gemini for Google Cloud API**
|
|
121
|
+
(`cloudaicompanion.googleapis.com`).
|
|
122
|
+
4. Configure the `projectId` in your Opencode config as shown above.
|
|
73
123
|
|
|
74
|
-
|
|
124
|
+
### Debugging
|
|
75
125
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
enabled, the plugin writes to a timestamped `gemini-debug-<ISO>.log` file in
|
|
79
|
-
your current working directory so the CLI output stays clean.
|
|
126
|
+
To view detailed logs of Gemini requests and responses, set the
|
|
127
|
+
`OPENCODE_GEMINI_DEBUG` environment variable:
|
|
80
128
|
|
|
81
129
|
```bash
|
|
82
130
|
OPENCODE_GEMINI_DEBUG=1 opencode
|
|
83
131
|
```
|
|
84
132
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
133
|
+
This will generate `gemini-debug-<timestamp>.log` files in your working
|
|
134
|
+
directory containing sanitized request/response details.
|
|
135
|
+
|
|
136
|
+
### Updating
|
|
137
|
+
|
|
138
|
+
Opencode does not automatically update plugins. To update to the latest version,
|
|
139
|
+
you must clear the cached plugin:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Clear the specific plugin cache
|
|
143
|
+
rm -rf ~/.cache/opencode/node_modules/opencode-gemini-auth
|
|
144
|
+
|
|
145
|
+
# Run Opencode to trigger a fresh install
|
|
146
|
+
opencode
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
To develop on this plugin locally:
|
|
152
|
+
|
|
153
|
+
1. **Clone**:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
git clone https://github.com/jenslys/opencode-gemini-auth.git
|
|
157
|
+
cd opencode-gemini-auth
|
|
158
|
+
bun install
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
2. **Link**:
|
|
162
|
+
Update your Opencode config to point to your local directory using a
|
|
163
|
+
`file://` URL:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"plugin": ["file:///absolute/path/to/opencode-gemini-auth"]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-gemini-auth",
|
|
3
3
|
"module": "index.ts",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.3",
|
|
5
5
|
"author": "jenslys",
|
|
6
6
|
"repository": "https://github.com/jenslys/opencode-gemini-auth",
|
|
7
7
|
"files": [
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@opencode-ai/plugin": "^0.15.
|
|
14
|
+
"@opencode-ai/plugin": "^0.15.31",
|
|
15
15
|
"@types/bun": "latest"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"typescript": "^5"
|
|
18
|
+
"typescript": "^5.9.3"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@openauthjs/openauth": "^0.4.3"
|
package/src/gemini/oauth.ts
CHANGED
|
@@ -14,7 +14,6 @@ interface PkcePair {
|
|
|
14
14
|
|
|
15
15
|
interface GeminiAuthState {
|
|
16
16
|
verifier: string;
|
|
17
|
-
projectId: string;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/**
|
|
@@ -23,7 +22,6 @@ interface GeminiAuthState {
|
|
|
23
22
|
export interface GeminiAuthorization {
|
|
24
23
|
url: string;
|
|
25
24
|
verifier: string;
|
|
26
|
-
projectId: string;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
interface GeminiTokenExchangeSuccess {
|
|
@@ -32,7 +30,6 @@ interface GeminiTokenExchangeSuccess {
|
|
|
32
30
|
access: string;
|
|
33
31
|
expires: number;
|
|
34
32
|
email?: string;
|
|
35
|
-
projectId: string;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
interface GeminiTokenExchangeFailure {
|
|
@@ -74,14 +71,13 @@ function decodeState(state: string): GeminiAuthState {
|
|
|
74
71
|
}
|
|
75
72
|
return {
|
|
76
73
|
verifier: parsed.verifier,
|
|
77
|
-
projectId: typeof parsed.projectId === "string" ? parsed.projectId : "",
|
|
78
74
|
};
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
/**
|
|
82
|
-
* Build the Gemini OAuth authorization URL including PKCE
|
|
78
|
+
* Build the Gemini OAuth authorization URL including PKCE.
|
|
83
79
|
*/
|
|
84
|
-
export async function authorizeGemini(
|
|
80
|
+
export async function authorizeGemini(): Promise<GeminiAuthorization> {
|
|
85
81
|
const pkce = (await generatePKCE()) as PkcePair;
|
|
86
82
|
|
|
87
83
|
const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
|
|
@@ -91,17 +87,13 @@ export async function authorizeGemini(projectId = ""): Promise<GeminiAuthorizati
|
|
|
91
87
|
url.searchParams.set("scope", GEMINI_SCOPES.join(" "));
|
|
92
88
|
url.searchParams.set("code_challenge", pkce.challenge);
|
|
93
89
|
url.searchParams.set("code_challenge_method", "S256");
|
|
94
|
-
url.searchParams.set(
|
|
95
|
-
"state",
|
|
96
|
-
encodeState({ verifier: pkce.verifier, projectId: projectId || "" }),
|
|
97
|
-
);
|
|
90
|
+
url.searchParams.set("state", encodeState({ verifier: pkce.verifier }));
|
|
98
91
|
url.searchParams.set("access_type", "offline");
|
|
99
92
|
url.searchParams.set("prompt", "consent");
|
|
100
93
|
|
|
101
94
|
return {
|
|
102
95
|
url: url.toString(),
|
|
103
96
|
verifier: pkce.verifier,
|
|
104
|
-
projectId: projectId || "",
|
|
105
97
|
};
|
|
106
98
|
}
|
|
107
99
|
|
|
@@ -113,7 +105,7 @@ export async function exchangeGemini(
|
|
|
113
105
|
state: string,
|
|
114
106
|
): Promise<GeminiTokenExchangeResult> {
|
|
115
107
|
try {
|
|
116
|
-
const { verifier
|
|
108
|
+
const { verifier } = decodeState(state);
|
|
117
109
|
|
|
118
110
|
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
|
|
119
111
|
method: "POST",
|
|
@@ -155,15 +147,12 @@ export async function exchangeGemini(
|
|
|
155
147
|
return { type: "failed", error: "Missing refresh token in response" };
|
|
156
148
|
}
|
|
157
149
|
|
|
158
|
-
const storedRefresh = `${refreshToken}|${projectId || ""}`;
|
|
159
|
-
|
|
160
150
|
return {
|
|
161
151
|
type: "success",
|
|
162
|
-
refresh:
|
|
152
|
+
refresh: refreshToken,
|
|
163
153
|
access: tokenPayload.access_token,
|
|
164
154
|
expires: Date.now() + tokenPayload.expires_in * 1000,
|
|
165
155
|
email: userInfo.email,
|
|
166
|
-
projectId: projectId || "",
|
|
167
156
|
};
|
|
168
157
|
} catch (error) {
|
|
169
158
|
return {
|
package/src/plugin/auth.ts
CHANGED
|
@@ -22,9 +22,17 @@ export function parseRefreshParts(refresh: string): RefreshParts {
|
|
|
22
22
|
* Serializes refresh token parts into the stored string format.
|
|
23
23
|
*/
|
|
24
24
|
export function formatRefreshParts(parts: RefreshParts): string {
|
|
25
|
+
if (!parts.refreshToken) {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!parts.projectId && !parts.managedProjectId) {
|
|
30
|
+
return parts.refreshToken;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
const projectSegment = parts.projectId ?? "";
|
|
26
|
-
const
|
|
27
|
-
return parts.
|
|
34
|
+
const managedSegment = parts.managedProjectId ?? "";
|
|
35
|
+
return `${parts.refreshToken}|${projectSegment}|${managedSegment}`;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
/**
|
package/src/plugin/project.ts
CHANGED
|
@@ -48,7 +48,7 @@ class ProjectIdRequiredError extends Error {
|
|
|
48
48
|
*/
|
|
49
49
|
constructor() {
|
|
50
50
|
super(
|
|
51
|
-
"Google Gemini requires a Google Cloud project. Enable the Gemini for Google Cloud API on a project you control,
|
|
51
|
+
"Google Gemini requires a Google Cloud project. Enable the Gemini for Google Cloud API on a project you control, then set `provider.google.options.projectId` in your Opencode config (or set OPENCODE_GEMINI_PROJECT_ID).",
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -109,8 +109,21 @@ export function invalidateProjectContextCache(refresh?: string): void {
|
|
|
109
109
|
projectContextResultCache.clear();
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
|
+
|
|
112
113
|
projectContextPendingCache.delete(refresh);
|
|
113
114
|
projectContextResultCache.delete(refresh);
|
|
115
|
+
|
|
116
|
+
const prefix = `${refresh}|cfg:`;
|
|
117
|
+
for (const key of projectContextPendingCache.keys()) {
|
|
118
|
+
if (key.startsWith(prefix)) {
|
|
119
|
+
projectContextPendingCache.delete(key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const key of projectContextResultCache.keys()) {
|
|
123
|
+
if (key.startsWith(prefix)) {
|
|
124
|
+
projectContextResultCache.delete(key);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
/**
|
|
@@ -220,13 +233,19 @@ export async function onboardManagedProject(
|
|
|
220
233
|
export async function ensureProjectContext(
|
|
221
234
|
auth: OAuthAuthDetails,
|
|
222
235
|
client: PluginClient,
|
|
236
|
+
configuredProjectId?: string,
|
|
223
237
|
): Promise<ProjectContextResult> {
|
|
224
238
|
const accessToken = auth.access;
|
|
225
239
|
if (!accessToken) {
|
|
226
240
|
return { auth, effectiveProjectId: "" };
|
|
227
241
|
}
|
|
228
242
|
|
|
229
|
-
const cacheKey =
|
|
243
|
+
const cacheKey = (() => {
|
|
244
|
+
const base = getCacheKey(auth);
|
|
245
|
+
if (!base) return undefined;
|
|
246
|
+
const project = configuredProjectId?.trim() ?? "";
|
|
247
|
+
return project ? `${base}|cfg:${project}` : base;
|
|
248
|
+
})();
|
|
230
249
|
if (cacheKey) {
|
|
231
250
|
const cached = projectContextResultCache.get(cacheKey);
|
|
232
251
|
if (cached) {
|
|
@@ -240,21 +259,24 @@ export async function ensureProjectContext(
|
|
|
240
259
|
|
|
241
260
|
const resolveContext = async (): Promise<ProjectContextResult> => {
|
|
242
261
|
const parts = parseRefreshParts(auth.refresh);
|
|
243
|
-
|
|
262
|
+
const effectiveConfiguredProjectId = configuredProjectId?.trim() || undefined;
|
|
263
|
+
const projectId = effectiveConfiguredProjectId ?? parts.projectId;
|
|
264
|
+
|
|
265
|
+
if (projectId || parts.managedProjectId) {
|
|
244
266
|
return {
|
|
245
267
|
auth,
|
|
246
|
-
effectiveProjectId:
|
|
268
|
+
effectiveProjectId: projectId || parts.managedProjectId || "",
|
|
247
269
|
};
|
|
248
270
|
}
|
|
249
271
|
|
|
250
|
-
const loadPayload = await loadManagedProject(accessToken,
|
|
272
|
+
const loadPayload = await loadManagedProject(accessToken, projectId);
|
|
251
273
|
if (loadPayload?.cloudaicompanionProject) {
|
|
252
274
|
const managedProjectId = loadPayload.cloudaicompanionProject;
|
|
253
275
|
const updatedAuth: OAuthAuthDetails = {
|
|
254
276
|
...auth,
|
|
255
277
|
refresh: formatRefreshParts({
|
|
256
278
|
refreshToken: parts.refreshToken,
|
|
257
|
-
projectId
|
|
279
|
+
projectId,
|
|
258
280
|
managedProjectId,
|
|
259
281
|
}),
|
|
260
282
|
};
|
|
@@ -283,13 +305,13 @@ export async function ensureProjectContext(
|
|
|
283
305
|
throw new ProjectIdRequiredError();
|
|
284
306
|
}
|
|
285
307
|
|
|
286
|
-
const managedProjectId = await onboardManagedProject(accessToken, tierId,
|
|
308
|
+
const managedProjectId = await onboardManagedProject(accessToken, tierId, projectId);
|
|
287
309
|
if (managedProjectId) {
|
|
288
310
|
const updatedAuth: OAuthAuthDetails = {
|
|
289
311
|
...auth,
|
|
290
312
|
refresh: formatRefreshParts({
|
|
291
313
|
refreshToken: parts.refreshToken,
|
|
292
|
-
projectId
|
|
314
|
+
projectId,
|
|
293
315
|
managedProjectId,
|
|
294
316
|
}),
|
|
295
317
|
};
|
|
@@ -27,15 +27,19 @@ export interface GeminiUsageMetadata {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* Thinking configuration accepted by Gemini.
|
|
31
|
+
* - Gemini 3 models use thinkingLevel (string: 'low', 'medium', 'high')
|
|
32
|
+
* - Gemini 2.5 models use thinkingBudget (number)
|
|
31
33
|
*/
|
|
32
34
|
export interface ThinkingConfig {
|
|
33
35
|
thinkingBudget?: number;
|
|
36
|
+
thinkingLevel?: string;
|
|
34
37
|
includeThoughts?: boolean;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
|
-
*
|
|
41
|
+
* Normalizes thinkingConfig - passes through values as-is without mapping.
|
|
42
|
+
* User should use thinkingLevel for Gemini 3 and thinkingBudget for Gemini 2.5.
|
|
39
43
|
*/
|
|
40
44
|
export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undefined {
|
|
41
45
|
if (!config || typeof config !== "object") {
|
|
@@ -44,15 +48,14 @@ export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undef
|
|
|
44
48
|
|
|
45
49
|
const record = config as Record<string, unknown>;
|
|
46
50
|
const budgetRaw = record.thinkingBudget ?? record.thinking_budget;
|
|
51
|
+
const levelRaw = record.thinkingLevel ?? record.thinking_level;
|
|
47
52
|
const includeRaw = record.includeThoughts ?? record.include_thoughts;
|
|
48
53
|
|
|
49
54
|
const thinkingBudget = typeof budgetRaw === "number" && Number.isFinite(budgetRaw) ? budgetRaw : undefined;
|
|
55
|
+
const thinkingLevel = typeof levelRaw === "string" && levelRaw.length > 0 ? levelRaw.toLowerCase() : undefined;
|
|
50
56
|
const includeThoughts = typeof includeRaw === "boolean" ? includeRaw : undefined;
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
const finalInclude = enableThinking ? includeThoughts ?? false : false;
|
|
54
|
-
|
|
55
|
-
if (!enableThinking && finalInclude === false && thinkingBudget === undefined && includeThoughts === undefined) {
|
|
58
|
+
if (thinkingBudget === undefined && thinkingLevel === undefined && includeThoughts === undefined) {
|
|
56
59
|
return undefined;
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -60,8 +63,11 @@ export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undef
|
|
|
60
63
|
if (thinkingBudget !== undefined) {
|
|
61
64
|
normalized.thinkingBudget = thinkingBudget;
|
|
62
65
|
}
|
|
63
|
-
if (
|
|
64
|
-
normalized.
|
|
66
|
+
if (thinkingLevel !== undefined) {
|
|
67
|
+
normalized.thinkingLevel = thinkingLevel;
|
|
68
|
+
}
|
|
69
|
+
if (includeThoughts !== undefined) {
|
|
70
|
+
normalized.includeThoughts = includeThoughts;
|
|
65
71
|
}
|
|
66
72
|
return normalized;
|
|
67
73
|
}
|
package/src/plugin/types.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface ProviderModel {
|
|
|
26
26
|
|
|
27
27
|
export interface Provider {
|
|
28
28
|
models?: Record<string, ProviderModel>;
|
|
29
|
+
options?: Record<string, unknown>;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export interface LoaderResult {
|
|
@@ -41,7 +42,7 @@ export interface AuthMethod {
|
|
|
41
42
|
url: string;
|
|
42
43
|
instructions: string;
|
|
43
44
|
method: string;
|
|
44
|
-
callback: (callbackUrl: string) => Promise<GeminiTokenExchangeResult
|
|
45
|
+
callback: (() => Promise<GeminiTokenExchangeResult>) | ((callbackUrl: string) => Promise<GeminiTokenExchangeResult>);
|
|
45
46
|
}>;
|
|
46
47
|
}
|
|
47
48
|
|
package/src/plugin.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
1
3
|
import { GEMINI_PROVIDER_ID, GEMINI_REDIRECT_URI } from "./constants";
|
|
2
4
|
import { authorizeGemini, exchangeGemini } from "./gemini/oauth";
|
|
3
5
|
import type { GeminiTokenExchangeResult } from "./gemini/oauth";
|
|
4
6
|
import { accessTokenExpired, isOAuthAuth } from "./plugin/auth";
|
|
5
|
-
import { promptProjectId } from "./plugin/cli";
|
|
6
7
|
import { ensureProjectContext } from "./plugin/project";
|
|
7
8
|
import { startGeminiDebugRequest } from "./plugin/debug";
|
|
8
9
|
import {
|
|
@@ -36,6 +37,17 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
const providerOptions =
|
|
41
|
+
provider && typeof provider === "object"
|
|
42
|
+
? ((provider as { options?: Record<string, unknown> }).options ?? undefined)
|
|
43
|
+
: undefined;
|
|
44
|
+
const projectIdFromConfig =
|
|
45
|
+
providerOptions && typeof providerOptions.projectId === "string"
|
|
46
|
+
? providerOptions.projectId.trim()
|
|
47
|
+
: "";
|
|
48
|
+
const projectIdFromEnv = process.env.OPENCODE_GEMINI_PROJECT_ID?.trim() ?? "";
|
|
49
|
+
const configuredProjectId = projectIdFromEnv || projectIdFromConfig || undefined;
|
|
50
|
+
|
|
39
51
|
if (provider.models) {
|
|
40
52
|
for (const model of Object.values(provider.models)) {
|
|
41
53
|
if (model) {
|
|
@@ -75,7 +87,7 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
75
87
|
*/
|
|
76
88
|
async function resolveProjectContext(): Promise<ProjectContextResult> {
|
|
77
89
|
try {
|
|
78
|
-
return await ensureProjectContext(authRecord, client);
|
|
90
|
+
return await ensureProjectContext(authRecord, client, configuredProjectId);
|
|
79
91
|
} catch (error) {
|
|
80
92
|
if (error instanceof Error) {
|
|
81
93
|
console.error(error.message);
|
|
@@ -120,8 +132,6 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
120
132
|
label: "OAuth with Google (Gemini CLI)",
|
|
121
133
|
type: "oauth",
|
|
122
134
|
authorize: async () => {
|
|
123
|
-
console.log("\n=== Google Gemini OAuth Setup ===");
|
|
124
|
-
|
|
125
135
|
const isHeadless = !!(
|
|
126
136
|
process.env.SSH_CONNECTION ||
|
|
127
137
|
process.env.SSH_CLIENT ||
|
|
@@ -133,40 +143,25 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
133
143
|
if (!isHeadless) {
|
|
134
144
|
try {
|
|
135
145
|
listener = await startOAuthListener();
|
|
136
|
-
const { host } = new URL(GEMINI_REDIRECT_URI);
|
|
137
|
-
console.log("1. You'll be asked to sign in to your Google account and grant permission.");
|
|
138
|
-
console.log(
|
|
139
|
-
`2. We'll automatically capture the browser redirect on http://${host}. No need to paste anything back here.`,
|
|
140
|
-
);
|
|
141
|
-
console.log("3. Once you see the 'Authentication complete' page in your browser, return to this terminal.");
|
|
142
146
|
} catch (error) {
|
|
143
|
-
console.log("1. You'll be asked to sign in to your Google account and grant permission.");
|
|
144
|
-
console.log("2. After you approve, the browser will try to redirect to a 'localhost' page.");
|
|
145
|
-
console.log(
|
|
146
|
-
"3. This page will show an error like 'This site can't be reached'. This is perfectly normal and means it worked!",
|
|
147
|
-
);
|
|
148
|
-
console.log(
|
|
149
|
-
"4. Once you see that error, copy the entire URL from the address bar, paste it back here, and press Enter.",
|
|
150
|
-
);
|
|
151
147
|
if (error instanceof Error) {
|
|
152
|
-
console.log(
|
|
148
|
+
console.log(
|
|
149
|
+
`Warning: Couldn't start the local callback listener (${error.message}). You'll need to paste the callback URL.`,
|
|
150
|
+
);
|
|
153
151
|
} else {
|
|
154
|
-
console.log(
|
|
152
|
+
console.log(
|
|
153
|
+
"Warning: Couldn't start the local callback listener. You'll need to paste the callback URL.",
|
|
154
|
+
);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
} else {
|
|
158
|
-
console.log("Headless environment detected.
|
|
159
|
-
console.log("1. You'll be asked to sign in to your Google account and grant permission.");
|
|
160
|
-
console.log("2. After you approve, the browser will redirect to a 'localhost' URL.");
|
|
161
|
-
console.log(
|
|
162
|
-
"3. Copy the ENTIRE URL from your browser's address bar (it will look like: http://localhost:8085/oauth2callback?code=...&state=...)",
|
|
163
|
-
);
|
|
164
|
-
console.log("4. Paste the URL back here and press Enter.");
|
|
158
|
+
console.log("Headless environment detected. You'll need to paste the callback URL.");
|
|
165
159
|
}
|
|
166
|
-
console.log("\n");
|
|
167
160
|
|
|
168
|
-
const
|
|
169
|
-
|
|
161
|
+
const authorization = await authorizeGemini();
|
|
162
|
+
if (!isHeadless) {
|
|
163
|
+
openBrowserUrl(authorization.url);
|
|
164
|
+
}
|
|
170
165
|
|
|
171
166
|
if (listener) {
|
|
172
167
|
return {
|
|
@@ -206,7 +201,7 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
206
201
|
return {
|
|
207
202
|
url: authorization.url,
|
|
208
203
|
instructions:
|
|
209
|
-
"
|
|
204
|
+
"Complete OAuth in your browser, then paste the full redirected URL (e.g., http://localhost:8085/oauth2callback?code=...&state=...)",
|
|
210
205
|
method: "code",
|
|
211
206
|
callback: async (callbackUrl: string): Promise<GeminiTokenExchangeResult> => {
|
|
212
207
|
try {
|
|
@@ -253,3 +248,20 @@ function toUrlString(value: RequestInfo): string {
|
|
|
253
248
|
}
|
|
254
249
|
return value.toString();
|
|
255
250
|
}
|
|
251
|
+
|
|
252
|
+
function openBrowserUrl(url: string): void {
|
|
253
|
+
try {
|
|
254
|
+
// Best-effort: don't block auth flow if spawning fails.
|
|
255
|
+
const platform = process.platform;
|
|
256
|
+
const command =
|
|
257
|
+
platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
258
|
+
const args =
|
|
259
|
+
platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
260
|
+
const child = spawn(command, args, {
|
|
261
|
+
stdio: "ignore",
|
|
262
|
+
detached: true,
|
|
263
|
+
});
|
|
264
|
+
child.unref?.();
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
}
|
package/src/plugin/cli.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Prompts the user for a project ID via stdin/stdout.
|
|
6
|
-
*/
|
|
7
|
-
export async function promptProjectId(): Promise<string> {
|
|
8
|
-
const rl = createInterface({ input, output });
|
|
9
|
-
try {
|
|
10
|
-
const answer = await rl.question("Project ID (leave blank to use your default project): ");
|
|
11
|
-
return answer.trim();
|
|
12
|
-
} finally {
|
|
13
|
-
rl.close();
|
|
14
|
-
}
|
|
15
|
-
}
|