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

added authentication and oauth token refresh

Browse files
Files changed (4) hide show
  1. .gitignore +3 -0
  2. README.md +120 -9
  3. gemini_proxy.py +142 -40
  4. requirements.txt +2 -1
.gitignore CHANGED
@@ -2,6 +2,9 @@
2
  oauth_creds.json
3
  *.json
4
 
 
 
 
5
  # Python
6
  __pycache__/
7
  *.py[cod]
 
2
  oauth_creds.json
3
  *.json
4
 
5
+ # Environment configuration
6
+ .env
7
+
8
  # Python
9
  __pycache__/
10
  *.py[cod]
README.md CHANGED
@@ -28,20 +28,31 @@ A proxy server that converts Google's Gemini CLI authentication to standard API
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
@@ -63,7 +74,7 @@ Configure your Gemini API client to use `http://localhost:8888` as the base URL.
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": [{
@@ -72,34 +83,134 @@ curl -X POST http://localhost:8888/v1/models/gemini-pro:generateContent \
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
 
 
28
 
29
  ### First Time Setup
30
 
31
+ 1. **(Optional) Configure your settings:**
32
+ If you know your Google Cloud project ID, you can set it in the `.env` file to skip automatic detection:
33
+ ```bash
34
+ # Edit the .env file
35
+ GEMINI_PROJECT_ID=your-project-id
36
+
37
+ # Optional: Change the default port
38
+ GEMINI_PORT=8888
39
+ ```
40
+
41
+ 2. **Start the proxy server:**
42
  ```bash
43
  python gemini_proxy.py
44
  ```
45
 
46
+ 3. **Authenticate with Google:**
47
  - On first run, the proxy will display an authentication URL
48
  - Open the URL in your browser and sign in with your Google account
49
  - Grant the necessary permissions
50
  - The browser will show "Authentication successful!" when complete
51
  - The proxy will automatically save your credentials for future use
52
 
53
+ 4. **Project ID Detection:**
54
+ - If `GEMINI_PROJECT_ID` is set in the `.env` file, it will be used
55
+ - Otherwise, the proxy will automatically detect and cache your Google Cloud project ID
56
  - This only happens once - subsequent runs will use the cached project ID
57
 
58
  ### Regular Usage
 
74
 
75
  Example request:
76
  ```bash
77
+ curl -X POST "http://localhost:8888/v1/models/gemini-pro:generateContent?key=123456" \
78
  -H "Content-Type: application/json" \
79
  -d '{
80
  "contents": [{
 
83
  }'
84
  ```
85
 
86
+ **Note:** The proxy supports multiple authentication methods. The `key` query parameter is the most compatible with standard Gemini clients.
87
+
88
+ ### Safety Settings
89
+
90
+ The proxy automatically sets default safety settings to `BLOCK_NONE` for all categories if no safety settings are specified in the request. This provides maximum flexibility for content generation. The default categories are:
91
+
92
+ - `HARM_CATEGORY_HARASSMENT`
93
+ - `HARM_CATEGORY_HATE_SPEECH`
94
+ - `HARM_CATEGORY_SEXUALLY_EXPLICIT`
95
+ - `HARM_CATEGORY_DANGEROUS_CONTENT`
96
+
97
+ You can override these defaults by including your own `safetySettings` in the request payload.
98
+
99
+ ## Authentication
100
+
101
+ The proxy supports multiple authentication methods for maximum compatibility:
102
+
103
+ - **Default Password:** `123456`
104
+ - **Configuration:** Set `GEMINI_AUTH_PASSWORD` in `.env` file to change the password
105
+
106
+ ### Authentication Methods
107
+
108
+ 1. **API Key (Query Parameter)** - Compatible with standard Gemini clients:
109
+ ```bash
110
+ curl -X POST "http://localhost:8888/v1/models/gemini-pro:generateContent?key=123456" \
111
+ -H "Content-Type: application/json" \
112
+ -d '{"contents": [{"parts": [{"text": "Hello!"}]}]}'
113
+ ```
114
+
115
+ 2. **Bearer Token** - Standard API token format:
116
+ ```bash
117
+ curl -X POST http://localhost:8888/v1/models/gemini-pro:generateContent \
118
+ -H "Authorization: Bearer 123456" \
119
+ -H "Content-Type: application/json" \
120
+ -d '{"contents": [{"parts": [{"text": "Hello!"}]}]}'
121
+ ```
122
+
123
+ 3. **HTTP Basic Authentication** - Traditional username/password:
124
+ ```bash
125
+ curl -u "user:123456" -X POST http://localhost:8888/v1/models/gemini-pro:generateContent \
126
+ -H "Content-Type: application/json" \
127
+ -d '{"contents": [{"parts": [{"text": "Hello!"}]}]}'
128
+ ```
129
+
130
+ **Python examples:**
131
+ ```python
132
+ import requests
133
+ from requests.auth import HTTPBasicAuth
134
+
135
+ # Method 1: Query parameter
136
+ response = requests.post(
137
+ "http://localhost:8888/v1/models/gemini-pro:generateContent?key=123456",
138
+ json={"contents": [{"parts": [{"text": "Hello!"}]}]}
139
+ )
140
+
141
+ # Method 2: Bearer token
142
+ response = requests.post(
143
+ "http://localhost:8888/v1/models/gemini-pro:generateContent",
144
+ headers={"Authorization": "Bearer 123456"},
145
+ json={"contents": [{"parts": [{"text": "Hello!"}]}]}
146
+ )
147
+
148
+ # Method 3: Basic auth
149
+ response = requests.post(
150
+ "http://localhost:8888/v1/models/gemini-pro:generateContent",
151
+ auth=HTTPBasicAuth("user", "123456"),
152
+ json={"contents": [{"parts": [{"text": "Hello!"}]}]}
153
+ )
154
+ ```
155
+
156
  ## Configuration
