@jx-grxf/patchpilot 1.0.0 → 1.2.0

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.
Files changed (154) hide show
  1. package/README.md +51 -16
  2. package/dist/cli.js +46 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/agent.d.ts +44 -1
  5. package/dist/core/agent.js +617 -70
  6. package/dist/core/agent.js.map +1 -1
  7. package/dist/core/clipboard.d.ts +14 -0
  8. package/dist/core/clipboard.js +134 -0
  9. package/dist/core/clipboard.js.map +1 -0
  10. package/dist/core/codex.d.ts +8 -0
  11. package/dist/core/codex.js +28 -2
  12. package/dist/core/codex.js.map +1 -1
  13. package/dist/core/compaction.d.ts +23 -0
  14. package/dist/core/compaction.js +145 -0
  15. package/dist/core/compaction.js.map +1 -0
  16. package/dist/core/contextFormat.d.ts +21 -0
  17. package/dist/core/contextFormat.js +87 -0
  18. package/dist/core/contextFormat.js.map +1 -0
  19. package/dist/core/contextItem.d.ts +41 -0
  20. package/dist/core/contextItem.js +93 -0
  21. package/dist/core/contextItem.js.map +1 -0
  22. package/dist/core/contextStore.d.ts +48 -0
  23. package/dist/core/contextStore.js +306 -0
  24. package/dist/core/contextStore.js.map +1 -0
  25. package/dist/core/doctor.js +9 -8
  26. package/dist/core/doctor.js.map +1 -1
  27. package/dist/core/gemini.js +10 -4
  28. package/dist/core/gemini.js.map +1 -1
  29. package/dist/core/geminiWrapper.d.ts +43 -2
  30. package/dist/core/geminiWrapper.js +582 -42
  31. package/dist/core/geminiWrapper.js.map +1 -1
  32. package/dist/core/http.js +70 -6
  33. package/dist/core/http.js.map +1 -1
  34. package/dist/core/json.d.ts +1 -1
  35. package/dist/core/json.js +18 -20
  36. package/dist/core/json.js.map +1 -1
  37. package/dist/core/nvidia.d.ts +1 -1
  38. package/dist/core/nvidia.js +13 -4
  39. package/dist/core/nvidia.js.map +1 -1
  40. package/dist/core/ollama.js +13 -3
  41. package/dist/core/ollama.js.map +1 -1
  42. package/dist/core/openrouter.js +15 -6
  43. package/dist/core/openrouter.js.map +1 -1
  44. package/dist/core/reasoning.js +3 -0
  45. package/dist/core/reasoning.js.map +1 -1
  46. package/dist/core/session.js +9 -3
  47. package/dist/core/session.js.map +1 -1
  48. package/dist/core/tokenAccounting.d.ts +4 -0
  49. package/dist/core/tokenAccounting.js +75 -13
  50. package/dist/core/tokenAccounting.js.map +1 -1
  51. package/dist/core/types.d.ts +58 -3
  52. package/dist/core/types.js +30 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/core/updateCheck.d.ts +19 -0
  55. package/dist/core/updateCheck.js +103 -0
  56. package/dist/core/updateCheck.js.map +1 -0
  57. package/dist/core/workspace.d.ts +29 -0
  58. package/dist/core/workspace.js +1271 -92
  59. package/dist/core/workspace.js.map +1 -1
  60. package/dist/tui/App.d.ts +1 -0
  61. package/dist/tui/App.js +1346 -112
  62. package/dist/tui/App.js.map +1 -1
  63. package/dist/tui/commands.js +109 -6
  64. package/dist/tui/commands.js.map +1 -1
  65. package/dist/tui/components/ApprovalPanel.js +16 -1
  66. package/dist/tui/components/ApprovalPanel.js.map +1 -1
  67. package/dist/tui/components/CommandSuggestions.js +26 -3
  68. package/dist/tui/components/CommandSuggestions.js.map +1 -1
  69. package/dist/tui/components/Composer.d.ts +3 -0
  70. package/dist/tui/components/Composer.js +57 -5
  71. package/dist/tui/components/Composer.js.map +1 -1
  72. package/dist/tui/components/ExperimentalPanel.d.ts +1 -1
  73. package/dist/tui/components/ExperimentalPanel.js +5 -0
  74. package/dist/tui/components/ExperimentalPanel.js.map +1 -1
  75. package/dist/tui/components/OnboardingPanel.d.ts +12 -0
  76. package/dist/tui/components/OnboardingPanel.js +69 -21
  77. package/dist/tui/components/OnboardingPanel.js.map +1 -1
  78. package/dist/tui/components/StartupBanner.d.ts +4 -0
  79. package/dist/tui/components/StartupBanner.js +9 -0
  80. package/dist/tui/components/StartupBanner.js.map +1 -0
  81. package/dist/tui/components/Transcript.d.ts +7 -0
  82. package/dist/tui/components/Transcript.js +86 -16
  83. package/dist/tui/components/Transcript.js.map +1 -1
  84. package/dist/tui/contextCommands.d.ts +8 -0
  85. package/dist/tui/contextCommands.js +205 -0
  86. package/dist/tui/contextCommands.js.map +1 -0
  87. package/dist/tui/experimental/AnimatedText.d.ts +38 -0
  88. package/dist/tui/experimental/AnimatedText.js +55 -0
  89. package/dist/tui/experimental/AnimatedText.js.map +1 -0
  90. package/dist/tui/experimental/Banner.d.ts +10 -0
  91. package/dist/tui/experimental/Banner.js +33 -0
  92. package/dist/tui/experimental/Banner.js.map +1 -0
  93. package/dist/tui/experimental/CommandPalette.d.ts +11 -0
  94. package/dist/tui/experimental/CommandPalette.js +25 -0
  95. package/dist/tui/experimental/CommandPalette.js.map +1 -0
  96. package/dist/tui/experimental/ExperimentalShell.d.ts +58 -0
  97. package/dist/tui/experimental/ExperimentalShell.js +366 -0
  98. package/dist/tui/experimental/ExperimentalShell.js.map +1 -0
  99. package/dist/tui/experimental/ThemePicker.d.ts +13 -0
  100. package/dist/tui/experimental/ThemePicker.js +12 -0
  101. package/dist/tui/experimental/ThemePicker.js.map +1 -0
  102. package/dist/tui/experimental/attachments.d.ts +35 -0
  103. package/dist/tui/experimental/attachments.js +244 -0
  104. package/dist/tui/experimental/attachments.js.map +1 -0
  105. package/dist/tui/experimental/composer.d.ts +24 -0
  106. package/dist/tui/experimental/composer.js +84 -0
  107. package/dist/tui/experimental/composer.js.map +1 -0
  108. package/dist/tui/experimental/geminiPricing.d.ts +16 -0
  109. package/dist/tui/experimental/geminiPricing.js +39 -0
  110. package/dist/tui/experimental/geminiPricing.js.map +1 -0
  111. package/dist/tui/experimental/layout.d.ts +46 -0
  112. package/dist/tui/experimental/layout.js +112 -0
  113. package/dist/tui/experimental/layout.js.map +1 -0
  114. package/dist/tui/experimental/theme.d.ts +35 -0
  115. package/dist/tui/experimental/theme.js +86 -0
  116. package/dist/tui/experimental/theme.js.map +1 -0
  117. package/dist/tui/experimental/transcriptRows.d.ts +20 -0
  118. package/dist/tui/experimental/transcriptRows.js +169 -0
  119. package/dist/tui/experimental/transcriptRows.js.map +1 -0
  120. package/dist/tui/experimental/ultraModes.d.ts +46 -0
  121. package/dist/tui/experimental/ultraModes.js +95 -0
  122. package/dist/tui/experimental/ultraModes.js.map +1 -0
  123. package/dist/tui/experimental/ultramaxx.d.ts +19 -0
  124. package/dist/tui/experimental/ultramaxx.js +43 -0
  125. package/dist/tui/experimental/ultramaxx.js.map +1 -0
  126. package/dist/tui/format.d.ts +4 -2
  127. package/dist/tui/format.js +14 -0
  128. package/dist/tui/format.js.map +1 -1
  129. package/dist/tui/hosts.js +7 -1
  130. package/dist/tui/hosts.js.map +1 -1
  131. package/dist/tui/layout.d.ts +26 -0
  132. package/dist/tui/layout.js +66 -0
  133. package/dist/tui/layout.js.map +1 -0
  134. package/dist/tui/modelSelection.d.ts +1 -1
  135. package/dist/tui/modelSelection.js +8 -6
  136. package/dist/tui/modelSelection.js.map +1 -1
  137. package/dist/tui/modes.d.ts +7 -0
  138. package/dist/tui/modes.js +12 -0
  139. package/dist/tui/modes.js.map +1 -1
  140. package/dist/tui/onboardingPreferences.d.ts +37 -0
  141. package/dist/tui/onboardingPreferences.js +118 -0
  142. package/dist/tui/onboardingPreferences.js.map +1 -0
  143. package/dist/tui/runStatus.d.ts +50 -0
  144. package/dist/tui/runStatus.js +164 -0
  145. package/dist/tui/runStatus.js.map +1 -0
  146. package/dist/tui/types.d.ts +8 -0
  147. package/dist/tui/types.js.map +1 -1
  148. package/docs/architecture.md +115 -0
  149. package/docs/gemini-wrapper.md +23 -0
  150. package/docs/product-context.md +43 -0
  151. package/docs/releases/v1.0.1.md +25 -0
  152. package/docs/releases/v1.1.0.md +30 -0
  153. package/docs/releases/v1.2.0.md +28 -0
  154. package/package.json +4 -2
