bibibi12345 commited on
Commit
1ac79d3
·
0 Parent(s):

first version

Browse files
Files changed (4) hide show
  1. .gitignore +78 -0
  2. README.md +118 -0
  3. gemini_proxy.py +480 -0
  4. requirements.txt +6 -0
.gitignore ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Credential files - should never be committed
2
+ oauth_creds.json
3
+ *.json
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ *.py,cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+
52
+ # Virtual environments
53
+ .env
54
+ .venv
55
+ env/
56
+ venv/
57
+ ENV/
58
+ env.bak/
59
+ venv.bak/
60
+
61
+ # IDE
62
+ .vscode/
63
+ .idea/
64
+ *.swp
65
+ *.swo
66
+ *~
67
+
68
+ # OS
69
+ .DS_Store
70
+ .DS_Store?
71
+ ._*
72
+ .Spotlight-V100
73
+ .Trashes
74
+ ehthumbs.db
75
+ Thumbs.db
76
+
77
+ # Logs
78
+ *.log
README.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gemini CLI to API Proxy
2
+
3
+ A proxy server that converts Google's Gemini CLI authentication to standard API format, allowing you to use Gemini models with any OpenAI-compatible client.
4
+
5
+ ## Features
6
+
7
+ - OAuth 2.0 authentication with Google Cloud
8
+ - Automatic project ID detection and caching
9
+ - Support for both streaming and non-streaming requests
10
+ - Converts Google's internal API format to standard Gemini API format
11
+ - Credential caching for seamless restarts
12
+
13
+ ## Prerequisites
14
+
15
+ - Python 3.7 or higher
16
+ - Google Cloud account with Gemini API access
17
+ - Required Python packages (see `requirements.txt`)
18
+
19
+ ## Installation
20
+
21
+ 1. Clone or download this repository
22
+ 2. Install the required dependencies:
23
+ ```bash
24
+ pip install -r requirements.txt
25
+ ```
26
+
27
+ ## Setup and Usage
28
+
29
+ ### First Time Setup
30
+
31
+ 1. **Start the proxy server:**
32
+ ```bash
33
+ python gemini_proxy.py
34
+ ```
35
+
36
+ 2. **Authenticate with Google:**
37
+ - On first run, the proxy will display an authentication URL
38
+ - Open the URL in your browser and sign in with your Google account
39
+ - Grant the necessary permissions
40
+ - The browser will show "Authentication successful!" when complete
41
+ - The proxy will automatically save your credentials for future use
42
+
43
+ 3. **Project ID Detection:**
44
+ - The proxy will automatically detect and cache your Google Cloud project ID
45
+ - This only happens once - subsequent runs will use the cached project ID
46
+
47
+ ### Regular Usage
48
+
49
+ After initial setup, simply run:
50
+ ```bash
51
+ python gemini_proxy.py
52
+ ```
53
+
54
+ The proxy server will start on `http://localhost:8888` and display:
55
+ ```
56
+ Starting Gemini proxy server on http://localhost:8888
57
+ Send your Gemini API requests to this address.
58
+ ```
59
+
60
+ ### Using with API Clients
61
+
62
+ Configure your Gemini API client to use `http://localhost:8888` as the base URL. The proxy accepts standard Gemini API requests and handles the authentication automatically.
63
+
64
+ Example request:
65
+ ```bash
66
+ curl -X POST http://localhost:8888/v1/models/gemini-pro:generateContent \
67
+ -H "Content-Type: application/json" \
68
+ -d '{
69
+ "contents": [{
70
+ "parts": [{"text": "Hello, how are you?"}]
71
+ }]
72
+ }'
73
+ ```
74
+
75
+ ## Configuration
76
+
77
+ The proxy uses the following configuration:
78
+ - **Port:** 8888 (hardcoded)
79
+ - **Credential file:** `oauth_creds.json` (automatically created)
80
+ - **Scopes:** Cloud Platform, User Info (email/profile), OpenID
81
+
82
+ ## File Structure
83
+
84
+ - `gemini_proxy.py` - Main proxy server
85
+ - `oauth_creds.json` - Cached OAuth credentials and project ID (auto-generated)
86
+ - `requirements.txt` - Python dependencies
87
+ - `.gitignore` - Prevents credential files from being committed
88
+
89
+ ## Troubleshooting
90
+
91
+ ### Port Already in Use
92
+ If you see "error while attempting to bind on address", another instance is already running. Stop the existing process or use a different port.
93
+
94
+ ### Authentication Issues
95
+ - Delete `oauth_creds.json` and restart to re-authenticate
96
+ - Ensure your Google account has access to Google Cloud and Gemini API
97
+ - Check that the required scopes are granted during authentication
98
+
99
+ ### Project ID Issues
100
+ - The proxy automatically detects your project ID on first run
101
+ - If detection fails, check your Google Cloud project permissions
102
+ - Delete `oauth_creds.json` to force re-detection
103
+
104
+ ## Security Notes
105
+
106
+ - **Never commit `oauth_creds.json`** - it contains sensitive authentication tokens
107
+ - The `.gitignore` file is configured to prevent accidental commits
108
+ - Credentials are stored locally and refreshed automatically when expired
109
+ - The proxy runs on localhost only for security
110
+
111
+ ## API Compatibility
112
+
113
+ This proxy converts between:
114
+ - **Input:** Standard Gemini API format
115
+ - **Output:** Standard Gemini API responses
116
+ - **Internal:** Google's Cloud Code Assist API format
117
+
118
+ The conversion is transparent to API clients.
gemini_proxy.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import requests
4
+ import re
5
+ import uvicorn
6
+ from datetime import datetime
7
+ from fastapi import FastAPI, Request, Response
8
+ from fastapi.responses import StreamingResponse
9
+ from http.server import BaseHTTPRequestHandler, HTTPServer
10
+ from urllib.parse import urlparse, parse_qs
11
+ import ijson
12
+
13
+ from google.oauth2.credentials import Credentials
14
+ from google_auth_oauthlib.flow import Flow
15
+ from google.auth.transport.requests import Request as GoogleAuthRequest
16
+
17
+ # --- Configuration ---
18
+ CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
19
+ CLIENT_SECRET = "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"
20
+ SCOPES = [
21
+ "https://www.googleapis.com/auth/cloud-platform",
22
+ "https://www.googleapis.com/auth/userinfo.email",
23
+ "https://www.googleapis.com/auth/userinfo.profile",
24
+ "openid",
25
+ ]
26
+ GEMINI_DIR = os.path.dirname(os.path.abspath(__file__)) # Same directory as the script
27
+ CREDENTIAL_FILE = os.path.join(GEMINI_DIR, "oauth_creds.json")
28
+ CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"
29
+
30
+ # --- Global State ---
31
+ credentials = None
32
+ user_project_id = None
33
+
34
+ app = FastAPI()
35
+
36
+ # Helper class to adapt a generator of bytes into a file-like object
37
+ # that ijson can read from.
38
+ class _GeneratorStream:
39
+ def __init__(self, generator):
40
+ self.generator = generator
41
+ self.buffer = b''
42
+
43
+ def read(self, size=-1):
44
+ # This read implementation is crucial for streaming.
45
+ # It must not block to read the entire stream if size is -1.
46
+ if size == -1:
47
+ # If asked to read all, return what's in the buffer and get one more chunk.
48
+ try:
49
+ self.buffer += next(self.generator)
50
+ except StopIteration:
51
+ pass
52
+ data = self.buffer
53
+ self.buffer = b''
54
+ return data
55
+
56
+ # Otherwise, read from the generator until we have enough bytes.
57
+ while len(self.buffer) < size:
58
+ try:
59
+ self.buffer += next(self.generator)
60
+ except StopIteration:
61
+ # Generator is exhausted.
62
+ break
63
+
64
+ data = self.buffer[:size]
65
+ self.buffer = self.buffer[size:]
66
+ return data
67
+
68
+ class _OAuthCallbackHandler(BaseHTTPRequestHandler):
69
+ auth_code = None
70
+ def do_GET(self):
71
+ query_components = parse_qs(urlparse(self.path).query)
72
+ code = query_components.get("code", [None])[0]
73
+ if code:
74
+ _OAuthCallbackHandler.auth_code = code
75
+ self.send_response(200)
76
+ self.send_header("Content-type", "text/html")
77
+ self.end_headers()
78
+ self.wfile.write(b"<h1>Authentication successful!</h1><p>You can close this window and restart the proxy.</p>")
79
+ else:
80
+ self.send_response(400)
81
+ self.send_header("Content-type", "text/html")
82
+ self.end_headers()
83
+ self.wfile.write(b"<h1>Authentication failed.</h1><p>Please try again.</p>")
84
+
85
+ def get_user_project_id(creds):
86
+ """Gets the user's project ID from cache or by probing the API."""
87
+ global user_project_id
88
+ if user_project_id:
89
+ return user_project_id
90
+
91
+ # First, try to load project ID from credential file
92
+ if os.path.exists(CREDENTIAL_FILE):
93
+ try:
94
+ with open(CREDENTIAL_FILE, "r") as f:
95
+ creds_data = json.load(f)
96
+ cached_project_id = creds_data.get("project_id")
97
+ if cached_project_id:
98
+ user_project_id = cached_project_id
99
+ print(f"Loaded project ID from cache: {user_project_id}")
100
+ return user_project_id
101
+ except Exception as e:
102
+ print(f"Could not load project ID from cache: {e}")
103
+
104
+ # If not found in cache, probe for it
105
+ print("Project ID not found in cache. Probing for user project ID...")
106
+ headers = {
107
+ "Authorization": f"Bearer {creds.token}",
108
+ "Content-Type": "application/json",
109
+ }
110
+
111
+ probe_payload = {
112
+ "cloudaicompanionProject": "gcp-project",
113
+ "metadata": {
114
+ "ideType": "VSCODE",
115
+ "pluginType": "GEMINI"
116
+ }
117
+ }
118
+
119
+ try:
120
+ resp = requests.post(
121
+ f"{CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist",
122
+ data=json.dumps(probe_payload),
123
+ headers=headers,
124
+ )
125
+ resp.raise_for_status()
126
+ data = resp.json()
127
+ user_project_id = data.get("cloudaicompanionProject")
128
+ if not user_project_id:
129
+ raise ValueError("Could not find 'cloudaicompanionProject' in loadCodeAssist response.")
130
+ print(f"Successfully fetched user project ID: {user_project_id}")
131
+
132
+ # Save the project ID to the credential file for future use
133
+ save_credentials(creds, user_project_id)
134
+ print("Project ID saved to credential file for future use.")
135
+
136
+ return user_project_id
137
+ except requests.exceptions.HTTPError as e:
138
+ print(f"Error fetching project ID: {e.response.text}")
139
+ raise
140
+
141
+ def save_credentials(creds, project_id=None):
142
+ os.makedirs(GEMINI_DIR, exist_ok=True)
143
+ creds_data = {
144
+ "access_token": creds.token,
145
+ "refresh_token": creds.refresh_token,
146
+ "scope": " ".join(creds.scopes),
147
+ "token_type": "Bearer",
148
+ "expiry_date": creds.expiry.isoformat() if creds.expiry else None,
149
+ }
150
+
151
+ # If project_id is provided, save it; otherwise preserve existing project_id
152
+ if project_id:
153
+ creds_data["project_id"] = project_id
154
+ elif os.path.exists(CREDENTIAL_FILE):
155
+ try:
156
+ with open(CREDENTIAL_FILE, "r") as f:
157
+ existing_data = json.load(f)
158
+ if "project_id" in existing_data:
159
+ creds_data["project_id"] = existing_data["project_id"]
160
+ except Exception:
161
+ pass # If we can't read existing file, just continue without project_id
162
+
163
+ with open(CREDENTIAL_FILE, "w") as f:
164
+ json.dump(creds_data, f)
165
+
166
+ def get_credentials():
167
+ """Loads credentials from cache or initiates the OAuth 2.0 flow."""
168
+ global credentials
169
+
170
+ if credentials:
171
+ if credentials.valid:
172
+ return credentials
173
+ if credentials.expired and credentials.refresh_token:
174
+ print("Refreshing expired credentials...")
175
+ try:
176
+ credentials.refresh(GoogleAuthRequest())
177
+ save_credentials(credentials)
178
+ print("Credentials refreshed successfully.")
179
+ return credentials
180
+ except Exception as e:
181
+ print(f"Could not refresh token: {e}. Attempting to load from file.")
182
+
183
+ if os.path.exists(CREDENTIAL_FILE):
184
+ try:
185
+ with open(CREDENTIAL_FILE, "r") as f:
186
+ creds_data = json.load(f)
187
+
188
+ # Load project ID if available
189
+ global user_project_id
190
+ cached_project_id = creds_data.get("project_id")
191
+ if cached_project_id:
192
+ user_project_id = cached_project_id
193
+ print(f"Loaded project ID from credential file: {user_project_id}")
194
+
195
+ expiry = None
196
+ expiry_str = creds_data.get("expiry_date")
197
+ if expiry_str:
198
+ if not isinstance(expiry_str, str) or not expiry_str.strip():
199
+ expiry = None
200
+ elif expiry_str.endswith('Z'):
201
+ expiry_str = expiry_str[:-1] + '+00:00'
202
+ expiry = datetime.fromisoformat(expiry_str)
203
+ else:
204
+ expiry = datetime.fromisoformat(expiry_str)
205
+
206
+ credentials = Credentials(
207
+ token=creds_data.get("access_token"),
208
+ refresh_token=creds_data.get("refresh_token"),
209
+ token_uri="https://oauth2.googleapis.com/token",
210
+ client_id=CLIENT_ID,
211
+ client_secret=CLIENT_SECRET,
212
+ scopes=SCOPES,
213
+ expiry=expiry
214
+ )
215
+
216
+ if credentials.expired and credentials.refresh_token:
217
+ print("Loaded credentials from file are expired. Refreshing...")
218
+ credentials.refresh(GoogleAuthRequest())
219
+ save_credentials(credentials)
220
+
221
+ print("Successfully loaded credentials from cache.")
222
+ return credentials
223
+ except Exception as e:
224
+ print(f"Could not load cached credentials: {e}. Starting new login.")
225
+
226
+ client_config = {
227
+ "installed": {
228
+ "client_id": CLIENT_ID,
229
+ "client_secret": CLIENT_SECRET,
230
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
231
+ "token_uri": "https://oauth2.googleapis.com/token",
232
+ }
233
+ }
234
+ flow = Flow.from_client_config(
235
+ client_config, scopes=SCOPES, redirect_uri="http://localhost:8080"
236
+ )
237
+ auth_url, _ = flow.authorization_url(access_type="offline", prompt="consent")
238
+ print(f"\nPlease open this URL in your browser to log in:\n{auth_url}\n")
239
+
240
+ server = HTTPServer(("", 8080), _OAuthCallbackHandler)
241
+ server.handle_request()
242
+
243
+ auth_code = _OAuthCallbackHandler.auth_code
244
+ if not auth_code:
245
+ print("Failed to retrieve authorization code.")
246
+ return None
247
+
248
+ flow.fetch_token(code=auth_code)
249
+ credentials = flow.credentials
250
+ save_credentials(credentials)
251
+ print("Authentication successful! Credentials saved.")
252
+ return credentials
253
+
254
+
255
+ @app.post("/{full_path:path}")
256
+ async def proxy_request(request: Request, full_path: str):
257
+ creds = get_credentials()
258
+ if not creds:
259
+ return Response(content="Authentication failed. Please restart the proxy to log in.", status_code=500)
260
+
261
+ proj_id = get_user_project_id(creds)
262
+ if not proj_id:
263
+ return Response(content="Failed to get user project ID.", status_code=500)
264
+
265
+ post_data = await request.body()
266
+ path = f"/{full_path}"
267
+ model_name_from_url = None
268
+ action = None
269
+
270
+ model_match = re.match(r"/(v\d+(?:beta)?)/models/([^:]+):(\w+)", path)
271
+
272
+ is_streaming = False
273
+ if model_match:
274
+ model_name_from_url = model_match.group(2)
275
+ action = model_match.group(3)
276
+ target_url = f"{CODE_ASSIST_ENDPOINT}/v1internal:{action}"
277
+ if "stream" in action.lower():
278
+ is_streaming = True
279
+ else:
280
+ target_url = f"{CODE_ASSIST_ENDPOINT}{path}"
281
+
282
+ try:
283
+ incoming_json = json.loads(post_data)
284
+ final_model = model_name_from_url if model_match else incoming_json.get("model")
285
+
286
+ structured_payload = {
287
+ "model": final_model,
288
+ "project": proj_id,
289
+ "request": {
290
+ "contents": incoming_json.get("contents"),
291
+ "systemInstruction": incoming_json.get("systemInstruction"),
292
+ "cachedContent": incoming_json.get("cachedContent"),
293
+ "tools": incoming_json.get("tools"),
294
+ "toolConfig": incoming_json.get("toolConfig"),
295
+ "safetySettings": incoming_json.get("safetySettings"),
296
+ "generationConfig": incoming_json.get("generationConfig"),
297
+ },
298
+ }
299
+ structured_payload["request"] = {
300
+ k: v
301
+ for k, v in structured_payload["request"].items()
302
+ if v is not None
303
+ }
304
+ final_post_data = json.dumps(structured_payload)
305
+ except (json.JSONDecodeError, AttributeError):
306
+ final_post_data = post_data
307
+
308
+ headers = {
309
+ "Authorization": f"Bearer {creds.token}",
310
+ "Content-Type": "application/json",
311
+ # We remove 'Accept-Encoding' to allow the server to send gzip,
312
+ # which it seems to stream correctly. We will decompress on the fly.
313
+ }
314
+
315
+ if is_streaming:
316
+ async def stream_generator():
317
+ try:
318
+ print(f"[STREAM] Starting streaming request to: {target_url}")
319
+ print(f"[STREAM] Request payload size: {len(final_post_data)} bytes")
320
+
321
+ with requests.post(target_url, data=final_post_data, headers=headers, stream=True) as resp:
322
+ print(f"[STREAM] Response status: {resp.status_code}")
323
+ print(f"[STREAM] Response headers: {dict(resp.headers)}")
324
+ resp.raise_for_status()
325
+
326
+ buffer = ""
327
+ brace_count = 0
328
+ in_array = False
329
+ chunk_count = 0
330
+ total_bytes = 0
331
+ objects_yielded = 0
332
+
333
+ print(f"[STREAM] Starting to process chunks...")
334
+
335
+ for chunk in resp.iter_content(chunk_size=1024, decode_unicode=True):
336
+ chunk_count += 1
337
+ chunk_size = len(chunk) if chunk else 0
338
+ total_bytes += chunk_size
339
+
340
+ print(f"[STREAM] Chunk #{chunk_count}: {chunk_size} bytes, total: {total_bytes} bytes")
341
+ if chunk:
342
+ print(f"[STREAM] Chunk content preview: {repr(chunk[:100])}")
343
+
344
+ buffer += chunk
345
+ print(f"[STREAM] Buffer size after chunk: {len(buffer)} chars")
346
+
347
+ # Process complete JSON objects from the buffer
348
+ processing_iterations = 0
349
+ while buffer:
350
+ processing_iterations += 1
351
+ if processing_iterations > 100: # Prevent infinite loops
352
+ print(f"[STREAM] WARNING: Too many processing iterations, breaking")
353
+ break
354
+
355
+ buffer = buffer.lstrip()
356
+
357
+ if not buffer:
358
+ print(f"[STREAM] Buffer empty after lstrip")
359
+ break
360
+
361
+ print(f"[STREAM] Processing buffer (len={len(buffer)}): {repr(buffer[:50])}")
362
+
363
+ # Handle array start
364
+ if buffer.startswith('[') and not in_array:
365
+ print(f"[STREAM] Found array start, entering array mode")
366
+ buffer = buffer[1:].lstrip()
367
+ in_array = True
368
+ continue
369
+
370
+ # Handle array end
371
+ if buffer.startswith(']'):
372
+ print(f"[STREAM] Found array end, stopping processing")
373
+ break
374
+
375
+ # Skip commas between objects
376
+ if buffer.startswith(','):
377
+ print(f"[STREAM] Skipping comma separator")
378
+ buffer = buffer[1:].lstrip()
379
+ continue
380
+
381
+ # Look for complete JSON objects
382
+ if buffer.startswith('{'):
383
+ print(f"[STREAM] Found object start, parsing JSON object...")
384
+ brace_count = 0
385
+ in_string = False
386
+ escape_next = False
387
+ end_pos = -1
388
+
389
+ for i, char in enumerate(buffer):
390
+ if escape_next:
391
+ escape_next = False
392
+ continue
393
+ if char == '\\':
394
+ escape_next = True
395
+ continue
396
+ if char == '"' and not escape_next:
397
+ in_string = not in_string
398
+ continue
399
+ if not in_string:
400
+ if char == '{':
401
+ brace_count += 1
402
+ elif char == '}':
403
+ brace_count -= 1
404
+ if brace_count == 0:
405
+ end_pos = i + 1
406
+ break
407
+
408
+ if end_pos > 0:
409
+ # Found complete JSON object
410
+ json_str = buffer[:end_pos]
411
+ buffer = buffer[end_pos:].lstrip()
412
+
413
+ print(f"[STREAM] Found complete JSON object ({len(json_str)} chars): {repr(json_str[:200])}")
414
+
415
+ try:
416
+ obj = json.loads(json_str)
417
+ print(f"[STREAM] Successfully parsed JSON object with keys: {list(obj.keys())}")
418
+
419
+ if "response" in obj:
420
+ response_chunk = obj["response"]
421
+ objects_yielded += 1
422
+ response_json = json.dumps(response_chunk)
423
+ print(f"[STREAM] Yielding object #{objects_yielded} (response size: {len(response_json)} chars)")
424
+ print(f"[STREAM] Response content preview: {repr(response_json[:200])}")
425
+ yield f"data: {response_json}\n\n"
426
+ else:
427
+ print(f"[STREAM] Object has no 'response' key, skipping")
428
+ except json.JSONDecodeError as e:
429
+ print(f"[STREAM] Failed to parse JSON object: {e}")
430
+ print(f"[STREAM] Problematic JSON: {repr(json_str[:500])}")
431
+ continue
432
+ else:
433
+ # Incomplete object, wait for more data
434
+ print(f"[STREAM] Incomplete JSON object (brace_count={brace_count}), waiting for more data")
435
+ break
436
+ else:
437
+ # Skip unexpected characters
438
+ print(f"[STREAM] Skipping unexpected character: {repr(buffer[0])}")
439
+ buffer = buffer[1:]
440
+
441
+ print(f"[STREAM] Finished processing. Total chunks: {chunk_count}, total bytes: {total_bytes}, objects yielded: {objects_yielded}")
442
+
443
+ except requests.exceptions.RequestException as e:
444
+ print(f"Error during streaming request: {e}")
445
+ error_message = json.dumps({"error": {"message": f"Upstream request failed: {e}"}})
446
+ yield f"data: {error_message}\n\n"
447
+ except Exception as e:
448
+ print(f"An unexpected error occurred during streaming: {e}")
449
+ error_message = json.dumps({"error": {"message": f"An unexpected error occurred: {e}"}})
450
+ yield f"data: {error_message}\n\n"
451
+
452
+ return StreamingResponse(stream_generator(), media_type="text/event-stream")
453
+ else:
454
+ resp = requests.post(target_url, data=final_post_data, headers=headers)
455
+ if resp.status_code == 200:
456
+ try:
457
+ google_api_response = resp.json()
458
+ # The actual response is nested under the "response" key
459
+ # The actual response is nested under the "response" key
460
+ standard_gemini_response = google_api_response.get("response")
461
+ # The standard client expects a list containing the response object
462
+ return Response(content=json.dumps([standard_gemini_response]), status_code=200, media_type="application/json")
463
+ except (json.JSONDecodeError, AttributeError) as e:
464
+ print(f"Error converting to standard Gemini format: {e}")
465
+ # Fallback to sending the original content if conversion fails
466
+ return Response(content=resp.content, status_code=resp.status_code, media_type=resp.headers.get("Content-Type"))
467
+ else:
468
+ return Response(content=resp.content, status_code=resp.status_code, media_type=resp.headers.get("Content-Type"))
469
+
470
+
471
+ if __name__ == "__main__":
472
+ print("Initializing credentials...")
473
+ creds = get_credentials()
474
+ if creds:
475
+ get_user_project_id(creds)
476
+ print("\nStarting Gemini proxy server on http://localhost:8888")
477
+ print("Send your Gemini API requests to this address.")
478
+ uvicorn.run(app, host="0.0.0.0", port=8888)
479
+ else:
480
+ print("\nCould not obtain credentials. Please authenticate and restart the server.")
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ google-auth
4
+ google-auth-oauthlib
5
+ requests
6
+ ijson