157
 
158
  The proxy uses the following configuration:
159
+ - **Port:** 8888 (default, configurable via `.env`)
160
  - **Credential file:** `oauth_creds.json` (automatically created)
161
+ - **Configuration file:** `.env` (optional settings)
162
  - **Scopes:** Cloud Platform, User Info (email/profile), OpenID
163
 
164
+ ### Configuration File (.env)
165
+
166
+ You can create a `.env` file in the same directory as the script to configure the proxy:
167
+
168
+ ```bash
169
+ # Set your Google Cloud Project ID to skip automatic detection
170
+ GEMINI_PROJECT_ID=my-gcp-project-123
171
+
172
+ # Set a custom port (default is 8888)
173
+ GEMINI_PORT=9000
174
+
175
+ # Set authentication password (default is 123456)
176
+ GEMINI_AUTH_PASSWORD=your-secure-password
177
+ ```
178
+
179
+ **Note:** The `.env` file is automatically excluded from version control via `.gitignore`.
180
+
181
  ## File Structure
182
 
183
  - `gemini_proxy.py` - Main proxy server
184
  - `oauth_creds.json` - Cached OAuth credentials and project ID (auto-generated)
185
  - `requirements.txt` - Python dependencies
186
+ - `.env` - Configuration file (optional, create as needed)
187
+ - `.gitignore` - Prevents credential and config files from being committed
188
 
189
  ## Troubleshooting
190
 
191
  ### Port Already in Use
192
  If you see "error while attempting to bind on address", another instance is already running. Stop the existing process or use a different port.
193
 
194
+ ### Authentication Issues (Google OAuth)
195
  - Delete `oauth_creds.json` and restart to re-authenticate
196
  - Ensure your Google account has access to Google Cloud and Gemini API
197
  - Check that the required scopes are granted during authentication
198
 
199
+ ### Authentication Issues (Proxy Access)
200
+ - If you get 401 Unauthorized errors, check your authentication method
201
+ - Default password is `123456` unless changed in `.env` file
202
+ - Try different authentication methods:
203
+ - Query parameter: `?key=123456`
204
+ - Bearer token: `Authorization: Bearer 123456`
205
+ - Basic auth: `Authorization: Basic base64(user:123456)`
206
+ - Most Gemini clients work best with the query parameter method (`?key=password`)
207
+
208
  ### Project ID Issues
209
  - The proxy automatically detects your project ID on first run
210
+ - If detection fails, you can manually set `GEMINI_PROJECT_ID` in the `.env` file
211
+ - Check your Google Cloud project permissions if auto-detection fails
212
  - Delete `oauth_creds.json` to force re-detection
213
+ - Project ID in `.env` file takes priority over cached and auto-detected project IDs
214
 
215
  ## Security Notes
216
 
gemini_proxy.py CHANGED
@@ -3,17 +3,23 @@ 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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"
@@ -26,12 +32,45 @@ SCOPES = [
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.
@@ -83,12 +122,21 @@ class _OAuthCallbackHandler(BaseHTTPRequestHandler):
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:
@@ -101,8 +149,8 @@ def get_user_project_id(creds):
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",
@@ -253,11 +301,26 @@ def get_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)
@@ -278,11 +341,32 @@ async def proxy_request(request: Request, full_path: str):
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,
@@ -292,7 +376,7 @@ async def proxy_request(request: Request, full_path: str):
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
  }