@@ -5,8 +5,39 @@ import { getPatchPilotConfigDir } from "./env.js";
5
5
  import { fetchWithTimeout } from "./http.js";
6
6
  import { attachTokenCost, estimateTokens } from "./tokenAccounting.js";
7
7
  export const defaultGeminiWrapperModel = "auto";
8
+ // Gemini 3 has no flash-lite tier — gemini_webapi 2.0.0 exposes only
9
+ // pro / flash / flash-thinking. Offering "flash-lite" advertised a model that
10
+ // does not exist and hard-failed every request, so it is no longer a shortcut.
11
+ export const geminiWrapperShortcutModels = ["auto", "flash", "pro"];
12
+ export const geminiWrapperLegacyModels = ["thinking"];
13
+ export const geminiWrapperCuratedModels = [...geminiWrapperShortcutModels, ...geminiWrapperLegacyModels];
8
14
  export const geminiWebApiVersion = "2.0.0";
9
- export const geminiWebApiInstallCommand = `PatchPilot managed install: python3 -m venv ~/.patchpilot/gemini-wrapper-venv && ~/.patchpilot/gemini-wrapper-venv/bin/python -m pip install gemini_webapi==${geminiWebApiVersion}`;
15
+ export const geminiWebApiInstallCommand = process.platform === "win32"
16
+ ? `PatchPilot managed install: python -m venv %USERPROFILE%\\.patchpilot\\gemini-wrapper-venv && %USERPROFILE%\\.patchpilot\\gemini-wrapper-venv\\Scripts\\python.exe -m pip install gemini_webapi==${geminiWebApiVersion} browser-cookie3`
17
+ : `PatchPilot managed install: python3 -m venv ~/.patchpilot/gemini-wrapper-venv && ~/.patchpilot/gemini-wrapper-venv/bin/python -m pip install gemini_webapi==${geminiWebApiVersion} browser-cookie3`;
18
+ const pythonBridgeReadyTtlMs = 5 * 60_000;
19
+ const geminiBrowserCookieImportTimeoutMs = 60_000;
20
+ const geminiBridgeOutputMaxBytes = 2 * 1024 * 1024;
21
+ const geminiWrapperBrowserCookieNames = new Set([
22
+ "__Secure-1PSID",
23
+ "__Secure-1PSIDTS",
24
+ "__Secure-1PSIDCC",
25
+ "__Secure-1PAPISID",
26
+ "__Secure-3PSID",
27
+ "__Secure-3PSIDTS",
28
+ "__Secure-3PSIDCC",
29
+ "__Secure-3PAPISID",
30
+ "__Secure-ENID",
31
+ "AEC",
32
+ "COMPASS",
33
+ "GOOGLE_ABUSE_EXEMPTION",
34
+ "NID",
35
+ "SID",
36
+ "HSID",
37
+ "SSID",
38
+ "APISID",
39
+ "SAPISID"
40
+ ]);
10
41
  export class GeminiWrapperClient {
11
42
  baseUrl;
12
43
  apiKey;
@@ -14,6 +45,9 @@ export class GeminiWrapperClient {
14
45
  mode;
15
46
  pythonCommand;
16
47
  cookiesJson;
48
+ modelDescriptorCache = null;
49
+ pythonBridgeReadyUntil = 0;
50
+ pythonBridgeReadyPromise = null;
17
51
  constructor(baseUrl = readGeminiWrapperBaseUrl(), apiKey = readGeminiWrapperApiKey(), runtimeOptions = readGeminiWrapperRuntimeOptions(), mode = readGeminiWrapperMode(), pythonCommand = readGeminiWrapperPythonCommand(), cookiesJson = readGeminiWrapperCookiesJson()) {
18
52
  this.baseUrl = baseUrl.replace(/\/$/, "");
19
53
  this.apiKey = apiKey;
@@ -41,9 +75,9 @@ export class GeminiWrapperClient {
41
75
  signal: options.signal
42
76
  });
43
77
  const durationMs = Date.now() - startedAt;
44
- const payload = (await readJsonSafely(response));
78
+ const { payload, text } = await readGeminiWrapperResponse(response);
45
79
  if (!response.ok || payload.error) {
46
- const reason = payload.error?.message ? ` ${payload.error.message}` : "";
80
+ const reason = formatGeminiWrapperErrorReason(payload, text, response);
47
81
  if (response.status === 401 || response.status === 403) {
48
82
  throw new Error("Gemini-Wrapper authentication failed. Check PATCHPILOT_GEMINI_WRAPPER_API_KEY.");
49
83
  }
@@ -62,6 +96,12 @@ export class GeminiWrapperClient {
62
96
  };
63
97
  }
64
98
  async listModels() {
99
+ return (await this.listModelDescriptors()).map((model) => model.id);
100
+ }
101
+ async listModelDescriptors() {
102
+ if (this.modelDescriptorCache && this.modelDescriptorCache.expiresAt > Date.now()) {
103
+ return this.modelDescriptorCache.descriptors;
104
+ }
65
105
  if (this.usesPythonBridge()) {
66
106
  await this.assertPythonBridgeReady();
67
107
  const result = await this.runPythonBridge({
@@ -71,29 +111,59 @@ export class GeminiWrapperClient {
71
111
  if (result.error) {
72
112
  throw new Error(result.error);
73
113
  }
74
- const models = [
75
- ...new Set((result.models ?? [])
76
- .map((model) => model.trim())
77
- .filter((model) => model && isLikelyGeminiWrapperChatModel(model)))
78
- ];
79
- return [defaultGeminiWrapperModel, ...models.filter((model) => model !== defaultGeminiWrapperModel)];
114
+ const descriptors = normalizeGeminiWrapperModelDescriptors(result.modelDescriptors && result.modelDescriptors.length > 0 ? result.modelDescriptors : result.models ?? []);
115
+ const mergedDescriptors = mergeGeminiWrapperModelDescriptors(descriptors);
116
+ this.modelDescriptorCache = {
117
+ descriptors: mergedDescriptors,
118
+ expiresAt: Date.now() + 5 * 60_000
119
+ };
120
+ return mergedDescriptors;
80
121
  }
81
122
  this.assertConfigured();
82
123
  const response = await this.fetchGeminiWrapper("/models", {
83
124
  headers: this.headers()
84
125
  });
85
- const payload = (await readJsonSafely(response));
126
+ const { payload, text } = await readGeminiWrapperResponse(response);
86
127
  if (!response.ok || payload.error) {
87
- const reason = payload.error?.message ? ` ${payload.error.message}` : "";
128
+ const reason = formatGeminiWrapperErrorReason(payload, text, response);
88
129
  throw new Error(`Gemini-Wrapper models failed with HTTP ${response.status}.${reason}`);
89
130
  }
90
- const models = [
91
- ...new Set(payload.data
92
- ?.map((model) => model.id?.trim())
93
- .filter((model) => Boolean(model))
94
- .filter(isLikelyGeminiWrapperChatModel) ?? [])
95
- ].sort();
96
- return models.length > 0 ? models : [defaultGeminiWrapperModel];
131
+ const descriptors = normalizeGeminiWrapperModelDescriptors(payload.data
132
+ ?.map((model) => model.id?.trim())
133
+ .filter((model) => Boolean(model))
134
+ .filter(isLikelyGeminiWrapperChatModel) ?? []);
135
+ const mergedDescriptors = mergeGeminiWrapperModelDescriptors(descriptors);
136
+ this.modelDescriptorCache = {
137
+ descriptors: mergedDescriptors,
138
+ expiresAt: Date.now() + 5 * 60_000
139
+ };
140
+ return mergedDescriptors;
141
+ }
142
+ supportsFileAnalysis() {
143
+ return this.usesPythonBridge();
144
+ }
145
+ async analyzeFile(options) {
146
+ if (!this.usesPythonBridge()) {
147
+ throw new Error("Gemini-Wrapper file analysis is only available through the managed Python Gemini-API bridge.");
148
+ }
149
+ await this.assertPythonBridgeReady();
150
+ const startedAt = Date.now();
151
+ const bridgeModel = await this.resolveGeminiWrapperBridgeModel(options.model);
152
+ const result = await this.runPythonBridge({
153
+ command: "chat",
154
+ model: bridgeModel,
155
+ prompt: options.prompt,
156
+ files: [options.path]
157
+ }, options.signal, getGeminiWrapperBridgeTimeoutMs(bridgeModel || defaultGeminiWrapperModel, this.runtimeOptions.bridgeTimeoutMs));
158
+ const durationMs = Date.now() - startedAt;
159
+ const content = result.content?.trim() ?? "";
160
+ if (!content) {
161
+ throw new Error(result.error ? `Gemini-API bridge failed: ${result.error}` : "Gemini-API bridge returned an empty file analysis response.");
162
+ }
163
+ return {
164
+ content,
165
+ telemetry: toEstimatedTelemetry(`${options.prompt}\nFILE:${options.path}`, content, durationMs, result.model ?? options.model)
166
+ };
97
167
  }
98
168
  async checkBridgeAuth() {
99
169
  await this.assertPythonBridgeReady();
@@ -104,12 +174,15 @@ export class GeminiWrapperClient {
104
174
  if (result.error) {
105
175
  throw new Error(result.error);
106
176
  }
177
+ if (isUnauthenticatedGeminiWebStatus(result.accountStatus)) {
178
+ throw new Error("Gemini-API bridge cookies are expired or unauthenticated. Refresh Gemini-Wrapper cookies.");
179
+ }
107
180
  }
108
181
  async fetchGeminiWrapper(path, init) {
109
182
  try {
110
183
  return await fetchWithTimeout(`${this.baseUrl}${path}`, init, {
111
184
  timeoutMs: init?.method === "POST" ? 90_000 : 8000,
112
- retries: init?.method === "POST" ? 0 : 1,
185
+ retries: init?.method === "POST" ? 2 : 1,
113
186
  label: `Gemini-Wrapper ${path}`
114
187
  });
115
188
  }
@@ -142,7 +215,7 @@ export class GeminiWrapperClient {
142
215
  await this.assertPythonBridgeReady();
143
216
  const startedAt = Date.now();
144
217
  const prompt = toBridgePrompt(options.messages, options.formatJson);
145
- const bridgeModel = normalizeGeminiWrapperBridgeModel(options.model);
218
+ const bridgeModel = await this.resolveGeminiWrapperBridgeModel(options.model);
146
219
  const result = await this.runPythonBridge({
147
220
  command: "chat",
148
221
  model: bridgeModel,
@@ -178,14 +251,46 @@ export class GeminiWrapperClient {
178
251
  proxy: process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy
179
252
  }, signal, this.runtimeOptions.bridgeMinIntervalMs, timeoutMs);
180
253
  }
254
+ async resolveGeminiWrapperBridgeModel(model) {
255
+ const normalizedModel = normalizeGeminiWrapperModel(model).trim();
256
+ if (normalizedModel === defaultGeminiWrapperModel || normalizedModel === "gemini-web-default") {
257
+ return "";
258
+ }
259
+ if (isGeminiWrapperShortcutModel(normalizedModel)) {
260
+ const descriptors = await this.getCachedModelDescriptors().catch(() => []);
261
+ const descriptor = resolveGeminiWrapperShortcutDescriptor(normalizedModel, descriptors);
262
+ if (descriptor) {
263
+ return descriptor.id;
264
+ }
265
+ }
266
+ return normalizeGeminiWrapperBridgeModelFallback(normalizedModel);
267
+ }
268
+ async getCachedModelDescriptors() {
269
+ if (this.modelDescriptorCache && this.modelDescriptorCache.expiresAt > Date.now()) {
270
+ return this.modelDescriptorCache.descriptors;
271
+ }
272
+ return await this.listModelDescriptors();
273
+ }
181
274
  async assertPythonBridgeReady() {
182
275
  if (!this.cookiesJson && !readGeminiWrapperSecure1psid()) {
183
- throw new Error("Gemini-API bridge needs explicit auth. Set PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON to a JSON cookie file, or set GEMINI_SECURE_1PSID / GEMINI_SECURE_1PSIDTS. PatchPilot will not scan browser cookies.");
276
+ throw new Error("Gemini-API bridge needs explicit auth. Run `patchpilot gemini-wrapper import-cookies`, set PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON to a JSON cookie file, or set GEMINI_SECURE_1PSID / GEMINI_SECURE_1PSIDTS.");
184
277
  }
278
+ if (this.pythonBridgeReadyUntil > Date.now()) {
279
+ return;
280
+ }
281
+ if (!this.pythonBridgeReadyPromise) {
282
+ this.pythonBridgeReadyPromise = this.checkPythonBridgeReady().finally(() => {
283
+ this.pythonBridgeReadyPromise = null;
284
+ });
285
+ }
286
+ await this.pythonBridgeReadyPromise;
287
+ }
288
+ async checkPythonBridgeReady() {
185
289
  const installed = await isGeminiWebApiInstalled(this.pythonCommand);
186
290
  if (!installed) {
187
291
  throw new Error(`Gemini-API Python wrapper is not installed for ${this.pythonCommand}. Run /doctor fix or patchpilot doctor --fix to install the pinned managed bridge. Manual fallback: ${geminiWebApiInstallCommand}`);
188
292
  }
293
+ this.pythonBridgeReadyUntil = Date.now() + pythonBridgeReadyTtlMs;
189
294
  }
190
295
  }
191
296
  export function readGeminiWrapperMode(env = process.env) {
@@ -230,11 +335,65 @@ export function saveGeminiWrapperCookieFile(values, env = process.env) {
230
335
  clearGeminiWrapperCookieCache(env);
231
336
  return cookiesPath;
232
337
  }
338
+ export function saveGeminiWrapperCookieJarFile(cookies, env = process.env) {
339
+ const sanitizedCookies = sanitizeGeminiWrapperBrowserCookies(cookies);
340
+ if (!sanitizedCookies.some((cookie) => cookie.name === "__Secure-1PSID")) {
341
+ throw new Error("Imported Gemini browser cookies did not include __Secure-1PSID.");
342
+ }
343
+ const configDir = getPatchPilotConfigDir(env);
344
+ mkdirSync(configDir, {
345
+ recursive: true,
346
+ mode: 0o700
347
+ });
348
+ tryChmod(configDir, 0o700);
349
+ const cookiesPath = getDefaultGeminiWrapperCookiesPath(env);
350
+ writeFileSync(cookiesPath, `${JSON.stringify({
351
+ cookies: sanitizedCookies
352
+ }, null, 2)}\n`, {
353
+ encoding: "utf8",
354
+ mode: 0o600
355
+ });
356
+ tryChmod(cookiesPath, 0o600);
357
+ clearGeminiWrapperCookieCache(env);
358
+ return cookiesPath;
359
+ }
360
+ export async function importGeminiWrapperBrowserCookies(options = {}) {
361
+ const env = options.env ?? process.env;
362
+ const pythonCommand = options.pythonCommand ?? readGeminiWrapperPythonCommand(env);
363
+ const isInstalled = await ensureGeminiWebApiInstalled(pythonCommand, env);
364
+ if (!isInstalled) {
365
+ throw new Error(`Gemini browser cookie import needs the managed bridge. Run /doctor fix or install manually: ${geminiWebApiInstallCommand}`);
366
+ }
367
+ if (!(await isGeminiBrowserCookieImportInstalled(pythonCommand))) {
368
+ throw new Error("Gemini browser cookie import needs the optional browser-cookie3 dependency. Run `patchpilot doctor --provider gemini-wrapper --fix`, then retry.");
369
+ }
370
+ const output = await runGeminiBrowserCookieImportBridge(pythonCommand, options.timeoutMs ?? geminiBrowserCookieImportTimeoutMs);
371
+ if (output.error) {
372
+ throw new Error(output.error);
373
+ }
374
+ const cookies = sanitizeGeminiWrapperBrowserCookies(output.cookies ?? []);
375
+ const hasSecure1psid = cookies.some((cookie) => cookie.name === "__Secure-1PSID");
376
+ const hasSecure1psidts = cookies.some((cookie) => cookie.name === "__Secure-1PSIDTS");
377
+ if (!hasSecure1psid) {
378
+ throw new Error("No __Secure-1PSID cookie was found in supported browsers. Sign in to Gemini in a supported browser, then retry the explicit import.");
379
+ }
380
+ const cookiesPath = saveGeminiWrapperCookieJarFile(cookies, env);
381
+ return {
382
+ cookiesPath,
383
+ cookieCount: cookies.length,
384
+ source: output.source ?? "browser",
385
+ availableSources: output.availableSources ?? [],
386
+ hasSecure1psid,
387
+ hasSecure1psidts
388
+ };
389
+ }
233
390
  export function readGeminiWrapperPythonCommand(env = process.env) {
234
391
  return env.PATCHPILOT_GEMINI_WRAPPER_PYTHON?.trim() || getManagedGeminiWrapperPythonPath(env);
235
392
  }
236
393
  export function readGeminiWrapperBootstrapPythonCommand(env = process.env) {
237
- return env.PATCHPILOT_GEMINI_WRAPPER_BOOTSTRAP_PYTHON?.trim() || "python3";
394
+ // Windows ships the `python` launcher (or the `py` redirector); `python3` is
395
+ // a POSIX convention and is usually not on PATH there.
396
+ return env.PATCHPILOT_GEMINI_WRAPPER_BOOTSTRAP_PYTHON?.trim() || (process.platform === "win32" ? "python" : "python3");
238
397
  }
239
398
  export function getGeminiWrapperVenvDir(env = process.env) {
240
399
  return path.join(getPatchPilotConfigDir(env), "gemini-wrapper-venv");
@@ -270,8 +429,12 @@ export async function isGeminiWebApiInstalled(pythonCommand = readGeminiWrapperP
270
429
  const result = await runQuietCommand(pythonCommand, ["-c", "import gemini_webapi"], 20_000);
271
430
  return result.ok;
272
431
  }
432
+ export async function isGeminiBrowserCookieImportInstalled(pythonCommand = readGeminiWrapperPythonCommand()) {
433
+ const result = await runQuietCommand(pythonCommand, ["-c", "import gemini_webapi, browser_cookie3"], 20_000);
434
+ return result.ok;
435
+ }
273
436
  export async function ensureGeminiWebApiInstalled(pythonCommand = readGeminiWrapperPythonCommand(), env = process.env) {
274
- if (await isGeminiWebApiInstalled(pythonCommand)) {
437
+ if ((await isGeminiWebApiInstalled(pythonCommand)) && (await isGeminiBrowserCookieImportInstalled(pythonCommand))) {
275
438
  return true;
276
439
  }
277
440
  const managedPython = getManagedGeminiWrapperPythonPath(env);
@@ -289,8 +452,8 @@ export async function ensureGeminiWebApiInstalled(pythonCommand = readGeminiWrap
289
452
  }
290
453
  }
291
454
  }
292
- const installResult = await runQuietCommand(pythonCommand, ["-m", "pip", "install", `gemini_webapi==${geminiWebApiVersion}`], 180_000);
293
- return installResult.ok && (await isGeminiWebApiInstalled(pythonCommand));
455
+ const installResult = await runQuietCommand(pythonCommand, ["-m", "pip", "install", `gemini_webapi==${geminiWebApiVersion}`, "browser-cookie3"], 180_000);
456
+ return installResult.ok && (await isGeminiBrowserCookieImportInstalled(pythonCommand));
294
457
  }
295
458
  function isLocalWrapperUrl(baseUrl) {
296
459
  try {
@@ -305,20 +468,136 @@ function normalizeGeminiWrapperModel(model) {
305
468
  const trimmedModel = model.trim();
306
469
  return trimmedModel || defaultGeminiWrapperModel;
307
470
  }
308
- function normalizeGeminiWrapperBridgeModel(model) {
471
+ /**
472
+ * Map a curated shortcut to the model string the gemini_webapi bridge accepts
473
+ * when live discovery is unavailable. Every returned non-empty value must be a
474
+ * valid gemini_webapi `Model` model_name, or the bridge request hard-fails.
475
+ */
476
+ export function normalizeGeminiWrapperBridgeModelFallback(model) {
309
477
  const normalizedModel = normalizeGeminiWrapperModel(model).trim();
310
- return normalizedModel === defaultGeminiWrapperModel || normalizedModel === "gemini-web-default" ? "" : normalizedModel;
478
+ if (normalizedModel === defaultGeminiWrapperModel || normalizedModel === "gemini-web-default") {
479
+ return "";
480
+ }
481
+ // Legacy alias — a stray "flash-lite" maps to the closest valid tier rather
482
+ // than crashing the bridge with an unknown-model error.
483
+ if (normalizedModel === "flash-lite") {
484
+ return "gemini-3-flash";
485
+ }
486
+ if (normalizedModel === "flash") {
487
+ return "gemini-3-flash";
488
+ }
489
+ if (normalizedModel === "thinking") {
490
+ return "gemini-3-flash-thinking";
491
+ }
492
+ if (normalizedModel === "pro") {
493
+ return "gemini-3-pro";
494
+ }
495
+ return normalizedModel;
496
+ }
497
+ function normalizeGeminiWrapperModelDescriptors(models) {
498
+ return models
499
+ .map((model) => (typeof model === "string" ? descriptorFromModelId(model) : normalizeGeminiWrapperModelDescriptor(model)))
500
+ .filter((model) => Boolean(model?.id && isLikelyGeminiWrapperChatModel(formatModelDescriptorSearchText(model))));
501
+ }
502
+ function normalizeGeminiWrapperModelDescriptor(model) {
503
+ const id = String(model.id || model.modelName || model.displayName || "").trim();
504
+ if (!id) {
505
+ return null;
506
+ }
507
+ return cleanUndefined({
508
+ id,
509
+ modelName: model.modelName?.trim() || undefined,
510
+ displayName: model.displayName?.trim() || undefined,
511
+ description: model.description?.trim() || undefined,
512
+ isAvailable: model.isAvailable,
513
+ capacity: typeof model.capacity === "number" && Number.isFinite(model.capacity) ? model.capacity : undefined,
514
+ capacityField: typeof model.capacityField === "number" && Number.isFinite(model.capacityField) ? model.capacityField : undefined,
515
+ advancedOnly: model.advancedOnly,
516
+ legacy: model.legacy
517
+ });
518
+ }
519
+ function descriptorFromModelId(model) {
520
+ const id = model.trim();
521
+ return {
522
+ id,
523
+ modelName: id,
524
+ displayName: id
525
+ };
526
+ }
527
+ function mergeGeminiWrapperModelDescriptors(models) {
528
+ const descriptors = [
529
+ {
530
+ id: "auto",
531
+ displayName: "Auto",
532
+ description: "Let Gemini Web choose its current default model."
533
+ },
534
+ {
535
+ id: "flash",
536
+ displayName: "Flash",
537
+ description: "Gemini 3 Flash — fast tier, resolved from live Gemini Web discovery."
538
+ },
539
+ {
540
+ id: "pro",
541
+ displayName: "Pro",
542
+ description: "Shortcut resolved from live Gemini Web discovery."
543
+ },
544
+ {
545
+ id: "thinking",
546
+ modelName: "gemini-3-flash-thinking",
547
+ displayName: "Thinking legacy",
548
+ description: "Legacy gemini_webapi shortcut; Gemini Web now exposes Denkaufwand instead of a recommended thinking model.",
549
+ legacy: true
550
+ }
551
+ ];
552
+ for (const model of models) {
553
+ if (!hasGeminiWrapperDescriptor(descriptors, model)) {
554
+ descriptors.push(model);
555
+ }
556
+ }
557
+ return descriptors;
558
+ }
559
+ function hasGeminiWrapperDescriptor(descriptors, model) {
560
+ const keys = descriptorKeys(model);
561
+ return descriptors.some((descriptor) => descriptorKeys(descriptor).some((key) => keys.includes(key)));
562
+ }
563
+ function descriptorKeys(model) {
564
+ return [model.id, model.modelName, model.displayName]
565
+ .filter((value) => Boolean(value?.trim()))
566
+ .map((value) => value.trim().toLowerCase());
567
+ }
568
+ function resolveGeminiWrapperShortcutDescriptor(shortcut, descriptors) {
569
+ const dynamicDescriptors = descriptors.filter((descriptor) => !geminiWrapperCuratedModels.includes(descriptor.id));
570
+ const matches = (pattern) => dynamicDescriptors.filter((descriptor) => pattern.test(formatModelDescriptorSearchText(descriptor)));
571
+ if (shortcut === "flash") {
572
+ return matches(/\bflash\b/i).find((descriptor) => !/lite|thinking/i.test(formatModelDescriptorSearchText(descriptor))) ?? null;
573
+ }
574
+ if (shortcut === "pro") {
575
+ return matches(/\bpro\b/i)[0] ?? null;
576
+ }
577
+ if (shortcut === "thinking") {
578
+ return matches(/thinking/i)[0] ?? null;
579
+ }
580
+ return null;
581
+ }
582
+ function isGeminiWrapperShortcutModel(model) {
583
+ return geminiWrapperCuratedModels.includes(model);
584
+ }
585
+ function formatModelDescriptorSearchText(model) {
586
+ return [model.id, model.modelName, model.displayName, model.description].filter(Boolean).join(" ");
311
587
  }
312
588
  function isLikelyGeminiWrapperChatModel(model) {
313
589
  const normalizedModel = model.toLowerCase();
314
- return !/(embedding|embed|imagen|veo|tts|audio|speech|rerank|rank|vision|bidi|live)/.test(normalizedModel);
590
+ return !/(embedding|embed|imagen|veo|tts|audio|speech|rerank|rank|bidi|live)/.test(normalizedModel);
315
591
  }
316
592
  function isUnauthenticatedGeminiWebError(error) {
317
593
  return Boolean(error?.includes("Gemini web cookies are expired or unauthenticated"));
318
594
  }
595
+ function isUnauthenticatedGeminiWebStatus(status) {
596
+ return /unauth|expired|invalid/i.test(status ?? "");
597
+ }
319
598
  function readGeminiWrapperRuntimeOptions(env = process.env) {
320
599
  return {
321
- maxTokens: readPositiveInteger(env.PATCHPILOT_NUM_PREDICT, 1024),
600
+ maxTokens: readPositiveInteger(env.PATCHPILOT_NUM_PREDICT, 8192),
322
601
  temperature: readTemperature(env.PATCHPILOT_TEMPERATURE, 0.1),
323
602
  bridgeMinIntervalMs: readNonNegativeInteger(env.PATCHPILOT_GEMINI_WRAPPER_MIN_INTERVAL_MS, 1500),
324
603
  bridgeTimeoutMs: readPositiveInteger(env.PATCHPILOT_GEMINI_WRAPPER_TIMEOUT_MS, 180_000)
@@ -459,10 +738,10 @@ function runGeminiWebApiBridge(pythonCommand, input, timeoutMs, signal) {
459
738
  let stdout = "";
460
739
  let stderr = "";
461
740
  child.stdout.on("data", (chunk) => {
462
- stdout += chunk.toString("utf8");
741
+ stdout = appendClipped(stdout, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
463
742
  });
464
743
  child.stderr.on("data", (chunk) => {
465
- stderr += chunk.toString("utf8");
744
+ stderr = appendClipped(stderr, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
466
745
  });
467
746
  child.on("error", (error) => {
468
747
  settleReject(error);
@@ -489,6 +768,196 @@ function runGeminiWebApiBridge(pythonCommand, input, timeoutMs, signal) {
489
768
  }));
490
769
  });
491
770
  }
771
+ function runGeminiBrowserCookieImportBridge(pythonCommand, timeoutMs) {
772
+ return new Promise((resolve, reject) => {
773
+ const child = spawn(pythonCommand, ["-c", geminiBrowserCookieImportScript], {
774
+ env: process.env,
775
+ stdio: ["ignore", "pipe", "pipe"],
776
+ detached: process.platform !== "win32",
777
+ windowsHide: true
778
+ });
779
+ let stdout = "";
780
+ let stderr = "";
781
+ let settled = false;
782
+ const timeout = setTimeout(() => {
783
+ if (settled) {
784
+ return;
785
+ }
786
+ settled = true;
787
+ killChildProcess(child, "SIGTERM");
788
+ reject(new Error("Gemini browser cookie import timed out."));
789
+ }, timeoutMs);
790
+ child.stdout.on("data", (chunk) => {
791
+ stdout = appendClipped(stdout, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
792
+ });
793
+ child.stderr.on("data", (chunk) => {
794
+ stderr = appendClipped(stderr, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
795
+ });
796
+ child.on("error", (error) => {
797
+ if (settled) {
798
+ return;
799
+ }
800
+ settled = true;
801
+ clearTimeout(timeout);
802
+ reject(error);
803
+ });
804
+ child.on("close", (code) => {
805
+ if (settled) {
806
+ return;
807
+ }
808
+ settled = true;
809
+ clearTimeout(timeout);
810
+ if (code !== 0) {
811
+ reject(new Error(`Gemini browser cookie import failed.${stderr.trim() ? ` ${redactCookieValues(stderr.trim())}` : ""}`));
812
+ return;
813
+ }
814
+ try {
815
+ resolve(JSON.parse(stdout.trim() || "{}"));
816
+ }
817
+ catch {
818
+ reject(new Error("Gemini browser cookie import did not return valid JSON."));
819
+ }
820
+ });
821
+ });
822
+ }
823
+ function killChildProcess(child, signalName) {
824
+ if (child.pid && process.platform !== "win32") {
825
+ try {
826
+ process.kill(-child.pid, signalName);
827
+ return;
828
+ }
829
+ catch {
830
+ // Fall through to killing the child directly.
831
+ }
832
+ }
833
+ child.kill(signalName);
834
+ }
835
+ function sanitizeGeminiWrapperBrowserCookies(cookies) {
836
+ const byName = new Map();
837
+ for (const cookie of cookies) {
838
+ if (!cookie || typeof cookie.name !== "string" || typeof cookie.value !== "string") {
839
+ continue;
840
+ }
841
+ const name = cookie.name.trim();
842
+ const value = cookie.value.trim();
843
+ if (!name || !value || !geminiWrapperBrowserCookieNames.has(name)) {
844
+ continue;
845
+ }
846
+ const domain = typeof cookie.domain === "string" ? cookie.domain.trim() : "";
847
+ const normalizedDomain = domain.replace(/^\./, "").toLowerCase();
848
+ if (normalizedDomain && normalizedDomain !== "google.com" && !normalizedDomain.endsWith(".google.com")) {
849
+ continue;
850
+ }
851
+ const previousCookie = byName.get(name);
852
+ const previousExpires = typeof previousCookie?.expires === "number" ? previousCookie.expires : 0;
853
+ const nextExpires = typeof cookie.expires === "number" ? cookie.expires : 0;
854
+ if (previousCookie && previousExpires > nextExpires) {
855
+ continue;
856
+ }
857
+ byName.set(name, {
858
+ name,
859
+ value,
860
+ ...(domain ? { domain } : {}),
861
+ ...(typeof cookie.path === "string" && cookie.path ? { path: cookie.path } : {}),
862
+ ...(typeof cookie.expires === "number" ? { expires: cookie.expires } : {}),
863
+ ...(typeof cookie.source === "string" && cookie.source ? { source: cookie.source } : {})
864
+ });
865
+ }
866
+ return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
867
+ }
868
+ function redactCookieValues(value) {
869
+ return value
870
+ .replace(/(__Secure-[A-Za-z0-9_-]+)\s*=\s*([^\s,;]+)/g, "$1=<redacted>")
871
+ .replace(/(PSID[A-Z]*)\s*[:=]\s*([^\s,;]+)/gi, "$1=<redacted>");
872
+ }
873
+ function appendClipped(currentValue, chunk, maxLength) {
874
+ const nextValue = currentValue + chunk;
875
+ if (nextValue.length <= maxLength) {
876
+ return nextValue;
877
+ }
878
+ const clippedMarker = `\n...[clipped ${nextValue.length - maxLength} chars]...\n`;
879
+ return `${clippedMarker}${nextValue.slice(-maxLength + clippedMarker.length)}`;
880
+ }
881
+ const geminiBrowserCookieImportScript = String.raw `
882
+ import json
883
+
884
+ ALLOWED_COOKIE_NAMES = {
885
+ "__Secure-1PSID",
886
+ "__Secure-1PSIDTS",
887
+ "__Secure-1PSIDCC",
888
+ "__Secure-1PAPISID",
889
+ "__Secure-3PSID",
890
+ "__Secure-3PSIDTS",
891
+ "__Secure-3PSIDCC",
892
+ "__Secure-3PAPISID",
893
+ "__Secure-ENID",
894
+ "AEC",
895
+ "COMPASS",
896
+ "GOOGLE_ABUSE_EXEMPTION",
897
+ "NID",
898
+ "SID",
899
+ "HSID",
900
+ "SSID",
901
+ "APISID",
902
+ "SAPISID",
903
+ }
904
+
905
+ def normalize_domain(value):
906
+ return (value or "").lstrip(".").lower()
907
+
908
+ def is_google_domain(value):
909
+ domain = normalize_domain(value)
910
+ return not domain or domain == "google.com" or domain.endswith(".google.com")
911
+
912
+ try:
913
+ from gemini_webapi.utils.load_browser_cookies import load_browser_cookies
914
+
915
+ browser_cookies = load_browser_cookies(domain_name="google.com", verbose=False)
916
+ candidates = []
917
+ available_sources = []
918
+ for browser_name, cookies in browser_cookies.items():
919
+ filtered = []
920
+ for cookie in cookies or []:
921
+ name = cookie.get("name")
922
+ value = cookie.get("value")
923
+ if name not in ALLOWED_COOKIE_NAMES or not value or not is_google_domain(cookie.get("domain")):
924
+ continue
925
+ filtered.append({
926
+ "name": name,
927
+ "value": value,
928
+ "domain": cookie.get("domain") or ".google.com",
929
+ "path": cookie.get("path") or "/",
930
+ "expires": cookie.get("expires"),
931
+ "source": browser_name,
932
+ })
933
+ if filtered:
934
+ available_sources.append(browser_name)
935
+ has_psid = any(cookie["name"] == "__Secure-1PSID" for cookie in filtered)
936
+ has_psidts = any(cookie["name"] == "__Secure-1PSIDTS" for cookie in filtered)
937
+ candidates.append({
938
+ "browser": browser_name,
939
+ "cookies": filtered,
940
+ "score": (1 if has_psid else 0, 1 if has_psidts else 0, len(filtered), browser_name),
941
+ })
942
+
943
+ candidates.sort(key=lambda item: item["score"], reverse=True)
944
+ if not candidates or not any(cookie["name"] == "__Secure-1PSID" for cookie in candidates[0]["cookies"]):
945
+ print(json.dumps({
946
+ "error": "No Gemini browser cookies were found in supported browsers. Sign in to Gemini in Chrome, Brave, Edge, Firefox, Safari, or another supported browser, then retry the explicit import.",
947
+ "availableSources": available_sources,
948
+ }))
949
+ else:
950
+ selected = candidates[0]
951
+ print(json.dumps({
952
+ "cookies": selected["cookies"],
953
+ "source": selected["browser"],
954
+ "availableSources": available_sources,
955
+ }))
956
+ except Exception as exc:
957
+ print(json.dumps({
958
+ "error": f"Gemini browser cookie import failed: {type(exc).__name__}. Unlock the browser profile or macOS Keychain access, then retry."
959
+ }))
960
+ `;
492
961
  const geminiWebApiBridgeScript = String.raw `
493
962
  import asyncio
494
963
  import json
@@ -526,7 +995,14 @@ async def main():
526
995
  lower = message.lower()
527
996
  return (
528
997
  "curl: (28)" in lower
998
+ or "curl: (56)" in lower
529
999
  or "connection timed out" in lower
1000
+ or "connection closed abruptly" in lower
1001
+ or "connection reset" in lower
1002
+ or "server returned nothing" in lower
1003
+ or "unexpected eof" in lower
1004
+ or "stream error" in lower
1005
+ or "http/2 stream" in lower
530
1006
  or "operation timed out" in lower
531
1007
  or "readtimeout" in lower
532
1008
  or "timeouterror" in lower
@@ -553,44 +1029,89 @@ async def main():
553
1029
 
554
1030
  async def generate_once(psidts_value):
555
1031
  client = GeminiClient(secure_1psid=psid, secure_1psidts=psidts_value, cookies=extra or None, proxy=payload.get("proxy"))
556
- await client.init(timeout=90, auto_refresh=False, verbose=False)
1032
+ await client.init(timeout=attempt_timeout, auto_refresh=True, verbose=False)
557
1033
  try:
558
1034
  status_name = account_status_name(client)
1035
+ if "unauth" in status_name.lower() or "expired" in status_name.lower() or "invalid" in status_name.lower():
1036
+ return {"error": expired_cookie_error(), "accountStatus": status_name}
1037
+
559
1038
  if payload.get("command") == "authCheck":
560
1039
  return {"content": "ok", "accountStatus": status_name}
561
1040
 
562
1041
  if payload.get("command") == "models":
563
1042
  models = []
1043
+ model_descriptors = []
564
1044
  for model in client.list_models() or []:
565
1045
  if not getattr(model, "is_available", True):
566
1046
  continue
567
- name = getattr(model, "model_name", None) or getattr(model, "display_name", None)
1047
+ model_id = getattr(model, "model_id", None) or ""
1048
+ name = getattr(model, "model_name", None) or ""
1049
+ display_name = getattr(model, "display_name", None) or ""
1050
+ description = getattr(model, "description", None) or ""
1051
+ selection_id = model_id or name or display_name
1052
+ if selection_id:
1053
+ descriptor = {
1054
+ "id": selection_id,
1055
+ "modelName": name or None,
1056
+ "displayName": display_name or name or selection_id,
1057
+ "description": description or None,
1058
+ "isAvailable": bool(getattr(model, "is_available", True)),
1059
+ "advancedOnly": bool(getattr(model, "advanced_only", False)),
1060
+ }
1061
+ capacity = getattr(model, "capacity", None)
1062
+ capacity_field = getattr(model, "capacity_field", None)
1063
+ if isinstance(capacity, (int, float)):
1064
+ descriptor["capacity"] = capacity
1065
+ if isinstance(capacity_field, (int, float)):
1066
+ descriptor["capacityField"] = capacity_field
1067
+ model_descriptors.append({key: value for key, value in descriptor.items() if value is not None})
1068
+ name = name or display_name or model_id
568
1069
  if name:
569
1070
  models.append(name)
570
- return {"models": models, "accountStatus": status_name}
1071
+ return {"models": models, "modelDescriptors": model_descriptors, "accountStatus": status_name}
571
1072
 
572
1073
  request_model = payload.get("model") or ""
573
1074
  request_kwargs = {"temporary": True}
574
1075
  if request_model:
575
1076
  request_kwargs["model"] = request_model
576
- response = await client.generate_content(payload.get("prompt") or "", **request_kwargs)
1077
+ files = payload.get("files") or []
1078
+ if files:
1079
+ request_kwargs["files"] = files
1080
+ used_model = request_model or "auto"
1081
+ model_warning = None
1082
+ prompt_text = payload.get("prompt") or ""
1083
+ try:
1084
+ response = await client.generate_content(prompt_text, **request_kwargs)
1085
+ except ValueError as model_error:
1086
+ # An unknown model name must not fail the whole request — retry
1087
+ # on Gemini Web's default model and report it back.
1088
+ if "model" not in str(model_error).lower():
1089
+ raise
1090
+ request_kwargs.pop("model", None)
1091
+ used_model = "auto"
1092
+ model_warning = f"Requested model '{request_model}' is unavailable; used the Gemini Web default instead."
1093
+ response = await client.generate_content(prompt_text, **request_kwargs)
577
1094
  text = getattr(response, "text", None) or str(response)
578
- return {"content": text, "accountStatus": status_name, "model": request_model or "auto"}
1095
+ result = {"content": text, "accountStatus": status_name, "model": used_model}
1096
+ if model_warning:
1097
+ result["warning"] = model_warning
1098
+ return result
579
1099
  finally:
580
1100
  await client.close()
581
1101
 
582
1102
  async def generate_with_timestamp(psidts_value):
583
1103
  last_error = None
584
- for attempt in range(3):
1104
+ max_attempts = 2 if payload.get("files") else 3
1105
+ for attempt in range(max_attempts):
585
1106
  try:
586
1107
  return await asyncio.wait_for(generate_once(psidts_value), timeout=attempt_timeout)
587
1108
  except asyncio.TimeoutError:
588
- if attempt >= 2:
1109
+ if attempt >= max_attempts - 1:
589
1110
  raise TimeoutError(f"Gemini-API bridge attempt timed out after {attempt_timeout}s.")
590
1111
  await asyncio.sleep(1.5 * (attempt + 1))
591
1112
  except Exception as exc:
592
1113
  last_error = exc
593
- if attempt >= 2 or not is_transient_network_error(str(exc)):
1114
+ if attempt >= max_attempts - 1 or not is_transient_network_error(str(exc)):
594
1115
  raise
595
1116
  await asyncio.sleep(1.5 * (attempt + 1))
596
1117
  raise last_error
@@ -629,14 +1150,33 @@ except Exception as exc:
629
1150
  function cleanUndefined(value) {
630
1151
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
631
1152
  }
632
- async function readJsonSafely(response) {
1153
+ async function readGeminiWrapperResponse(response) {
1154
+ const text = await response.text().catch(() => "");
1155
+ if (!text.trim()) {
1156
+ return {
1157
+ payload: {},
1158
+ text: ""
1159
+ };
1160
+ }
633
1161
  try {
634
- return await response.json();
1162
+ return {
1163
+ payload: JSON.parse(text),
1164
+ text
1165
+ };
635
1166
  }
636
1167
  catch {
637
- return {};
1168
+ return {
1169
+ payload: {},
1170
+ text
1171
+ };
638
1172
  }
639
1173
  }
1174
+ function formatGeminiWrapperErrorReason(payload, text, response) {
1175
+ const retryAfter = response.headers.get("retry-after");
1176
+ const providerMessage = payload.error?.message?.trim() || text.replace(/\s+/g, " ").trim().slice(0, 300);
1177
+ const parts = [providerMessage, retryAfter ? `retry-after ${retryAfter}s` : ""].filter(Boolean);
1178
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
1179
+ }
640
1180
  function readPositiveInteger(value, fallback) {
641
1181
  const parsedValue = Number.parseInt(value ?? "", 10);
642
1182
  return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : fallback;