opencode-gemini-auth 1.1.0 → 1.1.4

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 CHANGED
@@ -62,6 +62,15 @@ When you want Opencode to use a local checkout of this plugin, point the
62
62
  Replace `/absolute/path/to/opencode-gemini-auth` with the absolute path to
63
63
  your local clone.
64
64
 
65
+ ## Manual Google Cloud Setup
66
+
67
+ If automatic provisioning fails, use the console:
68
+
69
+ 1. Go to the Google Cloud Console and create (or select) a project, e.g. `gemini`.
70
+ 2. Select that project.
71
+ 3. Enable the **Gemini for Google Cloud API** (`cloudaicompanion.googleapis.com`).
72
+ 4. Re-run `opencode auth login` and enter the project **ID** (not the display name).
73
+
65
74
  ## Debugging Gemini Requests
66
75
 
67
76
  Set `OPENCODE_GEMINI_DEBUG=1` in the environment when you run an Opencode
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-gemini-auth",
3
3
  "module": "index.ts",
4
- "version": "1.1.0",
4
+ "version": "1.1.4",
5
5
  "author": "jenslys",
6
6
  "repository": "https://github.com/jenslys/opencode-gemini-auth",
7
7
  "files": [
@@ -8,6 +8,20 @@ const STREAM_ACTION = "streamGenerateContent";
8
8
  const MODEL_FALLBACKS: Record<string, string> = {
9
9
  "gemini-2.5-flash-image": "gemini-2.5-flash",
10
10
  };
11
+ const GEMINI_PREVIEW_LINK = "https://goo.gle/enable-preview-features";
12
+
13
+ interface GeminiApiError {
14
+ code?: number;
15
+ message?: string;
16
+ status?: string;
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ interface GeminiApiBody {
21
+ response?: unknown;
22
+ error?: GeminiApiError;
23
+ [key: string]: unknown;
24
+ }
11
25
 
12
26
  export function isGenerativeLanguageRequest(input: RequestInfo): input is string {
13
27
  return typeof input === "string" && input.includes("generativelanguage.googleapis.com");
@@ -40,7 +54,7 @@ export function prepareGeminiRequest(
40
54
  init: RequestInit | undefined,
41
55
  accessToken: string,
42
56
  projectId: string,
43
- ): { request: RequestInfo; init: RequestInit; streaming: boolean } {
57
+ ): { request: RequestInfo; init: RequestInit; streaming: boolean; requestedModel?: string } {
44
58
  const baseInit: RequestInit = { ...init };
45
59
  const headers = new Headers(init?.headers ?? {});
46
60
 
@@ -124,6 +138,7 @@ export function prepareGeminiRequest(
124
138
  body,
125
139
  },
126
140
  streaming,
141
+ requestedModel: rawModel,
127
142
  };
128
143
  }
129
144
 
@@ -131,9 +146,13 @@ export async function transformGeminiResponse(
131
146
  response: Response,
132
147
  streaming: boolean,
133
148
  debugContext?: GeminiDebugContext | null,
149
+ requestedModel?: string,
134
150
  ): Promise<Response> {
135
151
  const contentType = response.headers.get("content-type") ?? "";
136
- if (!streaming && !contentType.includes("application/json")) {
152
+ const isJsonResponse = contentType.includes("application/json");
153
+ const isEventStreamResponse = contentType.includes("text/event-stream");
154
+
155
+ if (!isJsonResponse && !isEventStreamResponse) {
137
156
  logGeminiDebugResponse(debugContext, response, {
138
157
  note: "Non-JSON response (body omitted)",
139
158
  });
@@ -154,13 +173,24 @@ export async function transformGeminiResponse(
154
173
  note: streaming ? "Streaming SSE payload" : undefined,
155
174
  });
156
175
 
157
- if (streaming) {
176
+ if (streaming && response.ok && isEventStreamResponse) {
158
177
  return new Response(transformStreamingPayload(text), init);
159
178
  }
160
179
 
161
- const parsed = JSON.parse(text) as { response?: unknown };
162
- if (parsed.response !== undefined) {
163
- return new Response(JSON.stringify(parsed.response), init);
180
+ const parsed = parseGeminiApiBody(text);
181
+ if (!parsed) {
182
+ return new Response(text, init);
183
+ }
184
+
185
+ const patched = rewriteGeminiPreviewAccessError(parsed, response.status, requestedModel);
186
+ const effectiveBody = patched ?? parsed;
187
+
188
+ if (effectiveBody.response !== undefined) {
189
+ return new Response(JSON.stringify(effectiveBody.response), init);
190
+ }
191
+
192
+ if (patched) {
193
+ return new Response(JSON.stringify(effectiveBody), init);
164
194
  }
165
195
 
166
196
  return new Response(text, init);
@@ -173,3 +203,76 @@ export async function transformGeminiResponse(
173
203
  return response;
174
204
  }
175
205
  }
206
+
207
+ function rewriteGeminiPreviewAccessError(
208
+ body: GeminiApiBody,
209
+ status: number,
210
+ requestedModel?: string,
211
+ ): GeminiApiBody | null {
212
+ if (!needsPreviewAccessOverride(status, body, requestedModel)) {
213
+ return null;
214
+ }
215
+
216
+ const error: GeminiApiError = body.error ?? {};
217
+ const trimmedMessage = typeof error.message === "string" ? error.message.trim() : "";
218
+ const messagePrefix = trimmedMessage.length > 0
219
+ ? trimmedMessage
220
+ : "Gemini 3 preview features are not enabled for this account.";
221
+ const enhancedMessage = `${messagePrefix} Request preview access at ${GEMINI_PREVIEW_LINK} before using Gemini 3 models.`;
222
+
223
+ return {
224
+ ...body,
225
+ error: {
226
+ ...error,
227
+ message: enhancedMessage,
228
+ },
229
+ };
230
+ }
231
+
232
+ function needsPreviewAccessOverride(
233
+ status: number,
234
+ body: GeminiApiBody,
235
+ requestedModel?: string,
236
+ ): boolean {
237
+ if (status !== 404) {
238
+ return false;
239
+ }
240
+
241
+ if (isGeminiThreeModel(requestedModel)) {
242
+ return true;
243
+ }
244
+
245
+ const errorMessage = typeof body.error?.message === "string" ? body.error.message : "";
246
+ return isGeminiThreeModel(errorMessage);
247
+ }
248
+
249
+ function isGeminiThreeModel(target?: string): boolean {
250
+ if (!target) {
251
+ return false;
252
+ }
253
+
254
+ return /gemini[\s-]?3/i.test(target);
255
+ }
256
+
257
+ function parseGeminiApiBody(rawText: string): GeminiApiBody | null {
258
+ try {
259
+ const parsed = JSON.parse(rawText);
260
+ if (Array.isArray(parsed)) {
261
+ const firstObject = parsed.find(function (item: unknown) {
262
+ return typeof item === "object" && item !== null;
263
+ });
264
+ if (firstObject && typeof firstObject === "object") {
265
+ return firstObject as GeminiApiBody;
266
+ }
267
+ return null;
268
+ }
269
+
270
+ if (parsed && typeof parsed === "object") {
271
+ return parsed as GeminiApiBody;
272
+ }
273
+
274
+ return null;
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
@@ -139,14 +139,13 @@ export async function refreshAccessToken(
139
139
  storeCachedAuth(updatedAuth);
140
140
  invalidateProjectContextCache(auth.refresh);
141
141
 
142
- const refreshTokenRotated =
143
- typeof payload.refresh_token === "string" && payload.refresh_token !== parts.refreshToken;
144
-
145
- if (refreshTokenRotated) {
142
+ try {
146
143
  await client.auth.set({
147
144
  path: { id: GEMINI_PROVIDER_ID },
148
145
  body: updatedAuth,
149
146
  });
147
+ } catch (storeError) {
148
+ console.error("Failed to persist refreshed Gemini OAuth credentials:", storeError);
150
149
  }
151
150
 
152
151
  return updatedAuth;
package/src/plugin.ts CHANGED
@@ -79,7 +79,12 @@ export const GeminiCLIOAuthPlugin = async (
79
79
 
80
80
  const projectContext = await resolveProjectContext();
81
81
 
82
- const { request, init: transformedInit, streaming } = prepareGeminiRequest(
82
+ const {
83
+ request,
84
+ init: transformedInit,
85
+ streaming,
86
+ requestedModel,
87
+ } = prepareGeminiRequest(
83
88
  input,
84
89
  init,
85
90
  accessToken,
@@ -99,7 +104,7 @@ export const GeminiCLIOAuthPlugin = async (
99
104
  });
100
105
 
101
106
  const response = await fetch(request, transformedInit);
102
- return transformGeminiResponse(response, streaming, debugContext);
107
+ return transformGeminiResponse(response, streaming, debugContext, requestedModel);
103
108
  },
104
109
  };
105
110
  },