opencode-gemini-auth 1.3.3 → 1.3.6

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
@@ -20,7 +20,7 @@ Add the plugin to your Opencode configuration file
20
20
  ```json
21
21
  {
22
22
  "$schema": "https://opencode.ai/config.json",
23
- "plugin": ["opencode-gemini-auth"]
23
+ "plugin": ["opencode-gemini-auth@latest"]
24
24
  }
25
25
  ```
26
26
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-gemini-auth",
3
3
  "module": "index.ts",
4
- "version": "1.3.3",
4
+ "version": "1.3.6",
5
5
  "author": "jenslys",
6
6
  "repository": "https://github.com/jenslys/opencode-gemini-auth",
7
7
  "files": [
@@ -120,34 +120,6 @@ export function extractUsageMetadata(body: GeminiApiBody): GeminiUsageMetadata |
120
120
  };
121
121
  }
122
122
 
123
- /**
124
- * Walks SSE lines to find a usage-bearing response chunk.
125
- */
126
- export function extractUsageFromSsePayload(payload: string): GeminiUsageMetadata | null {
127
- const lines = payload.split("\n");
128
- for (const line of lines) {
129
- if (!line.startsWith("data:")) {
130
- continue;
131
- }
132
- const jsonText = line.slice(5).trim();
133
- if (!jsonText) {
134
- continue;
135
- }
136
- try {
137
- const parsed = JSON.parse(jsonText);
138
- if (parsed && typeof parsed === "object") {
139
- const usage = extractUsageMetadata({ response: (parsed as Record<string, unknown>).response });
140
- if (usage) {
141
- return usage;
142
- }
143
- }
144
- } catch {
145
- continue;
146
- }
147
- }
148
- return null;
149
- }
150
-
151
123
  /**
152
124
  * Enhances 404 errors for Gemini 3 models with a direct preview-access message.
153
125
  */
@@ -1,7 +1,6 @@
1
1
  import { CODE_ASSIST_HEADERS, GEMINI_CODE_ASSIST_ENDPOINT } from "../constants";
2
2
  import { logGeminiDebugResponse, type GeminiDebugContext } from "./debug";
