opencode-gemini-auth 1.3.9 → 1.3.11

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.
@@ -1,164 +1,13 @@
1
- const GEMINI_PREVIEW_LINK = "https://goo.gle/enable-preview-features";
2
-
3
- export interface GeminiApiError {
4
- code?: number;
5
- message?: string;
6
- status?: string;
7
- details?: unknown[];
8
- [key: string]: unknown;
9
- }
10
-
11
- /**
12
- * Minimal representation of Gemini API responses we touch.
13
- */
14
- export interface GeminiApiBody {
15
- response?: unknown;
16
- error?: GeminiApiError;
17
- [key: string]: unknown;
18
- }
19
-
20
- interface GoogleRpcErrorInfo {
21
- "@type"?: string;
22
- reason?: string;
23
- domain?: string;
24
- metadata?: Record<string, string>;
25
- }
26
-
27
- interface GoogleRpcHelp {
28
- "@type"?: string;
29
- links?: Array<{
30
- description?: string;
31
- url?: string;
32
- }>;
33
- }
34
-
35
- interface GoogleRpcQuotaFailure {
36
- "@type"?: string;
37
- violations?: Array<{
38
- subject?: string;
39
- description?: string;
40
- }>;
41
- }
42
-
43
- interface GoogleRpcRetryInfo {
44
- "@type"?: string;
45
- retryDelay?: string | { seconds?: number; nanos?: number };
46
- }
47
-
48
- const CLOUDCODE_DOMAINS = [
49
- "cloudcode-pa.googleapis.com",
50
- "staging-cloudcode-pa.googleapis.com",
51
- "autopush-cloudcode-pa.googleapis.com",
52
- ];
53
-
54
- export interface GeminiErrorEnhancement {
55
- body?: GeminiApiBody;
56
- retryAfterMs?: number;
57
- }
58
-
59
- /**
60
- * Usage metadata exposed by Gemini responses. Fields are optional to reflect partial payloads.
61
- */
62
- export interface GeminiUsageMetadata {
63
- totalTokenCount?: number;
64
- promptTokenCount?: number;
65
- candidatesTokenCount?: number;
66
- cachedContentTokenCount?: number;
67
- }
68
-
69
- /**
70
- * Thinking configuration accepted by Gemini.
71
- * - Gemini 3 models use thinkingLevel (string: 'low', 'medium', 'high')
72
- * - Gemini 2.5 models use thinkingBudget (number)
73
- */
74
- export interface ThinkingConfig {
75
- thinkingBudget?: number;
76
- thinkingLevel?: string;
77
- includeThoughts?: boolean;
78
- }
79
-
80
- /**
81
- * Normalizes thinkingConfig - passes through values as-is without mapping.
82
- * User should use thinkingLevel for Gemini 3 and thinkingBudget for Gemini 2.5.
83
- */
84
- export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undefined {
85
- if (!config || typeof config !== "object") {
86
- return undefined;
87
- }
88
-
89
- const record = config as Record<string, unknown>;
90
- const budgetRaw = record.thinkingBudget ?? record.thinking_budget;
91
- const levelRaw = record.thinkingLevel ?? record.thinking_level;
92
- const includeRaw = record.includeThoughts ?? record.include_thoughts;
93
-
94
- const thinkingBudget = typeof budgetRaw === "number" && Number.isFinite(budgetRaw) ? budgetRaw : undefined;
95
- const thinkingLevel = typeof levelRaw === "string" && levelRaw.length > 0 ? levelRaw.toLowerCase() : undefined;
96
- const includeThoughts = typeof includeRaw === "boolean" ? includeRaw : undefined;
97
-
98
- if (thinkingBudget === undefined && thinkingLevel === undefined && includeThoughts === undefined) {
99
- return undefined;
100
- }
101
-
102
- const normalized: ThinkingConfig = {};
103
- if (thinkingBudget !== undefined) {
104
- normalized.thinkingBudget = thinkingBudget;
105
- }
106
- if (thinkingLevel !== undefined) {
107
- normalized.thinkingLevel = thinkingLevel;
108
- }
109
- if (includeThoughts !== undefined) {
110
- normalized.includeThoughts = includeThoughts;
111
- }
112
- return normalized;
113
- }
114
-
115
- /**
116
- * Parses a Gemini API body; handles array-wrapped responses the API sometimes returns.
117
- */
118
- export function parseGeminiApiBody(rawText: string): GeminiApiBody | null {
119
- try {
120
- const parsed = JSON.parse(rawText);
121
- if (Array.isArray(parsed)) {
122
- const firstObject = parsed.find((item: unknown) => typeof item === "object" && item !== null);
123
- if (firstObject && typeof firstObject === "object") {
124
- return firstObject as GeminiApiBody;
125
- }
126
- return null;
127
- }
128
-
129
- if (parsed && typeof parsed === "object") {
130
- return parsed as GeminiApiBody;
131
- }
132
-
133
- return null;
134
- } catch {
135
- return null;
136
- }
137
- }
138
-
139
- /**
140
- * Extracts usageMetadata from a response object, guarding types.
141
- */
142
- export function extractUsageMetadata(body: GeminiApiBody): GeminiUsageMetadata | null {
143
- const usage = (body.response && typeof body.response === "object"
144
- ? (body.response as { usageMetadata?: unknown }).usageMetadata
145
- : undefined) as GeminiUsageMetadata | undefined;
146
-
147
- if (!usage || typeof usage !== "object") {
148
- return null;
149
- }
150
-
151
- const asRecord = usage as Record<string, unknown>;
152
- const toNumber = (value: unknown): number | undefined =>
153
- typeof value === "number" && Number.isFinite(value) ? value : undefined;
154
-
155
- return {
156
- totalTokenCount: toNumber(asRecord.totalTokenCount),
157
- promptTokenCount: toNumber(asRecord.promptTokenCount),
158
- candidatesTokenCount: toNumber(asRecord.candidatesTokenCount),
159
- cachedContentTokenCount: toNumber(asRecord.cachedContentTokenCount),
160
- };
161
- }
1
+ import {
2
+ CLOUDCODE_DOMAINS,
3
+ GEMINI_PREVIEW_LINK,
4
+ type GeminiApiBody,
5
+ type GeminiErrorEnhancement,
6
+ type GoogleRpcErrorInfo,
7
+ type GoogleRpcHelp,
8
+ type GoogleRpcQuotaFailure,
9
+ type GoogleRpcRetryInfo,
10
+ } from "./types";
162
11
 