@@ -318,9 +402,31 @@ async def proxy_request(request: Request, full_path: str):
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 = ""
@@ -332,55 +438,44 @@ async def proxy_request(request: Request, full_path: str):
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
@@ -410,36 +505,24 @@ async def proxy_request(request: Request, full_path: str):
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}"}})
@@ -451,12 +534,29 @@ async def proxy_request(request: Request, full_path: str):
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")
@@ -473,8 +573,10 @@ if __name__ == "__main__":
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.")
 
3
  import requests
4
  import re
5
  import uvicorn
6
+ import base64
7
  from datetime import datetime
8
+ from fastapi import FastAPI, Request, Response, HTTPException, Depends
9
  from fastapi.responses import StreamingResponse
10
+ from fastapi.security import HTTPBasic, HTTPBasicCredentials
11
  from http.server import BaseHTTPRequestHandler, HTTPServer
12
  from urllib.parse import urlparse, parse_qs
13
  import ijson
14
+ from dotenv import load_dotenv
15
 
16
  from google.oauth2.credentials import Credentials
17
  from google_auth_oauthlib.flow import Flow
18
  from google.auth.transport.requests import Request as GoogleAuthRequest
19
 
20
+ # Load environment variables from .env file
21
+ load_dotenv()
22
+
23
  # --- Configuration ---
24
  CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
25
  CLIENT_SECRET = "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"
 
32
  GEMINI_DIR = os.path.dirname(os.path.abspath(__file__)) # Same directory as the script
33
  CREDENTIAL_FILE = os.path.join(GEMINI_DIR, "oauth_creds.json")
34
  CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"
35
+ GEMINI_PORT = int(os.getenv("GEMINI_PORT", "8888")) # Default to 8888 if not set
36
+ GEMINI_AUTH_PASSWORD = os.getenv("GEMINI_AUTH_PASSWORD", "123456") # Default password
37
 
38
  # --- Global State ---
39
  credentials = None
40
  user_project_id = None
41
 
42
  app = FastAPI()
43
+ security = HTTPBasic()
44
+
45
+ def authenticate_user(request: Request):
46
+ """Authenticate the user with multiple methods."""
47
+ # Check for API key in query parameters first (for Gemini client compatibility)
48
+ api_key = request.query_params.get("key")
49
+ if api_key and api_key == GEMINI_AUTH_PASSWORD:
50
+ return "api_key_user"
51
+
52
+ # Check for API key in Authorization header (Bearer token format)
53
+ auth_header = request.headers.get("authorization", "")
54
+ if auth_header.startswith("Bearer ") and auth_header[7:] == GEMINI_AUTH_PASSWORD:
55
+ return "bearer_user"
56
+
57
+ # Check for HTTP Basic Authentication
58
+ if auth_header.startswith("Basic "):
59
+ try:
60
+ encoded_credentials = auth_header[6:]
61
+ decoded_credentials = base64.b64decode(encoded_credentials).decode('utf-8')
62
+ username, password = decoded_credentials.split(':', 1)
63
+ if password == GEMINI_AUTH_PASSWORD:
64
+ return username
65
+ except Exception:
66
+ pass
67
+
68
+ # If none of the authentication methods work
69
+ raise HTTPException(
70
+ status_code=401,
71
+ detail="Invalid authentication credentials. Use HTTP Basic Auth, Bearer token, or 'key' query parameter.",
72
+ headers={"WWW-Authenticate": "Basic"},
73
+ )
74
 
75
  # Helper class to adapt a generator of bytes into a file-like object
76
  # that ijson can read from.
 
122
  self.wfile.write(b"<h1>Authentication failed.</h1><p>Please try again.</p>")
123
 
124
  def get_user_project_id(creds):
125
+ """Gets the user's project ID from environment variable, cache, or by probing the API."""
126
  global user_project_id
127
  if user_project_id:
128
  return user_project_id
129
 
130
+ # First, check for environment variable override
131
+ env_project_id = os.getenv("GEMINI_PROJECT_ID")
132
+ if env_project_id:
133
+ user_project_id = env_project_id
134
+ print(f"Using project ID from environment variable: {user_project_id}")
135
+ # Save the environment project ID to cache for consistency
136
+ save_credentials(creds, user_project_id)
137
+ return user_project_id
138
+
139
+ # Second, try to load project ID from credential file
140
  if os.path.exists(CREDENTIAL_FILE):
141
  try:
142
  with open(CREDENTIAL_FILE, "r") as f:
 
