opencode-gemini-auth 1.1.0 → 1.1.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 +9 -0
- package/package.json +1 -1
- package/src/plugin/request.ts +109 -6
- package/src/plugin.ts +7 -2
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
package/src/plugin/request.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
162
|
-
if (parsed
|
|
163
|
-
return new Response(
|
|
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
|
+
}
|
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 {
|
|
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
|
},
|