163
12
  /**
164
13
  * Enhances 404 errors for Gemini 3 models with a direct preview-access message.
@@ -172,7 +21,7 @@ export function rewriteGeminiPreviewAccessError(
172
21
  return null;
173
22
  }
174
23
 
175
- const error: GeminiApiError = body.error ?? {};
24
+ const error = body.error ?? {};
176
25
  const trimmedMessage = typeof error.message === "string" ? error.message.trim() : "";
177
26
  const messagePrefix = trimmedMessage.length > 0
178
27
  ? trimmedMessage
@@ -189,7 +38,7 @@ export function rewriteGeminiPreviewAccessError(
189
38
  }
190
39
 
191
40
  /**
192
- * Enhances Gemini error responses with friendly messages and retry hints.
41
+ * Enhances Gemini errors with validation/quota messaging and retry hints.
193
42
  */
194
43
  export function enhanceGeminiErrorResponse(
195
44
  body: GeminiApiBody,
@@ -245,11 +94,7 @@ export function enhanceGeminiErrorResponse(
245
94
  }
246
95
  }
247
96
 
248
- if (retryAfterMs !== undefined) {
249
- return { retryAfterMs };
250
- }
251
-
252
- return null;
97
+ return retryAfterMs !== undefined ? { retryAfterMs } : null;
253
98
  }
254
99
 