149
  except Exception as e:
150
  print(f"Could not load project ID from cache: {e}")
151
 
152
+ # If not found in environment or cache, probe for it
153
+ print("Project ID not found in environment or cache. Probing for user project ID...")
154
  headers = {
155
  "Authorization": f"Bearer {creds.token}",
156
  "Content-Type": "application/json",
 
301
 
302
 
303
  @app.post("/{full_path:path}")
304
+ async def proxy_request(request: Request, full_path: str, username: str = Depends(authenticate_user)):
305
  creds = get_credentials()
306
  if not creds:
307
  return Response(content="Authentication failed. Please restart the proxy to log in.", status_code=500)
308
 
309
+ # Only check if credentials are properly formed and not expired locally
310
+ if not creds.valid:
311
+ if creds.expired and creds.refresh_token:
312
+ print("Credentials expired locally. Refreshing...")
313
+ try:
314
+ creds.refresh(GoogleAuthRequest())
315
+ save_credentials(creds)
316
+ print("Credentials refreshed successfully.")
317
+ except Exception as e:
318
+ print(f"Could not refresh token during request: {e}")
319
+ return Response(content="Token refresh failed. Please restart the proxy to re-authenticate.", status_code=500)
320
+ else:
321
+ print("Credentials are invalid locally and cannot be refreshed.")
322
+ return Response(content="Invalid credentials. Please restart the proxy to re-authenticate.", status_code=500)
323
+
324
  proj_id = get_user_project_id(creds)
325
  if not proj_id:
326
  return Response(content="Failed to get user project ID.", status_code=500)
 
341
  is_streaming = True
342
  else:
343
  target_url = f"{CODE_ASSIST_ENDPOINT}{path}"
344
+
345
+ # Remove authentication query parameters before forwarding to Google API
346
+ query_params = dict(request.query_params)
347
+ # Remove our authentication parameters
348
+ query_params.pop("key", None)
349
+
350
+ # Add remaining query parameters to target URL if any
351
+ if query_params:
352
+ from urllib.parse import urlencode
353
+ target_url += "?" + urlencode(query_params)
354
 
355
  try:
356
  incoming_json = json.loads(post_data)
357
  final_model = model_name_from_url if model_match else incoming_json.get("model")
358
 
359
+ # Set default safety settings to BLOCK_NONE if not specified by user
360
+ safety_settings = incoming_json.get("safetySettings")
361
+ if not safety_settings:
362
+ safety_settings = [
363
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
364
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
365
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
366
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
367
+ {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}
368
+ ]
369
+
370
  structured_payload = {
371
  "model": final_model,
372
  "project": proj_id,
 
376
  "cachedContent": incoming_json.get("cachedContent"),
377
  "tools": incoming_json.get("tools"),
378
  "toolConfig": incoming_json.get("toolConfig"),
379
+ "safetySettings": safety_settings,
380
  "generationConfig": incoming_json.get("generationConfig"),
381
  },
382
  }
 
402
  print(f"[STREAM] Starting streaming request to: {target_url}")
403
  print(f"[STREAM] Request payload size: {len(final_post_data)} bytes")
404
 
405
+ # Make the initial streaming request
406
+ resp = requests.post(target_url, data=final_post_data, headers=headers, stream=True)
407
+ print(f"[STREAM] Response status: {resp.status_code}")
408
+ print(f"[STREAM] Response headers: {dict(resp.headers)}")
409
+
410
+ # If we get a 401, try refreshing the token and retry once
411
+ if resp.status_code == 401 and creds.refresh_token:
412
+ print("[STREAM] Received 401 from Google API. Attempting to refresh token and retry...")
413
+ resp.close() # Close the failed response
414
+ try:
415
+ creds.refresh(GoogleAuthRequest())
416
+ save_credentials(creds)
417
+ print("[STREAM] Token refreshed successfully. Retrying streaming request...")
418
+
419
+ # Update headers with new token and retry
420
+ headers["Authorization"] = f"Bearer {creds.token}"
421
+ resp = requests.post(target_url, data=final_post_data, headers=headers, stream=True)
422
+ print(f"[STREAM] Retry request status: {resp.status_code}")
423
+ except Exception as e:
424
+ print(f"[STREAM] Could not refresh token after 401 error: {e}")
425
+ error_message = json.dumps({"error": {"message": "Token refresh failed after 401 error. Please restart the proxy to re-authenticate."}})
426
+ yield f"data: {error_message}\n\n"
427
+ return
428
+
429
+ with resp:
430
  resp.raise_for_status()
431
 
432
  buffer = ""
 
438
 