3
3
  import {
4
- extractUsageFromSsePayload,
5
4
  extractUsageMetadata,
6
5
  normalizeThinkingConfig,
7
6
  parseGeminiApiBody,
@@ -20,32 +19,85 @@ const MODEL_FALLBACKS: Record<string, string> = {
20
19
  * @returns True when the URL targets generativelanguage.googleapis.com.
21
20
  */
22
21
  export function isGenerativeLanguageRequest(input: RequestInfo): input is string {
23
- return typeof input === "string" && input.includes("generativelanguage.googleapis.com");
22
+ return toRequestUrlString(input).includes("generativelanguage.googleapis.com");
24
23
  }
25
24
 
26
25
  /**
27
- * Rewrites SSE payloads so downstream consumers see only the inner `response` objects.
26
+ * Rewrites SSE payload lines so downstream consumers see only the inner `response` objects.
28
27
  */
29
- function transformStreamingPayload(payload: string): string {
30
- return payload
31
- .split("\n")
32
- .map((line) => {
33
- if (!line.startsWith("data:")) {
34
- return line;
35
- }
36
- const json = line.slice(5).trim();
37
- if (!json) {
38
- return line;
28
+ function transformStreamingLine(line: string): string {
29
+ if (!line.startsWith("data:")) {
30
+ return line;
31
+ }
32
+ const json = line.slice(5).trim();
33
+ if (!json) {
34
+ return line;
35
+ }
36
+ try {
37
+ const parsed = JSON.parse(json) as { response?: unknown };
38
+ if (parsed.response !== undefined) {
39
+ return `data: ${JSON.stringify(parsed.response)}`;
40
+ }
41
+ } catch (_) {}
42
+ return line;
43
+ }
44
+
45
+ /**
46
+ * Streams SSE payloads, rewriting data lines on the fly.
47
+ */
48
+ function transformStreamingPayloadStream(
49
+ stream: ReadableStream<Uint8Array>,
50
+ ): ReadableStream<Uint8Array> {
51
+ const decoder = new TextDecoder();
52
+ const encoder = new TextEncoder();
53
+ let buffer = "";
54
+ let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
55
+
56
+ return new ReadableStream<Uint8Array>({
57
+ start(controller) {
58
+ reader = stream.getReader();
59
+ const pump = (): void => {
60
+ reader!
61
+ .read()
62
+ .then(({ done, value }) => {
63
+ if (done) {
64
+ buffer += decoder.decode();
65
+ if (buffer.length > 0) {
66
+ controller.enqueue(encoder.encode(transformStreamingLine(buffer)));
67
+ }
68
+ controller.close();
69
+ return;
70
+ }
71
+
72
+ buffer += decoder.decode(value, { stream: true });
73
+
74
+ let newlineIndex = buffer.indexOf("\n");
75
+ while (newlineIndex !== -1) {
76
+ const line = buffer.slice(0, newlineIndex);
77
+ buffer = buffer.slice(newlineIndex + 1);
78
+ const hasCarriageReturn = line.endsWith("\r");
79
+ const rawLine = hasCarriageReturn ? line.slice(0, -1) : line;
80
+ const transformed = transformStreamingLine(rawLine);
81
+ const suffix = hasCarriageReturn ? "\r\n" : "\n";
82
+ controller.enqueue(encoder.encode(`${transformed}${suffix}`));
83
+ newlineIndex = buffer.indexOf("\n");
84
+ }
85
+
86
+ pump();
87
+ })
88
+ .catch((error) => {
89
+ controller.error(error);
90
+ });
91
+ };
92
+
93
+ pump();
94
+ },
95
+ cancel(reason) {
96
+ if (reader) {
97
+ reader.cancel(reason).catch(() => {});
39
98
  }
40
- try {
41
- const parsed = JSON.parse(json) as { response?: unknown };
42
- if (parsed.response !== undefined) {
43
- return `data: ${JSON.stringify(parsed.response)}`;
44
- }
45
- } catch (_) {}
46
- return line;
47
- })
48
- .join("\n");
99
+ },
100
+ });
49
101
  }
50
102
 
51
103
  /**
@@ -72,7 +124,7 @@ export function prepareGeminiRequest(
72
124
  headers.set("Authorization", `Bearer ${accessToken}`);
73
125
  headers.delete("x-api-key");
74
126
 
75
- const match = input.match(/\/models\/([^:]+):(\w+)/);
127
+ const match = toRequestUrlString(input).match(/\/models\/([^:]+):(\w+)/);
76
128
  if (!match) {
77
129
  return {
78
130
  request: input,
@@ -136,7 +188,6 @@ export function prepareGeminiRequest(
136
188
  }
137
189
 
138
190
  delete requestPayload.cached_content;
139
- delete requestPayload.cachedContent;
140
191
  if (requestPayload.extra_body && typeof requestPayload.extra_body === "object") {
141
192
  delete (requestPayload.extra_body as Record<string, unknown>).cached_content;
142
193
  delete (requestPayload.extra_body as Record<string, unknown>).cachedContent;
@@ -182,9 +233,23 @@ export function prepareGeminiRequest(
182
233
  };
183
234
  }
184
235
 
236
+ function toRequestUrlString(value: RequestInfo): string {
237
+ if (typeof value === "string") {
238
+ return value;
239
+ }
240
+ if (value instanceof URL) {
241
+ return value.toString();
242
+ }
243
+ const candidate = (value as Request).url;
244
+ if (candidate) {
245
+ return candidate;
246
+ }
247
+ return value.toString();
248
+ }
249
+
185
250
  /**
186
251
  * Normalizes Gemini responses: applies retry headers, extracts cache usage into headers,
187
- * rewrites preview errors, flattens streaming payloads, and logs debug metadata.
252
+ * rewrites preview errors, rewrites streaming payloads, and logs debug metadata.
188
253
  */
189
254
  export async function transformGeminiResponse(
190
255
  response: Response,
@@ -204,8 +269,22 @@ export async function transformGeminiResponse(
204
269
  }
205
270
 
206
271
  try {
207
- const text = await response.text();
208
272
  const headers = new Headers(response.headers);
273
+
274
+ if (streaming && response.ok && isEventStreamResponse && response.body) {
275
+ logGeminiDebugResponse(debugContext, response, {
276
+ note: "Streaming SSE payload (body omitted)",
277
+ headersOverride: headers,
278
+ });
279
+
280
+ return new Response(transformStreamingPayloadStream(response.body), {
281
+ status: response.status,
282
+ statusText: response.statusText,
283
+ headers,
284
+ });
285
+ }
286
+
287
+ const text = await response.text();
209
288
 
210
289
  if (!response.ok && text) {
211
290
  try {
@@ -238,12 +317,11 @@ export async function transformGeminiResponse(
238
317
  headers,
239
318
  };
240
319
 
241
- const usageFromSse = streaming && isEventStreamResponse ? extractUsageFromSsePayload(text) : null;
242
320
  const parsed: GeminiApiBody | null = !streaming || !isEventStreamResponse ? parseGeminiApiBody(text) : null;
243
321
  const patched = parsed ? rewriteGeminiPreviewAccessError(parsed, response.status, requestedModel) : null;
244
322
  const effectiveBody = patched ?? parsed ?? undefined;
245
323
 
246
- const usage = usageFromSse ?? (effectiveBody ? extractUsageMetadata(effectiveBody) : null);
324
+ const usage = effectiveBody ? extractUsageMetadata(effectiveBody) : null;
247
325
  if (usage?.cachedContentTokenCount !== undefined) {
248
326
  headers.set("x-gemini-cached-content-token-count", String(usage.cachedContentTokenCount));
249
327
  if (usage.totalTokenCount !== undefined) {
@@ -259,14 +337,10 @@ export async function transformGeminiResponse(
259
337
 
260
338
  logGeminiDebugResponse(debugContext, response, {
261
339
  body: text,
262
- note: streaming ? "Streaming SSE payload" : undefined,
340
+ note: streaming ? "Streaming SSE payload (buffered)" : undefined,
263
341
  headersOverride: headers,
264
342
  });
265
343
 
266
- if (streaming && response.ok && isEventStreamResponse) {
267
- return new Response(transformStreamingPayload(text), init);
268
- }
269
-
270
344
  if (!parsed) {
271
345
  return new Response(text, init);
272
346
  }
package/src/plugin.ts CHANGED
@@ -254,9 +254,13 @@ function openBrowserUrl(url: string): void {
254
254
  // Best-effort: don't block auth flow if spawning fails.
255
255
  const platform = process.platform;
256
256
  const command =
257
- platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
257
+ platform === "darwin"
258
+ ? "open"
259
+ : platform === "win32"
260
+ ? "rundll32"
261
+ : "xdg-open";
258
262
  const args =
259
- platform === "win32" ? ["/c", "start", "", url] : [url];
263
+ platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
260
264
  const child = spawn(command, args, {
261
265
  stdio: "ignore",
262
266
  detached: true,