255
100
  function needsPreviewAccessOverride(
@@ -260,21 +105,14 @@ function needsPreviewAccessOverride(
260
105
  if (status !== 404) {
261
106
  return false;
262
107
  }
263
-
264
108
  if (isGeminiThreeModel(requestedModel)) {
265
109
  return true;
266
110
  }
267
-
268
- const errorMessage = typeof body.error?.message === "string" ? body.error.message : "";
269
- return isGeminiThreeModel(errorMessage);
111
+ return isGeminiThreeModel(typeof body.error?.message === "string" ? body.error.message : "");
270
112
  }
271
113
 
272
114
  function isGeminiThreeModel(target?: string): boolean {
273
- if (!target) {
274
- return false;
275
- }
276
-
277
- return /gemini[\s-]?3/i.test(target);
115
+ return !!target && /gemini[\s-]?3/i.test(target);
278
116
  }
279
117
 
280
118
  function extractValidationInfo(details: unknown[]): { link?: string; learnMore?: string } | null {
@@ -350,17 +188,17 @@ function extractQuotaInfo(details: unknown[]): { retryable: boolean } | null {
350
188
  (detail as GoogleRpcQuotaFailure)["@type"] === "type.googleapis.com/google.rpc.QuotaFailure",
351
189
  );
352
190
 
353
- if (quotaFailure?.violations?.length) {
354
- const description = quotaFailure.violations
355
- .map((violation) => violation.description?.toLowerCase() ?? "")
356
- .join(" ");
357
- if (description.includes("daily") || description.includes("per day")) {
358
- return { retryable: false };
359
- }
360
- return { retryable: true };
191
+ if (!quotaFailure?.violations?.length) {
192
+ return null;
361
193
  }
362
194
 
363
- return null;
195
+ const description = quotaFailure.violations
196
+ .map((violation) => violation.description?.toLowerCase() ?? "")
197
+ .join(" ");
198
+ if (description.includes("daily") || description.includes("per day")) {
199
+ return { retryable: false };
200
+ }
201
+ return { retryable: true };
364
202
  }
365
203
 
366
204
  function extractRetryDelay(details: unknown[], errorMessage?: string): number | null {
@@ -378,24 +216,23 @@ function extractRetryDelay(details: unknown[], errorMessage?: string): number |
378
216
  }
379
217
  }
380
218
 
381
- if (errorMessage) {
382
- const retryMatch = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/);
383
- if (retryMatch?.[1]) {
384
- return parseRetryDelayValue(retryMatch[1]);
385
- }
386
- const resetMatch = errorMessage.match(/after\s+([0-9.]+(?:ms|s))/i);
387
- if (resetMatch?.[1]) {
388
- return parseRetryDelayValue(resetMatch[1]);
389
- }
219
+ if (!errorMessage) {
220
+ return null;
221
+ }
222
+
223
+ const retryMatch = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/);
224
+ if (retryMatch?.[1]) {
225
+ return parseRetryDelayValue(retryMatch[1]);
226
+ }
227
+ const resetMatch = errorMessage.match(/after\s+([0-9.]+(?:ms|s))/i);
228
+ if (resetMatch?.[1]) {
229
+ return parseRetryDelayValue(resetMatch[1]);
390
230
  }
391
231
 
392
232
  return null;
393
233
  }
394
234
 