439
  print(f"[STREAM] Starting to process chunks...")
440
 
441
+ for chunk in resp.iter_content(chunk_size=1024):
442
+ if isinstance(chunk, bytes):
443
+ chunk = chunk.decode('utf-8', errors='replace')
444
  chunk_count += 1
445
  chunk_size = len(chunk) if chunk else 0
446
  total_bytes += chunk_size
447
 
 
 
 
 
448
  buffer += chunk
 
449
 
450
  # Process complete JSON objects from the buffer
451
  processing_iterations = 0
452
  while buffer:
453
  processing_iterations += 1
454
  if processing_iterations > 100: # Prevent infinite loops
 
455
  break
456
 
457
  buffer = buffer.lstrip()
458
 
459
  if not buffer:
 
460
  break
461
+
 
 
462
  # Handle array start
463
  if buffer.startswith('[') and not in_array:
 
464
  buffer = buffer[1:].lstrip()
465
  in_array = True
466
  continue
467
 
468
  # Handle array end
469
  if buffer.startswith(']'):
 
470
  break
471
 
472
  # Skip commas between objects
473
  if buffer.startswith(','):
 
474
  buffer = buffer[1:].lstrip()
475
  continue
476
 
477
  # Look for complete JSON objects
478
  if buffer.startswith('{'):
 
479
  brace_count = 0
480
  in_string = False
481
  escape_next = False
 
505
  json_str = buffer[:end_pos]
506
  buffer = buffer[end_pos:].lstrip()
507
 
 
508
 
509
  try:
510
  obj = json.loads(json_str)
 
511
 
512
  if "response" in obj:
513
  response_chunk = obj["response"]
514
  objects_yielded += 1
515
  response_json = json.dumps(response_chunk)
 
 
516
  yield f"data: {response_json}\n\n"
 
 
517
  except json.JSONDecodeError as e:
 
 
518
  continue
519
  else:
520
  # Incomplete object, wait for more data
 
521
  break
522
  else:
523
  # Skip unexpected characters
 
524
  buffer = buffer[1:]
525
 
 
 
526
  except requests.exceptions.RequestException as e:
527
  print(f"Error during streaming request: {e}")
528
  error_message = json.dumps({"error": {"message": f"Upstream request failed: {e}"}})
 
534
 
535
  return StreamingResponse(stream_generator(), media_type="text/event-stream")
536
  else:
537
+ # Make the request
538
  resp = requests.post(target_url, data=final_post_data, headers=headers)
539
+
540
+ # If we get a 401, try refreshing the token and retry once
541
+ if resp.status_code == 401 and creds.refresh_token:
542
+ print("Received 401 from Google API. Attempting to refresh token and retry...")
543
+ try:
544
+ creds.refresh(GoogleAuthRequest())
545
+ save_credentials(creds)
546
+ print("Token refreshed successfully. Retrying request...")
547
+
548
+ # Update headers with new token and retry
549
+ headers["Authorization"] = f"Bearer {creds.token}"
550
+ resp = requests.post(target_url, data=final_post_data, headers=headers)
551
+ print(f"Retry request status: {resp.status_code}")
552
+ except Exception as e:
553
+ print(f"Could not refresh token after 401 error: {e}")
554
+ return Response(content="Token refresh failed after 401 error. Please restart the proxy to re-authenticate.", status_code=500)
555
+
556
  if resp.status_code == 200:
557
  try:
558
  google_api_response = resp.json()
559
  # The actual response is nested under the "response" key
 
560
  standard_gemini_response = google_api_response.get("response")
561
  # The standard client expects a list containing the response object
562
  return Response(content=json.dumps([standard_gemini_response]), status_code=200, media_type="application/json")
 
573
  creds = get_credentials()
574
  if creds:
575
  get_user_project_id(creds)
576
+ print(f"\nStarting Gemini proxy server on http://localhost:{GEMINI_PORT}")
577
  print("Send your Gemini API requests to this address.")
578
+ print(f"Authentication required - Password: {GEMINI_AUTH_PASSWORD}")
579
+ print("Use HTTP Basic Authentication with any username and the password above.")
580
+ uvicorn.run(app, host="0.0.0.0", port=GEMINI_PORT)
581
  else:
582
  print("\nCould not obtain credentials. Please authenticate and restart the server.")
requirements.txt CHANGED
@@ -3,4 +3,5 @@ uvicorn
3
  google-auth
4
  google-auth-oauthlib
5
  requests
6
- ijson
 
 
3
  google-auth
4
  google-auth-oauthlib
5
  requests
6
+ ijson
7
+ python-dotenv