@jx-grxf/patchpilot 0.4.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.
- package/.env.example +17 -1
- package/README.md +113 -23
- package/SECURITY.md +7 -1
- package/dist/cli.js +103 -14
- package/dist/cli.js.map +1 -1
- package/dist/core/agent.d.ts +47 -1
- package/dist/core/agent.js +667 -76
- package/dist/core/agent.js.map +1 -1
- package/dist/core/cleanup.d.ts +3 -0
- package/dist/core/cleanup.js +29 -0
- package/dist/core/cleanup.js.map +1 -0
- package/dist/core/clipboard.d.ts +14 -0
- package/dist/core/clipboard.js +134 -0
- package/dist/core/clipboard.js.map +1 -0
- package/dist/core/codex.d.ts +8 -0
- package/dist/core/codex.js +28 -2
- package/dist/core/codex.js.map +1 -1
- package/dist/core/compaction.d.ts +23 -0
- package/dist/core/compaction.js +145 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/contextFormat.d.ts +21 -0
- package/dist/core/contextFormat.js +87 -0
- package/dist/core/contextFormat.js.map +1 -0
- package/dist/core/contextItem.d.ts +41 -0
- package/dist/core/contextItem.js +93 -0
- package/dist/core/contextItem.js.map +1 -0
- package/dist/core/contextStore.d.ts +48 -0
- package/dist/core/contextStore.js +306 -0
- package/dist/core/contextStore.js.map +1 -0
- package/dist/core/doctor.d.ts +4 -1
- package/dist/core/doctor.js +122 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/gemini.js +10 -4
- package/dist/core/gemini.js.map +1 -1
- package/dist/core/geminiWrapper.d.ts +92 -0
- package/dist/core/geminiWrapper.js +1258 -0
- package/dist/core/geminiWrapper.js.map +1 -0
- package/dist/core/http.js +70 -6
- package/dist/core/http.js.map +1 -1
- package/dist/core/json.d.ts +1 -1
- package/dist/core/json.js +81 -19
- package/dist/core/json.js.map +1 -1
- package/dist/core/memory.d.ts +16 -0
- package/dist/core/memory.js +108 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/modelClient.js +7 -0
- package/dist/core/modelClient.js.map +1 -1
- package/dist/core/nvidia.d.ts +1 -1
- package/dist/core/nvidia.js +13 -4
- package/dist/core/nvidia.js.map +1 -1
- package/dist/core/ollama.js +13 -3
- package/dist/core/ollama.js.map +1 -1
- package/dist/core/openrouter.js +15 -6
- package/dist/core/openrouter.js.map +1 -1
- package/dist/core/projectInit.d.ts +6 -0
- package/dist/core/projectInit.js +44 -0
- package/dist/core/projectInit.js.map +1 -0
- package/dist/core/reasoning.js +6 -0
- package/dist/core/reasoning.js.map +1 -1
- package/dist/core/session.d.ts +1 -0
- package/dist/core/session.js +55 -3
- package/dist/core/session.js.map +1 -1
- package/dist/core/tokenAccounting.d.ts +4 -0
- package/dist/core/tokenAccounting.js +75 -13
- package/dist/core/tokenAccounting.js.map +1 -1
- package/dist/core/types.d.ts +65 -5
- package/dist/core/types.js +30 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/updateCheck.d.ts +19 -0
- package/dist/core/updateCheck.js +103 -0
- package/dist/core/updateCheck.js.map +1 -0
- package/dist/core/workspace.d.ts +37 -0
- package/dist/core/workspace.js +1535 -84
- package/dist/core/workspace.js.map +1 -1
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +1841 -140
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/commands.js +141 -9
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/components/ApprovalPanel.js +16 -1
- package/dist/tui/components/ApprovalPanel.js.map +1 -1
- package/dist/tui/components/CommandSuggestions.js +33 -5
- package/dist/tui/components/CommandSuggestions.js.map +1 -1
- package/dist/tui/components/Composer.d.ts +3 -0
- package/dist/tui/components/Composer.js +57 -5
- package/dist/tui/components/Composer.js.map +1 -1
- package/dist/tui/components/ExperimentalPanel.d.ts +10 -0
- package/dist/tui/components/ExperimentalPanel.js +38 -0
- package/dist/tui/components/ExperimentalPanel.js.map +1 -0
- package/dist/tui/components/Header.js +3 -3
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/OnboardingPanel.d.ts +25 -1
- package/dist/tui/components/OnboardingPanel.js +87 -25
- package/dist/tui/components/OnboardingPanel.js.map +1 -1
- package/dist/tui/components/Sidebar.js +17 -13
- package/dist/tui/components/Sidebar.js.map +1 -1
- package/dist/tui/components/StartupBanner.d.ts +4 -0
- package/dist/tui/components/StartupBanner.js +9 -0
- package/dist/tui/components/StartupBanner.js.map +1 -0
- package/dist/tui/components/Transcript.d.ts +7 -0
- package/dist/tui/components/Transcript.js +87 -17
- package/dist/tui/components/Transcript.js.map +1 -1
- package/dist/tui/contextCommands.d.ts +8 -0
- package/dist/tui/contextCommands.js +205 -0
- package/dist/tui/contextCommands.js.map +1 -0
- package/dist/tui/experimental/AnimatedText.d.ts +38 -0
- package/dist/tui/experimental/AnimatedText.js +55 -0
- package/dist/tui/experimental/AnimatedText.js.map +1 -0
- package/dist/tui/experimental/Banner.d.ts +10 -0
- package/dist/tui/experimental/Banner.js +33 -0
- package/dist/tui/experimental/Banner.js.map +1 -0
- package/dist/tui/experimental/CommandPalette.d.ts +11 -0
- package/dist/tui/experimental/CommandPalette.js +25 -0
- package/dist/tui/experimental/CommandPalette.js.map +1 -0
- package/dist/tui/experimental/ExperimentalShell.d.ts +58 -0
- package/dist/tui/experimental/ExperimentalShell.js +366 -0
- package/dist/tui/experimental/ExperimentalShell.js.map +1 -0
- package/dist/tui/experimental/ThemePicker.d.ts +13 -0
- package/dist/tui/experimental/ThemePicker.js +12 -0
- package/dist/tui/experimental/ThemePicker.js.map +1 -0
- package/dist/tui/experimental/attachments.d.ts +35 -0
- package/dist/tui/experimental/attachments.js +244 -0
- package/dist/tui/experimental/attachments.js.map +1 -0
- package/dist/tui/experimental/composer.d.ts +24 -0
- package/dist/tui/experimental/composer.js +84 -0
- package/dist/tui/experimental/composer.js.map +1 -0
- package/dist/tui/experimental/geminiPricing.d.ts +16 -0
- package/dist/tui/experimental/geminiPricing.js +39 -0
- package/dist/tui/experimental/geminiPricing.js.map +1 -0
- package/dist/tui/experimental/layout.d.ts +46 -0
- package/dist/tui/experimental/layout.js +112 -0
- package/dist/tui/experimental/layout.js.map +1 -0
- package/dist/tui/experimental/theme.d.ts +35 -0
- package/dist/tui/experimental/theme.js +86 -0
- package/dist/tui/experimental/theme.js.map +1 -0
- package/dist/tui/experimental/transcriptRows.d.ts +20 -0
- package/dist/tui/experimental/transcriptRows.js +169 -0
- package/dist/tui/experimental/transcriptRows.js.map +1 -0
- package/dist/tui/experimental/ultraModes.d.ts +46 -0
- package/dist/tui/experimental/ultraModes.js +95 -0
- package/dist/tui/experimental/ultraModes.js.map +1 -0
- package/dist/tui/experimental/ultramaxx.d.ts +19 -0
- package/dist/tui/experimental/ultramaxx.js +43 -0
- package/dist/tui/experimental/ultramaxx.js.map +1 -0
- package/dist/tui/format.d.ts +4 -2
- package/dist/tui/format.js +21 -7
- package/dist/tui/format.js.map +1 -1
- package/dist/tui/hosts.js +7 -1
- package/dist/tui/hosts.js.map +1 -1
- package/dist/tui/layout.d.ts +26 -0
- package/dist/tui/layout.js +66 -0
- package/dist/tui/layout.js.map +1 -0
- package/dist/tui/modelSelection.d.ts +1 -1
- package/dist/tui/modelSelection.js +8 -6
- package/dist/tui/modelSelection.js.map +1 -1
- package/dist/tui/modes.d.ts +8 -1
- package/dist/tui/modes.js +20 -2
- package/dist/tui/modes.js.map +1 -1
- package/dist/tui/onboardingPreferences.d.ts +37 -0
- package/dist/tui/onboardingPreferences.js +118 -0
- package/dist/tui/onboardingPreferences.js.map +1 -0
- package/dist/tui/runStatus.d.ts +50 -0
- package/dist/tui/runStatus.js +164 -0
- package/dist/tui/runStatus.js.map +1 -0
- package/dist/tui/types.d.ts +8 -0
- package/dist/tui/types.js.map +1 -1
- package/docs/architecture.md +115 -0
- package/docs/gemini-wrapper.md +110 -0
- package/docs/product-context.md +43 -0
- package/docs/releases/v0.1.1-beta.md +18 -0
- package/docs/releases/v0.2.1.md +1 -1
- package/docs/releases/v0.3.1-beta.md +4 -0
- package/docs/releases/v0.4.0.md +1 -1
- package/docs/releases/v1.0.0.md +28 -0
- package/docs/releases/v1.0.1.md +25 -0
- package/docs/releases/v1.1.0.md +30 -0
- package/docs/releases/v1.2.0.md +28 -0
- package/docs/showcase/patchpilot-banner.png +0 -0
- package/docs/showcase/patchpilot-logo.png +0 -0
- package/package.json +8 -3
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getPatchPilotConfigDir } from "./env.js";
|
|
5
|
+
import { fetchWithTimeout } from "./http.js";
|
|
6
|
+
import { attachTokenCost, estimateTokens } from "./tokenAccounting.js";
|
|
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];
|
|
14
|
+
export const geminiWebApiVersion = "2.0.0";
|
|
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
|
+
]);
|
|
41
|
+
export class GeminiWrapperClient {
|
|
42
|
+
baseUrl;
|
|
43
|
+
apiKey;
|
|
44
|
+
runtimeOptions;
|
|
45
|
+
mode;
|
|
46
|
+
pythonCommand;
|
|
47
|
+
cookiesJson;
|
|
48
|
+
modelDescriptorCache = null;
|
|
49
|
+
pythonBridgeReadyUntil = 0;
|
|
50
|
+
pythonBridgeReadyPromise = null;
|
|
51
|
+
constructor(baseUrl = readGeminiWrapperBaseUrl(), apiKey = readGeminiWrapperApiKey(), runtimeOptions = readGeminiWrapperRuntimeOptions(), mode = readGeminiWrapperMode(), pythonCommand = readGeminiWrapperPythonCommand(), cookiesJson = readGeminiWrapperCookiesJson()) {
|
|
52
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
53
|
+
this.apiKey = apiKey;
|
|
54
|
+
this.runtimeOptions = runtimeOptions;
|
|
55
|
+
this.mode = mode;
|
|
56
|
+
this.pythonCommand = pythonCommand;
|
|
57
|
+
this.cookiesJson = cookiesJson;
|
|
58
|
+
}
|
|
59
|
+
async chat(options) {
|
|
60
|
+
if (this.usesPythonBridge()) {
|
|
61
|
+
return await this.chatWithPythonBridge(options);
|
|
62
|
+
}
|
|
63
|
+
this.assertConfigured();
|
|
64
|
+
const startedAt = Date.now();
|
|
65
|
+
const response = await this.fetchGeminiWrapper("/chat/completions", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: this.headers(),
|
|
68
|
+
body: JSON.stringify(cleanUndefined({
|
|
69
|
+
model: normalizeGeminiWrapperModel(options.model),
|
|
70
|
+
messages: options.messages,
|
|
71
|
+
max_tokens: this.runtimeOptions.maxTokens,
|
|
72
|
+
temperature: this.runtimeOptions.temperature,
|
|
73
|
+
response_format: options.formatJson ? { type: "json_object" } : undefined
|
|
74
|
+
})),
|
|
75
|
+
signal: options.signal
|
|
76
|
+
});
|
|
77
|
+
const durationMs = Date.now() - startedAt;
|
|
78
|
+
const { payload, text } = await readGeminiWrapperResponse(response);
|
|
79
|
+
if (!response.ok || payload.error) {
|
|
80
|
+
const reason = formatGeminiWrapperErrorReason(payload, text, response);
|
|
81
|
+
if (response.status === 401 || response.status === 403) {
|
|
82
|
+
throw new Error("Gemini-Wrapper authentication failed. Check PATCHPILOT_GEMINI_WRAPPER_API_KEY.");
|
|
83
|
+
}
|
|
84
|
+
if (response.status === 429) {
|
|
85
|
+
throw new Error(`Gemini-Wrapper rate limit hit for model "${options.model}".${reason}`);
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Gemini-Wrapper chat failed for model "${options.model}": HTTP ${response.status}.${reason}`);
|
|
88
|
+
}
|
|
89
|
+
const content = payload.choices?.[0]?.message?.content?.trim() ?? "";
|
|
90
|
+
if (!content) {
|
|
91
|
+
throw new Error("Gemini-Wrapper returned an empty response.");
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
content,
|
|
95
|
+
telemetry: toTelemetry(payload, durationMs, options.model)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
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
|
+
}
|
|
105
|
+
if (this.usesPythonBridge()) {
|
|
106
|
+
await this.assertPythonBridgeReady();
|
|
107
|
+
const result = await this.runPythonBridge({
|
|
108
|
+
command: "models",
|
|
109
|
+
model: defaultGeminiWrapperModel
|
|
110
|
+
});
|
|
111
|
+
if (result.error) {
|
|
112
|
+
throw new Error(result.error);
|
|
113
|
+
}
|
|
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;
|
|
121
|
+
}
|
|
122
|
+
this.assertConfigured();
|
|
123
|
+
const response = await this.fetchGeminiWrapper("/models", {
|
|
124
|
+
headers: this.headers()
|
|
125
|
+
});
|
|
126
|
+
const { payload, text } = await readGeminiWrapperResponse(response);
|
|
127
|
+
if (!response.ok || payload.error) {
|
|
128
|
+
const reason = formatGeminiWrapperErrorReason(payload, text, response);
|
|
129
|
+
throw new Error(`Gemini-Wrapper models failed with HTTP ${response.status}.${reason}`);
|
|
130
|
+
}
|
|
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
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async checkBridgeAuth() {
|
|
169
|
+
await this.assertPythonBridgeReady();
|
|
170
|
+
const result = await this.runPythonBridge({
|
|
171
|
+
command: "authCheck",
|
|
172
|
+
model: defaultGeminiWrapperModel
|
|
173
|
+
});
|
|
174
|
+
if (result.error) {
|
|
175
|
+
throw new Error(result.error);
|
|
176
|
+
}
|
|
177
|
+
if (isUnauthenticatedGeminiWebStatus(result.accountStatus)) {
|
|
178
|
+
throw new Error("Gemini-API bridge cookies are expired or unauthenticated. Refresh Gemini-Wrapper cookies.");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async fetchGeminiWrapper(path, init) {
|
|
182
|
+
try {
|
|
183
|
+
return await fetchWithTimeout(`${this.baseUrl}${path}`, init, {
|
|
184
|
+
timeoutMs: init?.method === "POST" ? 90_000 : 8000,
|
|
185
|
+
retries: init?.method === "POST" ? 2 : 1,
|
|
186
|
+
label: `Gemini-Wrapper ${path}`
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
const suffix = error instanceof Error ? ` ${error.message}` : "";
|
|
191
|
+
throw new Error(`Cannot reach Gemini-Wrapper API at ${this.baseUrl}.${suffix}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
headers() {
|
|
195
|
+
return cleanUndefined({
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
Authorization: this.apiKey ? `Bearer ${this.apiKey}` : undefined
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
assertConfigured() {
|
|
201
|
+
if (this.usesPythonBridge()) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!this.baseUrl) {
|
|
205
|
+
throw new Error(`Gemini-Wrapper requires either an explicit OpenAI-compatible wrapper URL or the installed Gemini-API Python wrapper. Set PATCHPILOT_GEMINI_WRAPPER_BASE_URL, or install the bridge with: ${geminiWebApiInstallCommand}`);
|
|
206
|
+
}
|
|
207
|
+
if (geminiWrapperRequiresApiKey(this.baseUrl) && !this.apiKey) {
|
|
208
|
+
throw new Error("Gemini-Wrapper remote URLs require an explicit API key. Set PATCHPILOT_GEMINI_WRAPPER_API_KEY or GEMINI_WRAPPER_API_KEY. PatchPilot does not collect browser cookies.");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
usesPythonBridge() {
|
|
212
|
+
return this.mode === "python" || (this.mode === "auto" && !this.baseUrl);
|
|
213
|
+
}
|
|
214
|
+
async chatWithPythonBridge(options) {
|
|
215
|
+
await this.assertPythonBridgeReady();
|
|
216
|
+
const startedAt = Date.now();
|
|
217
|
+
const prompt = toBridgePrompt(options.messages, options.formatJson);
|
|
218
|
+
const bridgeModel = await this.resolveGeminiWrapperBridgeModel(options.model);
|
|
219
|
+
const result = await this.runPythonBridge({
|
|
220
|
+
command: "chat",
|
|
221
|
+
model: bridgeModel,
|
|
222
|
+
prompt
|
|
223
|
+
}, options.signal, getGeminiWrapperBridgeTimeoutMs(bridgeModel || defaultGeminiWrapperModel, this.runtimeOptions.bridgeTimeoutMs));
|
|
224
|
+
const durationMs = Date.now() - startedAt;
|
|
225
|
+
const content = result.content?.trim() ?? "";
|
|
226
|
+
if (!content) {
|
|
227
|
+
throw new Error(result.error ? `Gemini-API bridge failed: ${result.error}` : "Gemini-API bridge returned an empty response.");
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
content,
|
|
231
|
+
telemetry: toEstimatedTelemetry(prompt, content, durationMs, result.model ?? options.model)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async runPythonBridge(input, signal, timeoutMs = getGeminiWrapperBridgeTimeoutMs(input.model, this.runtimeOptions.bridgeTimeoutMs)) {
|
|
235
|
+
const result = await runThrottledGeminiWebApiBridge(this.pythonCommand, {
|
|
236
|
+
...input,
|
|
237
|
+
cookiesJson: this.cookiesJson || undefined,
|
|
238
|
+
secure1psid: readGeminiWrapperSecure1psid() || undefined,
|
|
239
|
+
secure1psidts: readGeminiWrapperSecure1psidts() || undefined,
|
|
240
|
+
proxy: process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy
|
|
241
|
+
}, signal, this.runtimeOptions.bridgeMinIntervalMs, timeoutMs);
|
|
242
|
+
if (!isUnauthenticatedGeminiWebError(result.error)) {
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
clearGeminiWrapperCookieCache();
|
|
246
|
+
return await runThrottledGeminiWebApiBridge(this.pythonCommand, {
|
|
247
|
+
...input,
|
|
248
|
+
cookiesJson: this.cookiesJson || undefined,
|
|
249
|
+
secure1psid: readGeminiWrapperSecure1psid() || undefined,
|
|
250
|
+
secure1psidts: readGeminiWrapperSecure1psidts() || undefined,
|
|
251
|
+
proxy: process.env.HTTPS_PROXY ?? process.env.https_proxy ?? process.env.HTTP_PROXY ?? process.env.http_proxy
|
|
252
|
+
}, signal, this.runtimeOptions.bridgeMinIntervalMs, timeoutMs);
|
|
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
|
+
}
|
|
274
|
+
async assertPythonBridgeReady() {
|
|
275
|
+
if (!this.cookiesJson && !readGeminiWrapperSecure1psid()) {
|
|
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.");
|
|
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() {
|
|
289
|
+
const installed = await isGeminiWebApiInstalled(this.pythonCommand);
|
|
290
|
+
if (!installed) {
|
|
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}`);
|
|
292
|
+
}
|
|
293
|
+
this.pythonBridgeReadyUntil = Date.now() + pythonBridgeReadyTtlMs;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
export function readGeminiWrapperMode(env = process.env) {
|
|
297
|
+
const value = env.PATCHPILOT_GEMINI_WRAPPER_MODE?.trim().toLowerCase();
|
|
298
|
+
return value === "http" || value === "python" || value === "auto" ? value : "auto";
|
|
299
|
+
}
|
|
300
|
+
export function readGeminiWrapperBaseUrl(env = process.env) {
|
|
301
|
+
return env.PATCHPILOT_GEMINI_WRAPPER_BASE_URL?.trim() || "";
|
|
302
|
+
}
|
|
303
|
+
export function readGeminiWrapperApiKey(env = process.env) {
|
|
304
|
+
return env.PATCHPILOT_GEMINI_WRAPPER_API_KEY?.trim() || env.GEMINI_WRAPPER_API_KEY?.trim() || "";
|
|
305
|
+
}
|
|
306
|
+
export function readGeminiWrapperCookiesJson(env = process.env) {
|
|
307
|
+
return env.PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON?.trim() || env.GEMINI_COOKIES_JSON?.trim() || "";
|
|
308
|
+
}
|
|
309
|
+
export function getDefaultGeminiWrapperCookiesPath(env = process.env) {
|
|
310
|
+
return path.join(getPatchPilotConfigDir(env), "gemini-cookies.json");
|
|
311
|
+
}
|
|
312
|
+
export function saveGeminiWrapperCookieFile(values, env = process.env) {
|
|
313
|
+
const secure1psid = values.secure1psid.trim();
|
|
314
|
+
const secure1psidts = values.secure1psidts?.trim() ?? "";
|
|
315
|
+
if (!secure1psid) {
|
|
316
|
+
throw new Error("__Secure-1PSID cannot be empty.");
|
|
317
|
+
}
|
|
318
|
+
const configDir = getPatchPilotConfigDir(env);
|
|
319
|
+
mkdirSync(configDir, {
|
|
320
|
+
recursive: true,
|
|
321
|
+
mode: 0o700
|
|
322
|
+
});
|
|
323
|
+
tryChmod(configDir, 0o700);
|
|
324
|
+
const cookiesPath = getDefaultGeminiWrapperCookiesPath(env);
|
|
325
|
+
writeFileSync(cookiesPath, `${JSON.stringify({
|
|
326
|
+
cookies: {
|
|
327
|
+
"__Secure-1PSID": secure1psid,
|
|
328
|
+
...(secure1psidts ? { "__Secure-1PSIDTS": secure1psidts } : {})
|
|
329
|
+
}
|
|
330
|
+
}, null, 2)}\n`, {
|
|
331
|
+
encoding: "utf8",
|
|
332
|
+
mode: 0o600
|
|
333
|
+
});
|
|
334
|
+
tryChmod(cookiesPath, 0o600);
|
|
335
|
+
clearGeminiWrapperCookieCache(env);
|
|
336
|
+
return cookiesPath;
|
|
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
|
+
}
|
|
390
|
+
export function readGeminiWrapperPythonCommand(env = process.env) {
|
|
391
|
+
return env.PATCHPILOT_GEMINI_WRAPPER_PYTHON?.trim() || getManagedGeminiWrapperPythonPath(env);
|
|
392
|
+
}
|
|
393
|
+
export function readGeminiWrapperBootstrapPythonCommand(env = process.env) {
|
|
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");
|
|
397
|
+
}
|
|
398
|
+
export function getGeminiWrapperVenvDir(env = process.env) {
|
|
399
|
+
return path.join(getPatchPilotConfigDir(env), "gemini-wrapper-venv");
|
|
400
|
+
}
|
|
401
|
+
export function getGeminiWrapperCookieCacheDir(env = process.env) {
|
|
402
|
+
return path.join(getPatchPilotConfigDir(env), "gemini-webapi-cache");
|
|
403
|
+
}
|
|
404
|
+
export function clearGeminiWrapperCookieCache(env = process.env) {
|
|
405
|
+
const cacheDir = getGeminiWrapperCookieCacheDir(env);
|
|
406
|
+
rmSync(cacheDir, {
|
|
407
|
+
recursive: true,
|
|
408
|
+
force: true
|
|
409
|
+
});
|
|
410
|
+
mkdirSync(cacheDir, {
|
|
411
|
+
recursive: true,
|
|
412
|
+
mode: 0o700
|
|
413
|
+
});
|
|
414
|
+
tryChmod(cacheDir, 0o700);
|
|
415
|
+
}
|
|
416
|
+
export function getManagedGeminiWrapperPythonPath(env = process.env) {
|
|
417
|
+
return path.join(getGeminiWrapperVenvDir(env), process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
|
|
418
|
+
}
|
|
419
|
+
export function readGeminiWrapperSecure1psid(env = process.env) {
|
|
420
|
+
return env.GEMINI_SECURE_1PSID?.trim() || "";
|
|
421
|
+
}
|
|
422
|
+
export function readGeminiWrapperSecure1psidts(env = process.env) {
|
|
423
|
+
return env.GEMINI_SECURE_1PSIDTS?.trim() || "";
|
|
424
|
+
}
|
|
425
|
+
export function geminiWrapperRequiresApiKey(baseUrl) {
|
|
426
|
+
return !isLocalWrapperUrl(baseUrl);
|
|
427
|
+
}
|
|
428
|
+
export async function isGeminiWebApiInstalled(pythonCommand = readGeminiWrapperPythonCommand()) {
|
|
429
|
+
const result = await runQuietCommand(pythonCommand, ["-c", "import gemini_webapi"], 20_000);
|
|
430
|
+
return result.ok;
|
|
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
|
+
}
|
|
436
|
+
export async function ensureGeminiWebApiInstalled(pythonCommand = readGeminiWrapperPythonCommand(), env = process.env) {
|
|
437
|
+
if ((await isGeminiWebApiInstalled(pythonCommand)) && (await isGeminiBrowserCookieImportInstalled(pythonCommand))) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
const managedPython = getManagedGeminiWrapperPythonPath(env);
|
|
441
|
+
if (path.resolve(pythonCommand) === path.resolve(managedPython)) {
|
|
442
|
+
const venvDir = getGeminiWrapperVenvDir(env);
|
|
443
|
+
mkdirSync(getPatchPilotConfigDir(env), {
|
|
444
|
+
recursive: true,
|
|
445
|
+
mode: 0o700
|
|
446
|
+
});
|
|
447
|
+
tryChmod(getPatchPilotConfigDir(env), 0o700);
|
|
448
|
+
if (!existsSync(managedPython)) {
|
|
449
|
+
const venvResult = await runQuietCommand(readGeminiWrapperBootstrapPythonCommand(env), ["-m", "venv", venvDir], 120_000);
|
|
450
|
+
if (!venvResult.ok) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const installResult = await runQuietCommand(pythonCommand, ["-m", "pip", "install", `gemini_webapi==${geminiWebApiVersion}`, "browser-cookie3"], 180_000);
|
|
456
|
+
return installResult.ok && (await isGeminiBrowserCookieImportInstalled(pythonCommand));
|
|
457
|
+
}
|
|
458
|
+
function isLocalWrapperUrl(baseUrl) {
|
|
459
|
+
try {
|
|
460
|
+
const url = new URL(baseUrl);
|
|
461
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function normalizeGeminiWrapperModel(model) {
|
|
468
|
+
const trimmedModel = model.trim();
|
|
469
|
+
return trimmedModel || defaultGeminiWrapperModel;
|
|
470
|
+
}
|
|
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) {
|
|
477
|
+
const normalizedModel = normalizeGeminiWrapperModel(model).trim();
|
|
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(" ");
|
|
587
|
+
}
|
|
588
|
+
function isLikelyGeminiWrapperChatModel(model) {
|
|
589
|
+
const normalizedModel = model.toLowerCase();
|
|
590
|
+
return !/(embedding|embed|imagen|veo|tts|audio|speech|rerank|rank|bidi|live)/.test(normalizedModel);
|
|
591
|
+
}
|
|
592
|
+
function isUnauthenticatedGeminiWebError(error) {
|
|
593
|
+
return Boolean(error?.includes("Gemini web cookies are expired or unauthenticated"));
|
|
594
|
+
}
|
|
595
|
+
function isUnauthenticatedGeminiWebStatus(status) {
|
|
596
|
+
return /unauth|expired|invalid/i.test(status ?? "");
|
|
597
|
+
}
|
|
598
|
+
function readGeminiWrapperRuntimeOptions(env = process.env) {
|
|
599
|
+
return {
|
|
600
|
+
maxTokens: readPositiveInteger(env.PATCHPILOT_NUM_PREDICT, 8192),
|
|
601
|
+
temperature: readTemperature(env.PATCHPILOT_TEMPERATURE, 0.1),
|
|
602
|
+
bridgeMinIntervalMs: readNonNegativeInteger(env.PATCHPILOT_GEMINI_WRAPPER_MIN_INTERVAL_MS, 1500),
|
|
603
|
+
bridgeTimeoutMs: readPositiveInteger(env.PATCHPILOT_GEMINI_WRAPPER_TIMEOUT_MS, 180_000)
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function toBridgePrompt(messages, formatJson) {
|
|
607
|
+
const body = messages
|
|
608
|
+
.map((message) => `${message.role.toUpperCase()}:\n${message.content}`)
|
|
609
|
+
.join("\n\n");
|
|
610
|
+
return formatJson ? `${body}\n\nReturn only valid JSON.` : body;
|
|
611
|
+
}
|
|
612
|
+
function toTelemetry(payload, durationMs, model) {
|
|
613
|
+
const promptTokens = payload.usage?.prompt_tokens ?? 0;
|
|
614
|
+
const responseTokens = payload.usage?.completion_tokens ?? 0;
|
|
615
|
+
return attachTokenCost({
|
|
616
|
+
promptTokens,
|
|
617
|
+
cachedPromptTokens: payload.usage?.prompt_tokens_details?.cached_tokens ?? 0,
|
|
618
|
+
cacheWriteTokens: payload.usage?.prompt_tokens_details?.cache_write_tokens ?? 0,
|
|
619
|
+
responseTokens,
|
|
620
|
+
totalTokens: payload.usage?.total_tokens ?? promptTokens + responseTokens,
|
|
621
|
+
evalTokensPerSecond: responseTokens > 0 && durationMs > 0 ? responseTokens / (durationMs / 1000) : null,
|
|
622
|
+
promptDurationMs: 0,
|
|
623
|
+
responseDurationMs: durationMs,
|
|
624
|
+
totalDurationMs: durationMs,
|
|
625
|
+
tokenSource: "provider"
|
|
626
|
+
}, "gemini-wrapper", model);
|
|
627
|
+
}
|
|
628
|
+
function toEstimatedTelemetry(prompt, content, durationMs, model) {
|
|
629
|
+
const promptTokens = estimateTokens(prompt);
|
|
630
|
+
const responseTokens = estimateTokens(content);
|
|
631
|
+
return attachTokenCost({
|
|
632
|
+
promptTokens,
|
|
633
|
+
cachedPromptTokens: 0,
|
|
634
|
+
cacheWriteTokens: 0,
|
|
635
|
+
responseTokens,
|
|
636
|
+
totalTokens: promptTokens + responseTokens,
|
|
637
|
+
evalTokensPerSecond: responseTokens > 0 && durationMs > 0 ? responseTokens / (durationMs / 1000) : null,
|
|
638
|
+
promptDurationMs: 0,
|
|
639
|
+
responseDurationMs: durationMs,
|
|
640
|
+
totalDurationMs: durationMs,
|
|
641
|
+
tokenSource: "estimated"
|
|
642
|
+
}, "gemini-wrapper", model);
|
|
643
|
+
}
|
|
644
|
+
let geminiBridgeQueue = Promise.resolve();
|
|
645
|
+
let lastGeminiBridgeStartedAt = 0;
|
|
646
|
+
function getGeminiWrapperBridgeTimeoutMs(model, configuredTimeoutMs = readGeminiWrapperRuntimeOptions().bridgeTimeoutMs) {
|
|
647
|
+
const timeoutMs = configuredTimeoutMs ?? 180_000;
|
|
648
|
+
return model.includes("pro") ? Math.max(timeoutMs, 240_000) : timeoutMs;
|
|
649
|
+
}
|
|
650
|
+
function runThrottledGeminiWebApiBridge(pythonCommand, input, signal, minIntervalMs = readGeminiWrapperRuntimeOptions().bridgeMinIntervalMs, timeoutMs = getGeminiWrapperBridgeTimeoutMs(input.model)) {
|
|
651
|
+
const run = async () => {
|
|
652
|
+
const intervalMs = minIntervalMs ?? 0;
|
|
653
|
+
const waitMs = Math.max(0, intervalMs - (Date.now() - lastGeminiBridgeStartedAt));
|
|
654
|
+
if (waitMs > 0) {
|
|
655
|
+
await sleep(waitMs, signal);
|
|
656
|
+
}
|
|
657
|
+
lastGeminiBridgeStartedAt = Date.now();
|
|
658
|
+
return await runGeminiWebApiBridge(pythonCommand, input, timeoutMs, signal);
|
|
659
|
+
};
|
|
660
|
+
const result = geminiBridgeQueue.then(run, run);
|
|
661
|
+
geminiBridgeQueue = result.catch(() => undefined);
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
function runGeminiWebApiBridge(pythonCommand, input, timeoutMs, signal) {
|
|
665
|
+
return new Promise((resolve, reject) => {
|
|
666
|
+
const cookieCacheDir = getGeminiWrapperCookieCacheDir();
|
|
667
|
+
mkdirSync(cookieCacheDir, {
|
|
668
|
+
recursive: true,
|
|
669
|
+
mode: 0o700
|
|
670
|
+
});
|
|
671
|
+
tryChmod(cookieCacheDir, 0o700);
|
|
672
|
+
const child = spawn(pythonCommand, ["-c", geminiWebApiBridgeScript], {
|
|
673
|
+
env: {
|
|
674
|
+
...process.env,
|
|
675
|
+
GEMINI_COOKIE_PATH: cookieCacheDir
|
|
676
|
+
},
|
|
677
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
678
|
+
detached: process.platform !== "win32",
|
|
679
|
+
windowsHide: true
|
|
680
|
+
});
|
|
681
|
+
let settled = false;
|
|
682
|
+
let pendingKillError = null;
|
|
683
|
+
let killTimer = null;
|
|
684
|
+
const cleanup = () => {
|
|
685
|
+
clearTimeout(timeout);
|
|
686
|
+
if (killTimer) {
|
|
687
|
+
clearTimeout(killTimer);
|
|
688
|
+
}
|
|
689
|
+
signal?.removeEventListener("abort", abort);
|
|
690
|
+
};
|
|
691
|
+
const settleReject = (error) => {
|
|
692
|
+
if (settled) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
settled = true;
|
|
696
|
+
cleanup();
|
|
697
|
+
reject(error);
|
|
698
|
+
};
|
|
699
|
+
const settleResolve = (output) => {
|
|
700
|
+
if (settled) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
settled = true;
|
|
704
|
+
cleanup();
|
|
705
|
+
resolve(output);
|
|
706
|
+
};
|
|
707
|
+
const killChild = (signalName) => {
|
|
708
|
+
if (child.pid && process.platform !== "win32") {
|
|
709
|
+
try {
|
|
710
|
+
process.kill(-child.pid, signalName);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
// Fall through to killing the child directly.
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
child.kill(signalName);
|
|
718
|
+
};
|
|
719
|
+
const terminateChild = (error) => {
|
|
720
|
+
if (settled || pendingKillError) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
pendingKillError = error;
|
|
724
|
+
killChild("SIGTERM");
|
|
725
|
+
killTimer = setTimeout(() => {
|
|
726
|
+
killChild("SIGKILL");
|
|
727
|
+
}, 1500);
|
|
728
|
+
};
|
|
729
|
+
const abort = () => {
|
|
730
|
+
terminateChild(new Error("Gemini-API bridge request aborted."));
|
|
731
|
+
};
|
|
732
|
+
signal?.addEventListener("abort", abort, {
|
|
733
|
+
once: true
|
|
734
|
+
});
|
|
735
|
+
const timeout = setTimeout(() => {
|
|
736
|
+
terminateChild(new Error(`Gemini-API bridge timed out after ${Math.round(timeoutMs / 1000)}s.`));
|
|
737
|
+
}, timeoutMs);
|
|
738
|
+
let stdout = "";
|
|
739
|
+
let stderr = "";
|
|
740
|
+
child.stdout.on("data", (chunk) => {
|
|
741
|
+
stdout = appendClipped(stdout, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
|
|
742
|
+
});
|
|
743
|
+
child.stderr.on("data", (chunk) => {
|
|
744
|
+
stderr = appendClipped(stderr, chunk.toString("utf8"), geminiBridgeOutputMaxBytes);
|
|
745
|
+
});
|
|
746
|
+
child.on("error", (error) => {
|
|
747
|
+
settleReject(error);
|
|
748
|
+
});
|
|
749
|
+
child.on("close", (exitCode) => {
|
|
750
|
+
if (pendingKillError) {
|
|
751
|
+
settleReject(pendingKillError);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (exitCode !== 0) {
|
|
755
|
+
settleReject(new Error(stderr.trim() || `Gemini-API bridge exited with ${exitCode}.`));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
settleResolve(JSON.parse(stdout));
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
settleReject(new Error(`Gemini-API bridge returned invalid JSON.${stderr.trim() ? ` ${stderr.trim()}` : ""}`));
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
child.stdin.end(JSON.stringify({
|
|
766
|
+
...input,
|
|
767
|
+
timeoutSeconds: Math.max(30, Math.min(300, Math.floor(timeoutMs / 1000)))
|
|
768
|
+
}));
|
|
769
|
+
});
|
|
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
|
+
`;
|
|
961
|
+
const geminiWebApiBridgeScript = String.raw `
|
|
962
|
+
import asyncio
|
|
963
|
+
import json
|
|
964
|
+
import os
|
|
965
|
+
import sys
|
|
966
|
+
|
|
967
|
+
async def main():
|
|
968
|
+
from gemini_webapi import GeminiClient
|
|
969
|
+
|
|
970
|
+
payload = json.load(sys.stdin)
|
|
971
|
+
cookies = {}
|
|
972
|
+
cookies_path = payload.get("cookiesJson")
|
|
973
|
+
if cookies_path:
|
|
974
|
+
with open(cookies_path, "r", encoding="utf-8") as handle:
|
|
975
|
+
data = json.load(handle)
|
|
976
|
+
if isinstance(data, dict) and isinstance(data.get("cookies"), dict):
|
|
977
|
+
cookies.update(data["cookies"])
|
|
978
|
+
elif isinstance(data, dict) and isinstance(data.get("cookies"), list):
|
|
979
|
+
cookies.update({item.get("name"): item.get("value") for item in data["cookies"] if item.get("name") and item.get("value")})
|
|
980
|
+
elif isinstance(data, list):
|
|
981
|
+
cookies.update({item.get("name"): item.get("value") for item in data if item.get("name") and item.get("value")})
|
|
982
|
+
elif isinstance(data, dict):
|
|
983
|
+
cookies.update({key: value for key, value in data.items() if isinstance(value, str)})
|
|
984
|
+
|
|
985
|
+
psid = cookies.get("__Secure-1PSID") or payload.get("secure1psid") or os.getenv("GEMINI_SECURE_1PSID")
|
|
986
|
+
psidts = cookies.get("__Secure-1PSIDTS") or payload.get("secure1psidts") or os.getenv("GEMINI_SECURE_1PSIDTS") or ""
|
|
987
|
+
if not psid:
|
|
988
|
+
print(json.dumps({"error": "Missing __Secure-1PSID. Set PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON or GEMINI_SECURE_1PSID."}))
|
|
989
|
+
return
|
|
990
|
+
|
|
991
|
+
extra = {key: value for key, value in cookies.items() if key not in {"__Secure-1PSID", "__Secure-1PSIDTS"}}
|
|
992
|
+
attempt_timeout = max(30, int(payload.get("timeoutSeconds") or 60))
|
|
993
|
+
|
|
994
|
+
def is_transient_network_error(message):
|
|
995
|
+
lower = message.lower()
|
|
996
|
+
return (
|
|
997
|
+
"curl: (28)" in lower
|
|
998
|
+
or "curl: (56)" in lower
|
|
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
|
|
1006
|
+
or "operation timed out" in lower
|
|
1007
|
+
or "readtimeout" in lower
|
|
1008
|
+
or "timeouterror" in lower
|
|
1009
|
+
or "temporarily unavailable" in lower
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
def account_status_name(client):
|
|
1013
|
+
status = getattr(client, "account_status", None)
|
|
1014
|
+
return getattr(status, "name", str(status or ""))
|
|
1015
|
+
|
|
1016
|
+
def expired_cookie_error():
|
|
1017
|
+
return "Gemini web cookies are expired or unauthenticated. Refresh ~/.patchpilot/gemini-cookies.json through Gemini-Wrapper onboarding."
|
|
1018
|
+
|
|
1019
|
+
def clear_cookie_cache():
|
|
1020
|
+
cache_dir = os.getenv("GEMINI_COOKIE_PATH")
|
|
1021
|
+
if not cache_dir:
|
|
1022
|
+
return
|
|
1023
|
+
try:
|
|
1024
|
+
for filename in os.listdir(cache_dir):
|
|
1025
|
+
if filename.startswith(".cached_cookies_") and filename.endswith(".json"):
|
|
1026
|
+
os.remove(os.path.join(cache_dir, filename))
|
|
1027
|
+
except OSError:
|
|
1028
|
+
pass
|
|
1029
|
+
|
|
1030
|
+
async def generate_once(psidts_value):
|
|
1031
|
+
client = GeminiClient(secure_1psid=psid, secure_1psidts=psidts_value, cookies=extra or None, proxy=payload.get("proxy"))
|
|
1032
|
+
await client.init(timeout=attempt_timeout, auto_refresh=True, verbose=False)
|
|
1033
|
+
try:
|
|
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
|
+
|
|
1038
|
+
if payload.get("command") == "authCheck":
|
|
1039
|
+
return {"content": "ok", "accountStatus": status_name}
|
|
1040
|
+
|
|
1041
|
+
if payload.get("command") == "models":
|
|
1042
|
+
models = []
|
|
1043
|
+
model_descriptors = []
|
|
1044
|
+
for model in client.list_models() or []:
|
|
1045
|
+
if not getattr(model, "is_available", True):
|
|
1046
|
+
continue
|
|
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
|
|
1069
|
+
if name:
|
|
1070
|
+
models.append(name)
|
|
1071
|
+
return {"models": models, "modelDescriptors": model_descriptors, "accountStatus": status_name}
|
|
1072
|
+
|
|
1073
|
+
request_model = payload.get("model") or ""
|
|
1074
|
+
request_kwargs = {"temporary": True}
|
|
1075
|
+
if request_model:
|
|
1076
|
+
request_kwargs["model"] = request_model
|
|
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)
|
|
1094
|
+
text = getattr(response, "text", None) or str(response)
|
|
1095
|
+
result = {"content": text, "accountStatus": status_name, "model": used_model}
|
|
1096
|
+
if model_warning:
|
|
1097
|
+
result["warning"] = model_warning
|
|
1098
|
+
return result
|
|
1099
|
+
finally:
|
|
1100
|
+
await client.close()
|
|
1101
|
+
|
|
1102
|
+
async def generate_with_timestamp(psidts_value):
|
|
1103
|
+
last_error = None
|
|
1104
|
+
max_attempts = 2 if payload.get("files") else 3
|
|
1105
|
+
for attempt in range(max_attempts):
|
|
1106
|
+
try:
|
|
1107
|
+
return await asyncio.wait_for(generate_once(psidts_value), timeout=attempt_timeout)
|
|
1108
|
+
except asyncio.TimeoutError:
|
|
1109
|
+
if attempt >= max_attempts - 1:
|
|
1110
|
+
raise TimeoutError(f"Gemini-API bridge attempt timed out after {attempt_timeout}s.")
|
|
1111
|
+
await asyncio.sleep(1.5 * (attempt + 1))
|
|
1112
|
+
except Exception as exc:
|
|
1113
|
+
last_error = exc
|
|
1114
|
+
if attempt >= max_attempts - 1 or not is_transient_network_error(str(exc)):
|
|
1115
|
+
raise
|
|
1116
|
+
await asyncio.sleep(1.5 * (attempt + 1))
|
|
1117
|
+
raise last_error
|
|
1118
|
+
|
|
1119
|
+
try:
|
|
1120
|
+
result = await generate_with_timestamp(psidts)
|
|
1121
|
+
except Exception as exc:
|
|
1122
|
+
message = str(exc)
|
|
1123
|
+
if psidts and ("__Secure-1PSIDTS" in message or "SECURE_1PSIDTS" in message):
|
|
1124
|
+
clear_cookie_cache()
|
|
1125
|
+
result = await generate_with_timestamp("")
|
|
1126
|
+
else:
|
|
1127
|
+
raise
|
|
1128
|
+
else:
|
|
1129
|
+
if psidts and isinstance(result, dict) and result.get("error") == expired_cookie_error():
|
|
1130
|
+
clear_cookie_cache()
|
|
1131
|
+
retry_result = await generate_with_timestamp("")
|
|
1132
|
+
if not retry_result.get("error"):
|
|
1133
|
+
retry_result["warning"] = "Retried without stale __Secure-1PSIDTS."
|
|
1134
|
+
print(json.dumps(retry_result))
|
|
1135
|
+
return
|
|
1136
|
+
print(json.dumps(result))
|
|
1137
|
+
return
|
|
1138
|
+
|
|
1139
|
+
result["warning"] = "Retried without stale __Secure-1PSIDTS."
|
|
1140
|
+
print(json.dumps(result))
|
|
1141
|
+
|
|
1142
|
+
try:
|
|
1143
|
+
asyncio.run(main())
|
|
1144
|
+
except Exception as exc:
|
|
1145
|
+
message = str(exc)
|
|
1146
|
+
if "currently unavailable or the request structure is outdated" in message:
|
|
1147
|
+
message = f"{message} Try /model auto and refresh Gemini-Wrapper cookies if this persists."
|
|
1148
|
+
print(json.dumps({"error": message}))
|
|
1149
|
+
`;
|
|
1150
|
+
function cleanUndefined(value) {
|
|
1151
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
1152
|
+
}
|
|
1153
|
+
async function readGeminiWrapperResponse(response) {
|
|
1154
|
+
const text = await response.text().catch(() => "");
|
|
1155
|
+
if (!text.trim()) {
|
|
1156
|
+
return {
|
|
1157
|
+
payload: {},
|
|
1158
|
+
text: ""
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
return {
|
|
1163
|
+
payload: JSON.parse(text),
|
|
1164
|
+
text
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
catch {
|
|
1168
|
+
return {
|
|
1169
|
+
payload: {},
|
|
1170
|
+
text
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
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
|
+
}
|
|
1180
|
+
function readPositiveInteger(value, fallback) {
|
|
1181
|
+
const parsedValue = Number.parseInt(value ?? "", 10);
|
|
1182
|
+
return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : fallback;
|
|
1183
|
+
}
|
|
1184
|
+
function readNonNegativeInteger(value, fallback) {
|
|
1185
|
+
const parsedValue = Number.parseInt(value ?? "", 10);
|
|
1186
|
+
return Number.isFinite(parsedValue) && parsedValue >= 0 ? parsedValue : fallback;
|
|
1187
|
+
}
|
|
1188
|
+
function readTemperature(value, fallback) {
|
|
1189
|
+
const parsedValue = Number.parseFloat(value ?? "");
|
|
1190
|
+
return Number.isFinite(parsedValue) && parsedValue >= 0 ? parsedValue : fallback;
|
|
1191
|
+
}
|
|
1192
|
+
function sleep(ms, signal) {
|
|
1193
|
+
if (ms <= 0) {
|
|
1194
|
+
return Promise.resolve();
|
|
1195
|
+
}
|
|
1196
|
+
return new Promise((resolve, reject) => {
|
|
1197
|
+
if (signal?.aborted) {
|
|
1198
|
+
reject(new Error("Gemini-API bridge request aborted."));
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const timeout = setTimeout(() => {
|
|
1202
|
+
signal?.removeEventListener("abort", abort);
|
|
1203
|
+
resolve();
|
|
1204
|
+
}, ms);
|
|
1205
|
+
const abort = () => {
|
|
1206
|
+
clearTimeout(timeout);
|
|
1207
|
+
reject(new Error("Gemini-API bridge request aborted."));
|
|
1208
|
+
};
|
|
1209
|
+
signal?.addEventListener("abort", abort, {
|
|
1210
|
+
once: true
|
|
1211
|
+
});
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
function tryChmod(filePath, mode) {
|
|
1215
|
+
try {
|
|
1216
|
+
chmodSync(filePath, mode);
|
|
1217
|
+
}
|
|
1218
|
+
catch {
|
|
1219
|
+
// Best-effort hardening for platforms that do not support POSIX permissions.
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function runQuietCommand(command, args, timeoutMs) {
|
|
1223
|
+
return new Promise((resolve) => {
|
|
1224
|
+
const child = spawn(command, args, {
|
|
1225
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1226
|
+
windowsHide: true
|
|
1227
|
+
});
|
|
1228
|
+
const timeout = setTimeout(() => {
|
|
1229
|
+
child.kill();
|
|
1230
|
+
resolve({
|
|
1231
|
+
ok: false,
|
|
1232
|
+
output: `${command} ${args.join(" ")} timed out.`
|
|
1233
|
+
});
|
|
1234
|
+
}, timeoutMs);
|
|
1235
|
+
let output = "";
|
|
1236
|
+
child.stdout.on("data", (chunk) => {
|
|
1237
|
+
output += chunk.toString("utf8");
|
|
1238
|
+
});
|
|
1239
|
+
child.stderr.on("data", (chunk) => {
|
|
1240
|
+
output += chunk.toString("utf8");
|
|
1241
|
+
});
|
|
1242
|
+
child.on("error", (error) => {
|
|
1243
|
+
clearTimeout(timeout);
|
|
1244
|
+
resolve({
|
|
1245
|
+
ok: false,
|
|
1246
|
+
output: error.message
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
child.on("close", (exitCode) => {
|
|
1250
|
+
clearTimeout(timeout);
|
|
1251
|
+
resolve({
|
|
1252
|
+
ok: exitCode === 0,
|
|
1253
|
+
output: output.trim()
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
//# sourceMappingURL=geminiWrapper.js.map
|