bibibi12345 commited on
Commit
9963145
·
1 Parent(s): 684b78c

added image and file upload support for openai

Browse files
run.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import uvicorn
2
+ from src.main import app
3
+
4
+ if __name__ == "__main__":
5
+ uvicorn.run(app, host="0.0.0.0", port=8888)
src/__init__.py ADDED
File without changes
src/auth.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import base64
4
+ import time
5
+ from datetime import datetime
6
+ from fastapi import Request, HTTPException, Depends
7
+ from fastapi.security import HTTPBasic
8
+ from http.server import BaseHTTPRequestHandler, HTTPServer
9
+ from urllib.parse import urlparse, parse_qs
10
+
11
+ from google.oauth2.credentials import Credentials
12
+ from google_auth_oauthlib.flow import Flow
13
+ from google.auth.transport.requests import Request as GoogleAuthRequest
14
+
15
+ from .utils import get_user_agent, get_client_metadata
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
+ ]
25
+ SCRIPT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26
+ CREDENTIAL_FILE = os.path.join(SCRIPT_DIR, "oauth_creds.json")
27
+ CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"
28
+ GEMINI_AUTH_PASSWORD = os.getenv("GEMINI_AUTH_PASSWORD", "123456") # Default password
29
+
30
+ # --- Global State ---
31
+ credentials = None
32
+ user_project_id = None
33
+ onboarding_complete = False
34
+
35
+ security = HTTPBasic()
36
+
37
+ class _OAuthCallbackHandler(BaseHTTPRequestHandler):
38
+ auth_code = None
39
+ def do_GET(self):
40
+ query_components = parse_qs(urlparse(self.path).query)
41
+ code = query_components.get("code", [None])[0]
42
+ if code:
43
+ _OAuthCallbackHandler.auth_code = code
44
+ self.send_response(200)
45
+ self.send_header("Content-type", "text/html")
46
+ self.end_headers()
47
+ self.wfile.write(b"<h1>Authentication successful!</h1><p>You can close this window and restart the proxy.</p>")
48
+ else:
49
+ self.send_response(400)
50
+ self.send_header("Content-type", "text/html")
51
+ self.end_headers()
52
+ self.wfile.write(b"<h1>Authentication failed.</h1><p>Please try again.</p>")
53
+
54
+ def authenticate_user(request: Request):
55
+ """Authenticate the user with multiple methods."""
56
+ # Check for API key in query parameters first (for Gemini client compatibility)
57
+ api_key = request.query_params.get("key")
58
+ if api_key and api_key == GEMINI_AUTH_PASSWORD:
59
+ return "api_key_user"
60
+
61
+ # Check for API key in x-goog-api-key header (Google SDK format)
62
+ goog_api_key = request.headers.get("x-goog-api-key", "")
63
+ if goog_api_key and goog_api_key == GEMINI_AUTH_PASSWORD:
64
+ return "goog_api_key_user"
65
+
66
+ # Check for API key in Authorization header (Bearer token format)
67
+ auth_header = request.headers.get("authorization", "")
68
+ if auth_header.startswith("Bearer "):
69
+ bearer_token = auth_header[7:]
70
+ if bearer_token == GEMINI_AUTH_PASSWORD:
71
+ return "bearer_user"
72
+
73
+ # Check for HTTP Basic Authentication
74
+ if auth_header.startswith("Basic "):
75
+ try:
76
+ encoded_credentials = auth_header[6:]
77
+ decoded_credentials = base64.b64decode(encoded_credentials).decode('utf-8')
78
+ username, password = decoded_credentials.split(':', 1)
79
+ if password == GEMINI_AUTH_PASSWORD:
80
+ return username
81
+ except Exception:
82
+ pass
83
+
84
+ # If none of the authentication methods work
85
+ raise HTTPException(
86
+ status_code=401,
87
+ detail="Invalid authentication credentials. Use HTTP Basic Auth, Bearer token, 'key' query parameter, or 'x-goog-api-key' header.",
88
+ headers={"WWW-Authenticate": "Basic"},
89
+ )
90
+
91
+ def save_credentials(creds, project_id=None):
92
+ print(f"DEBUG: Saving credentials - Token: {creds.token[:20] if creds.token else 'None'}..., Expired: {creds.expired}, Expiry: {creds.expiry}")
93
+
94
+ creds_data = {
95
+ "client_id": CLIENT_ID,
96
+ "client_secret": CLIENT_SECRET,
97
+ "token": creds.token,
98
+ "refresh_token": creds.refresh_token,
99
+ "scopes": creds.scopes if creds.scopes else SCOPES,
100
+ "token_uri": "https://oauth2.googleapis.com/token",
101
+ }
102
+
103
+ if creds.expiry:
104
+ if creds.expiry.tzinfo is None:
105
+ from datetime import timezone
106
+ expiry_utc = creds.expiry.replace(tzinfo=timezone.utc)
107
+ else:
108
+ expiry_utc = creds.expiry
109
+ creds_data["expiry"] = expiry_utc.isoformat()
110
+ print(f"DEBUG: Saving expiry as: {creds_data['expiry']}")
111
+ else:
112
+ print("DEBUG: No expiry time available to save")
113
+
114
+ if project_id:
115
+ creds_data["project_id"] = project_id
116
+ elif os.path.exists(CREDENTIAL_FILE):
117
+ try:
118
+ with open(CREDENTIAL_FILE, "r") as f:
119
+ existing_data = json.load(f)
120
+ if "project_id" in existing_data:
121
+ creds_data["project_id"] = existing_data["project_id"]
122
+ except Exception:
123
+ pass
124
+
125
+ print(f"DEBUG: Final credential data to save: {json.dumps(creds_data, indent=2)}")
126
+
127
+ with open(CREDENTIAL_FILE, "w") as f:
128
+ json.dump(creds_data, f, indent=2)
129
+
130
+ print("DEBUG: Credentials saved to file")
131
+
132
+ def get_credentials():
133
+ """Loads credentials matching gemini-cli OAuth2 flow."""
134
+ global credentials
135
+
136
+ if credentials and credentials.token:
137
+ print("Using valid credentials from memory cache.")
138
+ print(f"DEBUG: Memory credentials - Token: {credentials.token[:20] if credentials.token else 'None'}..., Expired: {credentials.expired}, Expiry: {credentials.expiry}")
139
+ return credentials
140
+ else:
141
+ print("No valid credentials in memory. Loading from disk.")
142
+
143
+ env_creds = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
144
+ if env_creds and os.path.exists(env_creds):
145
+ try:
146
+ with open(env_creds, "r") as f:
147
+ creds_data = json.load(f)
148
+ credentials = Credentials.from_authorized_user_info(creds_data, SCOPES)
149
+ print("Loaded credentials from GOOGLE_APPLICATION_CREDENTIALS.")
150
+ print(f"DEBUG: Env credentials - Token: {credentials.token[:20] if credentials.token else 'None'}..., Expired: {credentials.expired}, Expiry: {credentials.expiry}")
151
+
152
+ if credentials.refresh_token:
153
+ print("Refreshing environment credentials at startup for reliability...")
154
+ try:
155
+ credentials.refresh(GoogleAuthRequest())
156
+ print("Startup token refresh successful for environment credentials.")
157
+ except Exception as refresh_error:
158
+ print(f"Startup token refresh failed for environment credentials: {refresh_error}. Credentials may be stale.")
159
+ else:
160
+ print("No refresh token available in environment credentials - using as-is.")
161
+
162
+ return credentials
163
+ except Exception as e:
164
+ print(f"Could not load credentials from GOOGLE_APPLICATION_CREDENTIALS: {e}")
165
+
166
+ if os.path.exists(CREDENTIAL_FILE):
167
+ try:
168
+ with open(CREDENTIAL_FILE, "r") as f:
169
+ creds_data = json.load(f)
170
+
171
+ print(f"DEBUG: Raw credential data from file: {json.dumps(creds_data, indent=2)}")
172
+
173
+ if "access_token" in creds_data and "token" not in creds_data:
174
+ creds_data["token"] = creds_data["access_token"]
175
+ print("DEBUG: Converted access_token to token field")
176
+
177
+ if "scope" in creds_data and "scopes" not in creds_data:
178
+ creds_data["scopes"] = creds_data["scope"].split()
179
+ print("DEBUG: Converted scope string to scopes list")
180
+
181
+ credentials = Credentials.from_authorized_user_info(creds_data, SCOPES)
182
+ print("Loaded credentials from cache.")
183
+ print(f"DEBUG: Loaded credentials - Token: {credentials.token[:20] if credentials.token else 'None'}..., Expired: {credentials.expired}, Expiry: {credentials.expiry}")
184
+
185
+ if credentials.refresh_token:
186
+ print("Refreshing tokens at startup for reliability...")
187
+ try:
188
+ credentials.refresh(GoogleAuthRequest())
189
+ save_credentials(credentials)
190
+ print("Startup token refresh successful.")
191
+ except Exception as refresh_error:
192
+ print(f"Startup token refresh failed: {refresh_error}. Credentials may be stale.")
193
+ else:
194
+ print("No refresh token available - using cached credentials as-is.")
195
+
196
+ return credentials
197
+ except Exception as e:
198
+ print(f"Could not load cached credentials: {e}. Starting new login.")
199
+
200
+ client_config = {
201
+ "installed": {
202
+ "client_id": CLIENT_ID,
203
+ "client_secret": CLIENT_SECRET,
204
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
205
+ "token_uri": "https://oauth2.googleapis.com/token",
206
+ }
207
+ }
208
+
209
+ flow = Flow.from_client_config(
210
+ client_config,
211
+ scopes=SCOPES,
212
+ redirect_uri="http://localhost:8080"
213
+ )
214
+
215
+ flow.oauth2session.scope = SCOPES
216
+
217
+ auth_url, _ = flow.authorization_url(
218
+ access_type="offline",
219
+ prompt="consent",
220
+ include_granted_scopes='true'
221
+ )
222
+ print(f"\nPlease open this URL in your browser to log in:\n{auth_url}\n")
223
+
224
+ server = HTTPServer(("", 8080), _OAuthCallbackHandler)
225
+ server.handle_request()
226
+
227
+ auth_code = _OAuthCallbackHandler.auth_code
228
+ if not auth_code:
229
+ print("Failed to retrieve authorization code.")
230
+ return None
231
+
232
+ import oauthlib.oauth2.rfc6749.parameters
233
+ original_validate = oauthlib.oauth2.rfc6749.parameters.validate_token_parameters
234
+
235
+ def patched_validate(params):
236
+ try:
237
+ return original_validate(params)
238
+ except Warning:
239
+ pass
240
+
241
+ oauthlib.oauth2.rfc6749.parameters.validate_token_parameters = patched_validate
242
+
243
+ try:
244
+ flow.fetch_token(code=auth_code)
245
+ credentials = flow.credentials
246
+ save_credentials(credentials)
247
+ print("Authentication successful! Credentials saved.")
248
+ return credentials
249
+ except Exception as e:
250
+ print(f"Authentication failed: {e}")
251
+ return None
252
+ finally:
253
+ oauthlib.oauth2.rfc6749.parameters.validate_token_parameters = original_validate
254
+
255
+ def onboard_user(creds, project_id):
256
+ """Ensures the user is onboarded, matching gemini-cli setupUser behavior."""
257
+ global onboarding_complete
258
+ if onboarding_complete:
259
+ return
260
+
261
+ if creds.expired and creds.refresh_token:
262
+ print("Credentials expired. Refreshing before onboarding...")
263
+ try:
264
+ creds.refresh(GoogleAuthRequest())
265
+ save_credentials(creds)
266
+ print("Credentials refreshed successfully.")
267
+ except Exception as e:
268
+ print(f"Could not refresh credentials: {e}")
269
+ raise
270
+
271
+ print("Checking user onboarding status...")
272
+ headers = {
273
+ "Authorization": f"Bearer {creds.token}",
274
+ "Content-Type": "application/json",
275
+ "User-Agent": get_user_agent(),
276
+ }
277
+
278
+ load_assist_payload = {
279
+ "cloudaicompanionProject": project_id,
280
+ "metadata": get_client_metadata(project_id),
281
+ }
282
+
283
+ try:
284
+ import requests
285
+ resp = requests.post(
286
+ f"{CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist",
287
+ data=json.dumps(load_assist_payload),
288
+ headers=headers,
289
+ )
290
+ resp.raise_for_status()
291
+ load_data = resp.json()
292
+
293
+ tier = None
294
+ if load_data.get("currentTier"):
295
+ tier = load_data["currentTier"]
296
+ print("User is already onboarded.")
297
+ else:
298
+ for allowed_tier in load_data.get("allowedTiers", []):
299
+ if allowed_tier.get("isDefault"):
300
+ tier = allowed_tier
301
+ break
302
+
303
+ if not tier:
304
+ tier = {
305
+ "name": "",
306
+ "description": "",
307
+ "id": "legacy-tier",
308
+ "userDefinedCloudaicompanionProject": True,
309
+ }
310
+
311
+ if tier.get("userDefinedCloudaicompanionProject") and not project_id:
312
+ raise ValueError("This account requires setting the GOOGLE_CLOUD_PROJECT env var.")
313
+
314
+ if load_data.get("currentTier"):
315
+ onboarding_complete = True
316
+ return
317
+
318
+ print(f"Onboarding user to tier: {tier.get('name', 'legacy-tier')}")
319
+ onboard_req_payload = {
320
+ "tierId": tier.get("id"),
321
+ "cloudaicompanionProject": project_id,
322
+ "metadata": get_client_metadata(project_id),
323
+ }
324
+
325
+ while True:
326
+ onboard_resp = requests.post(
327
+ f"{CODE_ASSIST_ENDPOINT}/v1internal:onboardUser",
328
+ data=json.dumps(onboard_req_payload),
329
+ headers=headers,
330
+ )
331
+ onboard_resp.raise_for_status()
332
+ lro_data = onboard_resp.json()
333
+
334
+ if lro_data.get("done"):
335
+ print("Onboarding successful.")
336
+ onboarding_complete = True
337
+ break
338
+
339
+ print("Onboarding in progress, waiting 5 seconds...")
340
+ time.sleep(5)
341
+
342
+ except requests.exceptions.HTTPError as e:
343
+ print(f"Error during onboarding: {e.response.text}")
344
+ raise
345
+
346
+ def get_user_project_id(creds):
347
+ """Gets the user's project ID matching gemini-cli setupUser logic."""
348
+ global user_project_id
349
+ if user_project_id:
350
+ return user_project_id
351
+
352
+ env_project_id = os.getenv("GOOGLE_CLOUD_PROJECT")
353
+ if env_project_id:
354
+ user_project_id = env_project_id
355
+ print(f"Using project ID from GOOGLE_CLOUD_PROJECT: {user_project_id}")
356
+ save_credentials(creds, user_project_id)
357
+ return user_project_id
358
+
359
+ gemini_env_project_id = os.getenv("GEMINI_PROJECT_ID")
360
+ if gemini_env_project_id:
361
+ user_project_id = gemini_env_project_id
362
+ print(f"Using project ID from GEMINI_PROJECT_ID: {user_project_id}")
363
+ save_credentials(creds, user_project_id)
364
+ return user_project_id
365
+
366
+ if os.path.exists(CREDENTIAL_FILE):
367
+ try:
368
+ with open(CREDENTIAL_FILE, "r") as f:
369
+ creds_data = json.load(f)
370
+ cached_project_id = creds_data.get("project_id")
371
+ if cached_project_id:
372
+ user_project_id = cached_project_id
373
+ print(f"Loaded project ID from cache: {user_project_id}")
374
+ return user_project_id
375
+ except Exception as e:
376
+ print(f"Could not load project ID from cache: {e}")
377
+
378
+ print("Project ID not found in environment or cache. Probing for user project ID...")
379
+
380
+ if creds.expired and creds.refresh_token:
381
+ print("Credentials expired. Refreshing before project ID probe...")
382
+ try:
383
+ creds.refresh(GoogleAuthRequest())
384
+ save_credentials(creds)
385
+ print("Credentials refreshed successfully.")
386
+ except Exception as e:
387
+ print(f"Could not refresh credentials: {e}")
388
+ raise
389
+
390
+ headers = {
391
+ "Authorization": f"Bearer {creds.token}",
392
+ "Content-Type": "application/json",
393
+ "User-Agent": get_user_agent(),
394
+ }
395
+
396
+ probe_payload = {
397
+ "metadata": get_client_metadata(),
398
+ }
399
+
400
+ try:
401
+ import requests
402
+ resp = requests.post(
403
+ f"{CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist",
404
+ data=json.dumps(probe_payload),
405
+ headers=headers,
406
+ )
407
+ resp.raise_for_status()
408
+ data = resp.json()
409
+ user_project_id = data.get("cloudaicompanionProject")
410
+ if not user_project_id:
411
+ raise ValueError("Could not find 'cloudaicompanionProject' in loadCodeAssist response.")
412
+ print(f"Successfully fetched user project ID: {user_project_id}")
413
+
414
+ save_credentials(creds, user_project_id)
415
+ print("Project ID saved to credential file for future use.")
416
+
417
+ return user_project_id
418
+ except requests.exceptions.HTTPError as e:
419
+ print(f"Error fetching project ID: {e.response.text}")
420
+ raise
src/gemini.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import requests
3
+ from fastapi import APIRouter, Request, Response, Depends
4
+
5
+ from .auth import authenticate_user, get_credentials, get_user_project_id, onboard_user, save_credentials
6
+ from .utils import get_user_agent
7
+ from .gemini_request_builder import build_gemini_request
8
+ from .gemini_response_handler import handle_gemini_response
9
+
10
+ CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"
11
+
12
+ router = APIRouter()
13
+
14
+ @router.get("/v1beta/models")
15
+ async def list_models(request: Request, username: str = Depends(authenticate_user)):
16
+ """List available models - matching gemini-cli supported models exactly."""
17
+ print(f"[GET] {request.url.path} - User: {username}")
18
+ print(f"[MODELS] Serving models list (both /v1/models and /v1beta/models return the same data)")
19
+
20
+ models_response = {
21
+ "models": [
22
+ {
23
+ "name": "models/gemini-1.5-pro",
24
+ "version": "001",
25
+ "displayName": "Gemini 1.5 Pro",
26
+ "description": "Mid-size multimodal model that supports up to 2 million tokens",
27
+ "inputTokenLimit": 2097152,
28
+ "outputTokenLimit": 8192,
29
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
30
+ "temperature": 1.0,
31
+ "maxTemperature": 2.0,
32
+ "topP": 0.95,
33
+ "topK": 64
34
+ },
35
+ {
36
+ "name": "models/gemini-1.5-flash",
37
+ "version": "001",
38
+ "displayName": "Gemini 1.5 Flash",
39
+ "description": "Fast and versatile multimodal model for scaling across diverse tasks",
40
+ "inputTokenLimit": 1048576,
41
+ "outputTokenLimit": 8192,
42
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
43
+ "temperature": 1.0,
44
+ "maxTemperature": 2.0,
45
+ "topP": 0.95,
46
+ "topK": 64
47
+ },
48
+ {
49
+ "name": "models/gemini-2.5-pro-preview-05-06",
50
+ "version": "001",
51
+ "displayName": "Gemini 2.5 Pro Preview 05-06",
52
+ "description": "Preview version of Gemini 2.5 Pro from May 6th",
53
+ "inputTokenLimit": 1048576,
54
+ "outputTokenLimit": 8192,
55
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
56
+ "temperature": 1.0,
57
+ "maxTemperature": 2.0,
58
+ "topP": 0.95,
59
+ "topK": 64
60
+ },
61
+ {
62
+ "name": "models/gemini-2.5-pro-preview-06-05",
63
+ "version": "001",
64
+ "displayName": "Gemini 2.5 Pro Preview 06-05",
65
+ "description": "Preview version of Gemini 2.5 Pro from June 5th",
66
+ "inputTokenLimit": 1048576,
67
+ "outputTokenLimit": 8192,
68
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
69
+ "temperature": 1.0,
70
+ "maxTemperature": 2.0,
71
+ "topP": 0.95,
72
+ "topK": 64
73
+ },
74
+ {
75
+ "name": "models/gemini-2.5-pro",
76
+ "version": "001",
77
+ "displayName": "Gemini 2.5 Pro",
78
+ "description": "Advanced multimodal model with enhanced capabilities",
79
+ "inputTokenLimit": 1048576,
80
+ "outputTokenLimit": 8192,
81
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
82
+ "temperature": 1.0,
83
+ "maxTemperature": 2.0,
84
+ "topP": 0.95,
85
+ "topK": 64
86
+ },
87
+ {
88
+ "name": "models/gemini-2.5-flash-preview-05-20",
89
+ "version": "001",
90
+ "displayName": "Gemini 2.5 Flash Preview 05-20",
91
+ "description": "Preview version of Gemini 2.5 Flash from May 20th",
92
+ "inputTokenLimit": 1048576,
93
+ "outputTokenLimit": 8192,
94
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
95
+ "temperature": 1.0,
96
+ "maxTemperature": 2.0,
97
+ "topP": 0.95,
98
+ "topK": 64
99
+ },
100
+ {
101
+ "name": "models/gemini-2.5-flash",
102
+ "version": "001",
103
+ "displayName": "Gemini 2.5 Flash",
104
+ "description": "Fast and efficient multimodal model with latest improvements",
105
+ "inputTokenLimit": 1048576,
106
+ "outputTokenLimit": 8192,
107
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
108
+ "temperature": 1.0,
109
+ "maxTemperature": 2.0,
110
+ "topP": 0.95,
111
+ "topK": 64
112
+ },
113
+ {
114
+ "name": "models/gemini-2.0-flash",
115
+ "version": "001",
116
+ "displayName": "Gemini 2.0 Flash",
117
+ "description": "Latest generation fast multimodal model",
118
+ "inputTokenLimit": 1048576,
119
+ "outputTokenLimit": 8192,
120
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
121
+ "temperature": 1.0,
122
+ "maxTemperature": 2.0,
123
+ "topP": 0.95,
124
+ "topK": 64
125
+ },
126
+ {
127
+ "name": "models/gemini-2.0-flash-preview-image-generation",
128
+ "version": "001",
129
+ "displayName": "Gemini 2.0 Flash Preview Image Generation",
130
+ "description": "Preview version with image generation capabilities",
131
+ "inputTokenLimit": 32000,
132
+ "outputTokenLimit": 8192,
133
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
134
+ "temperature": 1.0,
135
+ "maxTemperature": 2.0,
136
+ "topP": 0.95,
137
+ "topK": 64
138
+ },
139
+ {
140
+ "name": "models/gemini-embedding-001",
141
+ "version": "001",
142
+ "displayName": "Gemini Embedding 001",
143
+ "description": "Text embedding model for semantic similarity and search",
144
+ "inputTokenLimit": 2048,
145
+ "outputTokenLimit": 1,
146
+ "supportedGenerationMethods": ["embedContent"],
147
+ "temperature": 0.0,
148
+ "maxTemperature": 0.0,
149
+ "topP": 1.0,
150
+ "topK": 1
151
+ }
152
+ ]
153
+ }
154
+
155
+ return Response(content=json.dumps(models_response), status_code=200, media_type="application/json; charset=utf-8")
156
+
157
+ async def proxy_request(post_data: bytes, full_path: str, username: str, method: str, query_params: dict, is_openai: bool = False, is_streaming: bool = False):
158
+ print(f"[{method}] /{full_path} - User: {username}")
159
+
160
+ creds = get_credentials()
161
+ if not creds:
162
+ print("❌ No credentials available")
163
+ return Response(content="Authentication failed. Please restart the proxy to log in.", status_code=500)
164
+
165
+ print(f"Using credentials - Token: {creds.token[:20] if creds.token else 'None'}..., Expired: {creds.expired}")
166
+
167
+ if creds.expired and creds.refresh_token:
168
+ print("Credentials expired. Refreshing...")
169
+ try:
170
+ from google.auth.transport.requests import Request as GoogleAuthRequest
171
+ creds.refresh(GoogleAuthRequest())
172
+ save_credentials(creds)
173
+ print("Credentials refreshed successfully.")
174
+ except Exception as e:
175
+ print(f"Could not refresh token during request: {e}")
176
+ return Response(content="Token refresh failed. Please restart the proxy to re-authenticate.", status_code=500)
177
+ elif not creds.token:
178
+ print("No access token available.")
179
+ return Response(content="No access token. Please restart the proxy to re-authenticate.", status_code=500)
180
+
181
+ proj_id = get_user_project_id(creds)
182
+ if not proj_id:
183
+ return Response(content="Failed to get user project ID.", status_code=500)
184
+
185
+ onboard_user(creds, proj_id)
186
+
187
+ if is_openai:
188
+ target_url, final_post_data, request_headers, _ = build_gemini_request(post_data, full_path, creds, is_streaming)
189
+ else:
190
+ action = "streamGenerateContent" if is_streaming else "generateContent"
191
+ target_url = f"{CODE_ASSIST_ENDPOINT}/v1internal:{action}" + "?alt=sse"
192
+
193
+ try:
194
+ incoming_json = json.loads(post_data)
195
+ except (json.JSONDecodeError, AttributeError):
196
+ incoming_json = {}
197
+
198
+ final_post_data = json.dumps({
199
+ "model": full_path.split('/')[2].split(':')[0],
200
+ "project": proj_id,
201
+ "request": incoming_json,
202
+ })
203
+
204
+ request_headers = {
205
+ "Authorization": f"Bearer {creds.token}",
206
+ "Content-Type": "application/json",
207
+ "User-Agent": get_user_agent(),
208
+ }
209
+
210
+
211
+ if is_streaming:
212
+ print(f"STREAMING REQUEST to: {target_url}")
213
+ print(f"STREAMING REQUEST PAYLOAD: {final_post_data}")
214
+ resp = requests.post(target_url, data=final_post_data, headers=request_headers, stream=True)
215
+ print(f"STREAMING RESPONSE: {resp.status_code}")
216
+ return handle_gemini_response(resp, is_streaming=True)
217
+ else:
218
+ print(f"REQUEST to: {target_url}")
219
+ print(f"REQUEST PAYLOAD: {final_post_data}")
220
+ resp = requests.post(target_url, data=final_post_data, headers=request_headers)
221
+ print(f"RESPONSE: {resp.status_code}, {resp.text}")
222
+ return handle_gemini_response(resp, is_streaming=False)
223
+
224
+ @router.api_route("/{full_path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
225
+ async def proxy(request: Request, full_path: str, username: str = Depends(authenticate_user)):
226
+ post_data = await request.body()
227
+ is_streaming = "stream" in full_path
228
+ return await proxy_request(post_data, full_path, username, request.method, dict(request.query_params), is_streaming=is_streaming)
src/gemini_request_builder.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+
4
+ from .auth import get_user_project_id
5
+ from .utils import get_user_agent
6
+
7
+ CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"
8
+
9
+ def build_gemini_request(post_data: bytes, full_path: str, creds, is_streaming: bool = False):
10
+ try:
11
+ incoming_json = json.loads(post_data)
12
+ except (json.JSONDecodeError, AttributeError):
13
+ incoming_json = {}
14
+
15
+ # Set the action based on streaming
16
+ action = "streamGenerateContent" if is_streaming else "generateContent"
17
+
18
+ # The target URL is always one of two values
19
+ target_url = f"{CODE_ASSIST_ENDPOINT}/v1internal:{action}"
20
+
21
+ if is_streaming:
22
+ target_url += "?alt=sse"
23
+
24
+ # Extract model from the incoming JSON payload
25
+ final_model = incoming_json.get("model")
26
+
27
+ # Default safety settings if not provided
28
+ safety_settings = incoming_json.get("safetySettings")
29
+ if not safety_settings:
30
+ safety_settings = [
31
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
32
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
33
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
34
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
35
+ {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}
36
+ ]
37
+
38
+ # Build the final payload for the Google API
39
+ structured_payload = {
40
+ "model": final_model,
41
+ "project": get_user_project_id(creds),
42
+ "request": {
43
+ "contents": incoming_json.get("contents"),
44
+ "systemInstruction": incoming_json.get("systemInstruction"),
45
+ "cachedContent": incoming_json.get("cachedContent"),
46
+ "tools": incoming_json.get("tools"),
47
+ "toolConfig": incoming_json.get("toolConfig"),
48
+ "safetySettings": safety_settings,
49
+ "generationConfig": incoming_json.get("generationConfig", {}),
50
+ },
51
+ }
52
+ # Remove any keys with None values from the request
53
+ structured_payload["request"] = {
54
+ k: v
55
+ for k, v in structured_payload["request"].items()
56
+ if v is not None
57
+ }
58
+
59
+ final_post_data = json.dumps(structured_payload)
60
+
61
+ # Build the request headers
62
+ request_headers = {
63
+ "Authorization": f"Bearer {creds.token}",
64
+ "Content-Type": "application/json",
65
+ "User-Agent": get_user_agent(),
66
+ }
67
+
68
+ return target_url, final_post_data, request_headers, is_streaming
src/gemini_response_handler.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import requests
3
+ from fastapi import Response
4
+ from fastapi.responses import StreamingResponse
5
+ import asyncio
6
+
7
+ def handle_gemini_response(resp, is_streaming):
8
+ if is_streaming:
9
+ async def stream_generator():
10
+ try:
11
+ with resp:
12
+ resp.raise_for_status()
13
+
14
+ print("[STREAM] Processing with Gemini SDK-compatible logic")
15
+
16
+ for chunk in resp.iter_lines():
17
+ if chunk:
18
+ if not isinstance(chunk, str):
19
+ chunk = chunk.decode('utf-8')
20
+
21
+ print(chunk)
22
+
23
+ if chunk.startswith('data: '):
24
+ chunk = chunk[len('data: '):]
25
+
26
+ try:
27
+ obj = json.loads(chunk)
28
+
29
+ if "response" in obj:
30
+ response_chunk = obj["response"]
31
+ response_json = json.dumps(response_chunk, separators=(',', ':'))
32
+ response_line = f"data: {response_json}\n\n"
33
+ yield response_line
34
+ await asyncio.sleep(0)
35
+ except json.JSONDecodeError:
36
+ continue
37
+
38
+ except requests.exceptions.RequestException as e:
39
+ print(f"Error during streaming request: {e}")
40
+ yield f'data: {{"error": {{"message": "Upstream request failed: {str(e)}"}}}}\n\n'.encode('utf-8')
41
+ except Exception as e:
42
+ print(f"An unexpected error occurred during streaming: {e}")
43
+ yield f'data: {{"error": {{"message": "An unexpected error occurred: {str(e)}"}}}}\n\n'.encode('utf-8')
44
+
45
+ response_headers = {
46
+ "Content-Type": "text/event-stream",
47
+ "Content-Disposition": "attachment",
48
+ "Vary": "Origin, X-Origin, Referer",
49
+ "X-XSS-Protection": "0",
50
+ "X-Frame-Options": "SAMEORIGIN",
51
+ "X-Content-Type-Options": "nosniff",
52
+ "Server": "ESF"
53
+ }
54
+
55
+ return StreamingResponse(
56
+ stream_generator(),
57
+ media_type="text/event-stream",
58
+ headers=response_headers
59
+ )
60
+ else:
61
+ if resp.status_code == 200:
62
+ try:
63
+ google_api_response = resp.text
64
+ if google_api_response.startswith('data: '):
65
+ google_api_response = google_api_response[len('data: '):]
66
+ google_api_response = json.loads(google_api_response)
67
+ standard_gemini_response = google_api_response.get("response")
68
+ return Response(content=json.dumps(standard_gemini_response), status_code=200, media_type="application/json; charset=utf-8")
69
+ except (json.JSONDecodeError, AttributeError) as e:
70
+ print(f"Error converting to standard Gemini format: {e}")
71
+ return Response(content=resp.content, status_code=resp.status_code, media_type=resp.headers.get("Content-Type"))
72
+ else:
73
+ return Response(content=resp.content, status_code=resp.status_code, media_type=resp.headers.get("Content-Type"))
src/main.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, Response
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from .gemini import router as gemini_router
4
+ from .openai import router as openai_router
5
+ from .auth import get_credentials, get_user_project_id, onboard_user
6
+
7
+ app = FastAPI()
8
+
9
+ # Add CORS middleware for preflight requests
10
+ app.add_middleware(
11
+ CORSMiddleware,
12
+ allow_origins=["*"], # Allow all origins
13
+ allow_credentials=True,
14
+ allow_methods=["*"], # Allow all methods
15
+ allow_headers=["*"], # Allow all headers
16
+ )
17
+
18
+ @app.on_event("startup")
19
+ async def startup_event():
20
+ print("Initializing credentials...")
21
+ creds = get_credentials()
22
+ if creds:
23
+ proj_id = get_user_project_id(creds)
24
+ if proj_id:
25
+ onboard_user(creds, proj_id)
26
+ print(f"\nStarting Gemini proxy server")
27
+ print("Send your Gemini API requests to this address.")
28
+ print(f"Authentication required - Password: see .env file")
29
+ print("Use HTTP Basic Authentication with any username and the password above.")
30
+ else:
31
+ print("\nCould not obtain credentials. Please authenticate and restart the server.")
32
+
33
+ @app.options("/{full_path:path}")
34
+ async def handle_preflight(request: Request, full_path: str):
35
+ """Handle CORS preflight requests without authentication."""
36
+ return Response(
37
+ status_code=200,
38
+ headers={
39
+ "Access-Control-Allow-Origin": "*",
40
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
41
+ "Access-Control-Allow-Headers": "*",
42
+ "Access-Control-Allow-Credentials": "true",
43
+ }
44
+ )
45
+
46
+ app.include_router(openai_router)
47
+ app.include_router(gemini_router)
src/models.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional, Union, Dict, Any
3
+
4
+ # OpenAI Models
5
+ class OpenAIChatMessage(BaseModel):
6
+ role: str
7
+ content: Union[str, List[Dict[str, Any]]]
8
+
9
+ class OpenAIChatCompletionRequest(BaseModel):
10
+ model: str
11
+ messages: List[OpenAIChatMessage]
12
+ stream: bool = False
13
+ temperature: Optional[float] = None
14
+ top_p: Optional[float] = None
15
+ max_tokens: Optional[int] = None
16
+
17
+ class OpenAIChatCompletionChoice(BaseModel):
18
+ index: int
19
+ message: OpenAIChatMessage
20
+ finish_reason: Optional[str] = None
21
+
22
+ class OpenAIChatCompletionResponse(BaseModel):
23
+ id: str
24
+ object: str
25
+ created: int
26
+ model: str
27
+ choices: List[OpenAIChatCompletionChoice]
28
+
29
+ class OpenAIDelta(BaseModel):
30
+ content: Optional[str] = None
31
+
32
+ class OpenAIChatCompletionStreamChoice(BaseModel):
33
+ index: int
34
+ delta: OpenAIDelta
35
+ finish_reason: Optional[str] = None
36
+
37
+ class OpenAIChatCompletionStreamResponse(BaseModel):
38
+ id: str
39
+ object: str
40
+ created: int
41
+ model: str
42
+ choices: List[OpenAIChatCompletionStreamChoice]
43
+
44
+ # Gemini Models
45
+ class GeminiPart(BaseModel):
46
+ text: str
47
+
48
+ class GeminiContent(BaseModel):
49
+ role: str
50
+ parts: List[GeminiPart]
51
+
52
+ class GeminiRequest(BaseModel):
53
+ contents: List[GeminiContent]
54
+
55
+ class GeminiCandidate(BaseModel):
56
+ content: GeminiContent
57
+ finish_reason: Optional[str] = None
58
+ index: int
59
+
60
+ class GeminiResponse(BaseModel):
61
+ candidates: List[GeminiCandidate]
src/openai.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import time
3
+ import uuid
4
+ from fastapi import APIRouter, Request, Response, Depends
5
+ from fastapi.responses import StreamingResponse
6
+
7
+ from .auth import authenticate_user
8
+ from .models import OpenAIChatCompletionRequest, OpenAIChatCompletionResponse, OpenAIChatCompletionStreamResponse, OpenAIChatMessage, OpenAIChatCompletionChoice, OpenAIChatCompletionStreamChoice, OpenAIDelta, GeminiRequest, GeminiContent, GeminiPart, GeminiResponse
9
+ from .gemini import proxy_request
10
+
11
+ import asyncio
12
+
13
+ router = APIRouter()
14
+
15
+ def openai_to_gemini(openai_request: OpenAIChatCompletionRequest) -> dict:
16
+ contents = []
17
+ for message in openai_request.messages:
18
+ role = message.role
19
+ if role == "assistant":
20
+ role = "model"
21
+ if role == "system":
22
+ role = "user"
23
+ if isinstance(message.content, list):
24
+ parts = []
25
+ for part in message.content:
26
+ if part.get("type") == "text":
27
+ parts.append({"text": part.get("text", "")})
28
+ elif part.get("type") == "image_url":
29
+ image_url = part.get("image_url", {}).get("url")
30
+ if image_url:
31
+ # Assuming the image_url is a base64 encoded string
32
+ # "data:image/jpeg;base64,{base64_image}"
33
+ mime_type, base64_data = image_url.split(";")
34
+ _, mime_type = mime_type.split(":")
35
+ _, base64_data = base64_data.split(",")
36
+ parts.append({
37
+ "inlineData": {
38
+ "mimeType": mime_type,
39
+ "data": base64_data
40
+ }
41
+ })
42
+ contents.append({"role": role, "parts": parts})
43
+ else:
44
+ contents.append({"role": role, "parts": [{"text": message.content}]})
45
+
46
+ generation_config = {}
47
+ if openai_request.temperature is not None:
48
+ generation_config["temperature"] = openai_request.temperature
49
+ if openai_request.top_p is not None:
50
+ generation_config["topP"] = openai_request.top_p
51
+ if openai_request.max_tokens is not None:
52
+ generation_config["maxOutputTokens"] = openai_request.max_tokens
53
+
54
+ safety_settings = [
55
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
56
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
57
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
58
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
59
+ {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}
60
+ ]
61
+
62
+ return {
63
+ "contents": contents,
64
+ "generationConfig": generation_config,
65
+ "safetySettings": safety_settings,
66
+ "model": openai_request.model
67
+ }
68
+
69
+ def gemini_to_openai(gemini_response: dict, model: str) -> OpenAIChatCompletionResponse:
70
+ choices = []
71
+ for candidate in gemini_response.get("candidates", []):
72
+ role = candidate.get("content", {}).get("role", "assistant")
73
+ if role == "model":
74
+ role = "assistant"
75
+ choices.append(
76
+ {
77
+ "index": candidate.get("index"),
78
+ "message": {
79
+ "role": role,
80
+ "content": candidate.get("content", {}).get("parts", [{}])[0].get("text"),
81
+ },
82
+ "finish_reason": map_finish_reason(candidate.get("finishReason")),
83
+ }
84
+ )
85
+ return {
86
+ "id": str(uuid.uuid4()),
87
+ "object": "chat.completion",
88
+ "created": int(time.time()),
89
+ "model": model,
90
+ "choices": choices,
91
+ }
92
+
93
+ def gemini_to_openai_stream(gemini_response: dict, model: str, response_id: str) -> dict:
94
+ choices = []
95
+ for candidate in gemini_response.get("candidates", []):
96
+ role = candidate.get("content", {}).get("role", "assistant")
97
+ if role == "model":
98
+ role = "assistant"
99
+ choices.append(
100
+ {
101
+ "index": candidate.get("index"),
102
+ "delta": {
103
+ "content": candidate.get("content", {}).get("parts", [{}])[0].get("text"),
104
+ },
105
+ "finish_reason": map_finish_reason(candidate.get("finishReason")),
106
+ }
107
+ )
108
+ return {
109
+ "id": response_id,
110
+ "object": "chat.completion.chunk",
111
+ "created": int(time.time()),
112
+ "model": model,
113
+ "choices": choices,
114
+ }
115
+
116
+ def map_finish_reason(reason: str) -> str:
117
+ if reason == "STOP":
118
+ return "stop"
119
+ elif reason == "MAX_TOKENS":
120
+ return "length"
121
+ elif reason in ["SAFETY", "RECITATION"]:
122
+ return "content_filter"
123
+ else:
124
+ return None
125
+
126
+ @router.post("/v1/chat/completions")
127
+ async def chat_completions(request: OpenAIChatCompletionRequest, http_request: Request, username: str = Depends(authenticate_user)):
128
+ gemini_request = openai_to_gemini(request)
129
+
130
+ if request.stream:
131
+ async def stream_generator():
132
+ response = await proxy_request(json.dumps(gemini_request).encode('utf-8'), http_request.url.path, username, "POST", dict(http_request.query_params), is_openai=True, is_streaming=True)
133
+ if isinstance(response, StreamingResponse):
134
+ response_id = "chatcmpl-realstream-" + str(uuid.uuid4())
135
+ async for chunk in response.body_iterator:
136
+ if chunk.startswith('data: '):
137
+ try:
138
+ data = json.loads(chunk[6:])
139
+ openai_response = gemini_to_openai_stream(data, request.model, response_id)
140
+ yield f"data: {json.dumps(openai_response)}\n\n"
141
+ await asyncio.sleep(0)
142
+ except (json.JSONDecodeError, KeyError):
143
+ continue
144
+ yield "data: [DONE]\n\n"
145
+ else:
146
+ yield f"data: {response.body.decode()}\n\n"
147
+ yield "data: [DONE]\n\n"
148
+
149
+ return StreamingResponse(stream_generator(), media_type="text/event-stream")
150
+ else:
151
+ response = await proxy_request(json.dumps(gemini_request).encode('utf-8'), http_request.url.path, username, "POST", dict(http_request.query_params), is_openai=True, is_streaming=False)
152
+ if isinstance(response, Response) and response.status_code != 200:
153
+ return response
154
+ gemini_response = json.loads(response.body)
155
+ openai_response = gemini_to_openai(gemini_response, request.model)
156
+ return openai_response
157
+
158
+
159
+ async def event_generator():
160
+ """
161
+ A generator function that yields a message in the Server-Sent Event (SSE)
162
+ format every second, five times.
163
+ """
164
+ count = 0
165
+ while count < 5:
166
+ # SSE format is "data: <content>\n\n"
167
+ # The two newlines are crucial as they mark the end of an event.
168
+ yield "data: 1\n\n"
169
+
170
+ # Log to the server console to see it working on the backend
171
+ count += 1
172
+ print(f"Sent chunk {count}/5")
173
+
174
+ # Wait for 1 second
175
+ await asyncio.sleep(1)
176
+
177
+ @router.post("/v1/test")
178
+ async def stream_data(request: OpenAIChatCompletionRequest, http_request: Request, username: str = Depends(authenticate_user)):
179
+ """
180
+ This endpoint returns a streaming response.
181
+ It uses the event_generator to send data chunks.
182
+ The media_type is 'text/event-stream' which is standard for SSE.
183
+ """
184
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
src/utils.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import platform
2
+
3
+ CLI_VERSION = "0.1.5" # Match current gemini-cli version
4
+
5
+ def get_user_agent():
6
+ """Generate User-Agent string matching gemini-cli format."""
7
+ version = CLI_VERSION
8
+ system = platform.system()
9
+ arch = platform.machine()
10
+ return f"GeminiCLI/{version} ({system}; {arch})"
11
+
12
+ def get_platform_string():
13
+ """Generate platform string matching gemini-cli format."""
14
+ system = platform.system().upper()
15
+ arch = platform.machine().upper()
16
+
17
+ # Map to gemini-cli platform format
18
+ if system == "DARWIN":
19
+ if arch in ["ARM64", "AARCH64"]:
20
+ return "DARWIN_ARM64"
21
+ else:
22
+ return "DARWIN_AMD64"
23
+ elif system == "LINUX":
24
+ if arch in ["ARM64", "AARCH64"]:
25
+ return "LINUX_ARM64"
26
+ else:
27
+ return "LINUX_AMD64"
28
+ elif system == "WINDOWS":
29
+ return "WINDOWS_AMD64"
30
+ else:
31
+ return "PLATFORM_UNSPECIFIED"
32
+
33
+ def get_client_metadata(project_id=None):
34
+ return {
35
+ "ideType": "IDE_UNSPECIFIED",
36
+ "platform": get_platform_string(),
37
+ "pluginType": "GEMINI",
38
+ "duetProject": project_id,
39
+ }