395
235
  function parseRetryDelayValue(value: string | { seconds?: number; nanos?: number }): number | null {
396
- if (!value) {
397
- return null;
398
- }
399
236
  if (typeof value === "string") {
400
237
  const trimmed = value.trim();
401
238
  if (!trimmed) {
@@ -0,0 +1,12 @@
1
+ export {
2
+ CLOUDCODE_DOMAINS,
3
+ GEMINI_PREVIEW_LINK,
4
+ type GeminiApiBody,
5
+ type GeminiApiError,
6
+ type GeminiErrorEnhancement,
7
+ type GeminiUsageMetadata,
8
+ type ThinkingConfig,
9
+ } from "./types";
10
+ export { normalizeThinkingConfig } from "./thinking";
11
+ export { parseGeminiApiBody, extractUsageMetadata } from "./parsing";
12
+ export { rewriteGeminiPreviewAccessError, enhanceGeminiErrorResponse } from "./errors";
@@ -0,0 +1,44 @@
1
+ import type { GeminiApiBody, GeminiUsageMetadata } from "./types";
2
+
3
+ /**
4
+ * Parses a Gemini API body; handles array-wrapped responses the API sometimes returns.
5
+ */
6
+ export function parseGeminiApiBody(rawText: string): GeminiApiBody | null {
7
+ try {
8
+ const parsed = JSON.parse(rawText);
9
+ if (Array.isArray(parsed)) {
10
+ const firstObject = parsed.find((item: unknown) => typeof item === "object" && item !== null);
11
+ return firstObject && typeof firstObject === "object"
12
+ ? (firstObject as GeminiApiBody)
13
+ : null;
14
+ }
15
+
16
+ return parsed && typeof parsed === "object" ? (parsed as GeminiApiBody) : null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Extracts usageMetadata from a response object, guarding types.
24
+ */
25
+ export function extractUsageMetadata(body: GeminiApiBody): GeminiUsageMetadata | null {
26
+ const usage = (body.response && typeof body.response === "object"
27
+ ? (body.response as { usageMetadata?: unknown }).usageMetadata
28
+ : undefined) as GeminiUsageMetadata | undefined;
29
+
30
+ if (!usage || typeof usage !== "object") {
31
+ return null;
32
+ }
33
+
34
+ const asRecord = usage as Record<string, unknown>;
35
+ const toNumber = (value: unknown): number | undefined =>
36
+ typeof value === "number" && Number.isFinite(value) ? value : undefined;
37
+
38
+ return {
39
+ totalTokenCount: toNumber(asRecord.totalTokenCount),
40
+ promptTokenCount: toNumber(asRecord.promptTokenCount),
41
+ candidatesTokenCount: toNumber(asRecord.candidatesTokenCount),
42
+ cachedContentTokenCount: toNumber(asRecord.cachedContentTokenCount),
43
+ };
44
+ }
@@ -0,0 +1,36 @@
1
+ import type { ThinkingConfig } from "./types";
2
+
3
+ /**
4
+ * Normalizes thinkingConfig aliases into canonical Gemini field names.
5
+ */
6
+ export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undefined {
7
+ if (!config || typeof config !== "object") {
8
+ return undefined;
9
+ }
10
+
11
+ const record = config as Record<string, unknown>;
12
+ const budgetRaw = record.thinkingBudget ?? record.thinking_budget;
13
+ const levelRaw = record.thinkingLevel ?? record.thinking_level;
14
+ const includeRaw = record.includeThoughts ?? record.include_thoughts;
15
+
16
+ const thinkingBudget = typeof budgetRaw === "number" && Number.isFinite(budgetRaw) ? budgetRaw : undefined;
17
+ const thinkingLevel = typeof levelRaw === "string" && levelRaw.length > 0 ? levelRaw.toLowerCase() : undefined;
18
+ const includeThoughts = typeof includeRaw === "boolean" ? includeRaw : undefined;
19
+
20
+ if (thinkingBudget === undefined && thinkingLevel === undefined && includeThoughts === undefined) {
21
+ return undefined;
22
+ }
23
+
24
+ const normalized: ThinkingConfig = {};
25
+ if (thinkingBudget !== undefined) {
26
+ normalized.thinkingBudget = thinkingBudget;
27
+ }
28
+ if (thinkingLevel !== undefined) {
29
+ normalized.thinkingLevel = thinkingLevel;
30
+ }
31
+ if (includeThoughts !== undefined) {
32
+ normalized.includeThoughts = includeThoughts;
33
+ }
34
+
35
+ return normalized;
36
+ }
@@ -0,0 +1,78 @@
1
+ export const GEMINI_PREVIEW_LINK = "https://goo.gle/enable-preview-features";
2
+
3
+ export interface GeminiApiError {
4
+ code?: number;
5
+ message?: string;
6
+ status?: string;
7
+ details?: unknown[];
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ /**
12
+ * Minimal representation of Gemini API responses we touch.
13
+ */
14
+ export interface GeminiApiBody {
15
+ response?: unknown;
16
+ error?: GeminiApiError;
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ export interface GeminiErrorEnhancement {
21
+ body?: GeminiApiBody;
22
+ retryAfterMs?: number;
23
+ }
24
+
25
+ /**
26
+ * Usage metadata exposed by Gemini responses. Fields are optional to reflect partial payloads.
27
+ */
28
+ export interface GeminiUsageMetadata {
29
+ totalTokenCount?: number;
30
+ promptTokenCount?: number;
31
+ candidatesTokenCount?: number;
32
+ cachedContentTokenCount?: number;
33
+ }
34
+
35
+ /**
36
+ * Thinking configuration accepted by Gemini.
37
+ * - Gemini 3 models use thinkingLevel (string: 'low', 'medium', 'high')
38
+ * - Gemini 2.5 models use thinkingBudget (number)
39
+ */
40
+ export interface ThinkingConfig {
41
+ thinkingBudget?: number;
42
+ thinkingLevel?: string;
43
+ includeThoughts?: boolean;
44
+ }
45
+
46
+ export interface GoogleRpcErrorInfo {
47
+ "@type"?: string;
48
+ reason?: string;
49
+ domain?: string;
50
+ metadata?: Record<string, string>;
51
+ }
52
+
53
+ export interface GoogleRpcHelp {
54
+ "@type"?: string;
55
+ links?: Array<{
56
+ description?: string;
57
+ url?: string;
58
+ }>;
59
+ }
60
+
61
+ export interface GoogleRpcQuotaFailure {
62
+ "@type"?: string;
63
+ violations?: Array<{
64
+ subject?: string;
65
+ description?: string;
66
+ }>;
67
+ }
68
+
69
+ export interface GoogleRpcRetryInfo {
70
+ "@type"?: string;
71
+ retryDelay?: string | { seconds?: number; nanos?: number };
72
+ }
73
+
74
+ export const CLOUDCODE_DOMAINS = [
75
+ "cloudcode-pa.googleapis.com",
76
+ "staging-cloudcode-pa.googleapis.com",
77
+ "autopush-cloudcode-pa.googleapis.com",
78
+ ];
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { enhanceGeminiErrorResponse } from "./request-helpers";
4
+
5
+ describe("enhanceGeminiErrorResponse", () => {
6
+ it("adds retry hint and rate-limit message for 429 rate limits", () => {
7
+ const body = {
8
+ error: {
9
+ message: "rate limited",
10
+ details: [
11
+ {
12
+ "@type": "type.googleapis.com/google.rpc.ErrorInfo",
13
+ reason: "RATE_LIMIT_EXCEEDED",
14
+ domain: "cloudcode-pa.googleapis.com",
15
+ },
16
+ {
17
+ "@type": "type.googleapis.com/google.rpc.RetryInfo",
18
+ retryDelay: "5s",
19
+ },
20
+ ],
21
+ },
22
+ };
23
+
24
+ const result = enhanceGeminiErrorResponse(body, 429);
25
+ expect(result?.retryAfterMs).toBe(5000);
26
+ expect(result?.body?.error?.message).toContain("Rate limit exceeded");
27
+ });
28
+
29
+ it("adds quota exhausted message for terminal limits", () => {
30
+ const body = {
31
+ error: {
32
+ message: "quota exhausted",
33
+ details: [
34
+ {
35
+ "@type": "type.googleapis.com/google.rpc.ErrorInfo",
36
+ reason: "QUOTA_EXHAUSTED",
37
+ domain: "cloudcode-pa.googleapis.com",
38
+ },
39
+ ],
40
+ },
41
+ };
42
+
43
+ const result = enhanceGeminiErrorResponse(body, 429);
44
+ expect(result?.body?.error?.message).toContain("Quota exhausted");
45
+ expect(result?.retryAfterMs).toBeUndefined();
46
+ });
47
+
48
+ it("adds validation links for VALIDATION_REQUIRED errors", () => {
49
+ const body = {
50
+ error: {
51
+ message: "validation required",
52
+ details: [
53
+ {
54
+ "@type": "type.googleapis.com/google.rpc.ErrorInfo",
55
+ reason: "VALIDATION_REQUIRED",
56
+ domain: "cloudcode-pa.googleapis.com",
57
+ },
58
+ {
59
+ "@type": "type.googleapis.com/google.rpc.Help",
60
+ links: [
61
+ { url: "https://example.com/validate" },
62
+ { description: "Learn more", url: "https://support.google.com/help" },
63
+ ],
64
+ },
65
+ ],
66
+ },
67
+ };
68
+
69
+ const result = enhanceGeminiErrorResponse(body, 403);
70
+ expect(result?.body?.error?.message).toContain("Complete validation: https://example.com/validate");
71
+ expect(result?.body?.error?.message).toContain("Learn more: https://support.google.com/help");
72
+ });
73
+
74
+ it("extracts retry delay from message text when details are missing", () => {
75
+ const body = {
76
+ error: {
77
+ message: "Please retry in 2s",
78
+ },
79
+ };
80
+
81
+ const result = enhanceGeminiErrorResponse(body, 503);
82
+ expect(result?.retryAfterMs).toBe(2000);
83
+ });
84
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { GEMINI_CODE_ASSIST_ENDPOINT } from "../constants";
4
+ import {
5
+ isGenerativeLanguageRequest,
6
+ prepareGeminiRequest,
7
+ transformGeminiResponse,
8
+ } from "./request";
9
+
10
+ describe("request helpers", () => {
11
+ it("detects generativelanguage URLs", () => {
12
+ expect(
13
+ isGenerativeLanguageRequest(
14
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent",
15
+ ),
16
+ ).toBe(true);
17
+ expect(isGenerativeLanguageRequest("https://example.com/foo")).toBe(false);
18
+ });
19
+
20
+ it("wraps requests for Gemini Code Assist streaming", () => {
21
+ const input =
22
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:streamGenerateContent";
23
+ const init: RequestInit = {
24
+ method: "POST",
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ "x-api-key": "should-be-removed",
28
+ },
29
+ body: JSON.stringify({
30
+ contents: [{ role: "user", parts: [{ text: "hi" }] }],
31
+ system_instruction: { parts: [{ text: "system" }] },
32
+ }),
33
+ };
34
+
35
+ const result = prepareGeminiRequest(input, init, "token-123", "project-456");
36
+
37
+ expect(result.streaming).toBe(true);
38
+ expect(typeof result.request).toBe("string");
39
+ expect(result.request).toBe(
40
+ `${GEMINI_CODE_ASSIST_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`,
41
+ );
42
+
43
+ const headers = new Headers(result.init.headers);
44
+ expect(headers.get("Authorization")).toBe("Bearer token-123");
45
+ expect(headers.get("x-api-key")).toBeNull();
46
+ expect(headers.get("Accept")).toBe("text/event-stream");
47
+ expect(headers.get("x-activity-request-id")).toBeTruthy();
48
+
49
+ const parsed = JSON.parse(result.init.body as string) as Record<string, unknown>;
50
+ expect(parsed.project).toBe("project-456");
51
+ expect(parsed.model).toBe("gemini-3-flash-preview");
52
+ expect(parsed.user_prompt_id).toBeTruthy();
53
+ expect((parsed.request as Record<string, unknown>).session_id).toBeTruthy();
54
+ expect((parsed.request as Record<string, unknown>).systemInstruction).toBeDefined();
55
+ expect((parsed.request as Record<string, unknown>).system_instruction).toBeUndefined();
56
+ });
57
+
58
+ it("maps traceId to responseId for JSON responses", async () => {
59
+ const response = new Response(
60
+ JSON.stringify({
61
+ traceId: "trace-123",
62
+ response: {
63
+ candidates: [],
64
+ },
65
+ }),
66
+ {
67
+ status: 200,
68
+ headers: { "content-type": "application/json" },
69
+ },
70
+ );
71
+
72
+ const transformed = await transformGeminiResponse(response, false);
73
+ const parsed = (await transformed.json()) as Record<string, unknown>;
74
+ expect(parsed.responseId).toBe("trace-123");
75
+ });
76
+
77
+ it("maps traceId to responseId for streaming payloads", async () => {
78
+ const response = new Response(
79
+ 'data: {"traceId":"trace-456","response":{"candidates":[]}}\n\n',
80
+ {
81
+ status: 200,
82
+ headers: { "content-type": "text/event-stream" },
83
+ },
84
+ );
85
+
86
+ const transformed = await transformGeminiResponse(response, true);
87
+ const payload = await transformed.text();
88
+ expect(payload).toContain('"responseId":"trace-456"');
89
+ expect(payload).not.toContain('"traceId"');
90
+ });
91
+ });