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

major refactor

Browse files
src/auth.py CHANGED
@@ -13,19 +13,10 @@ 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
@@ -44,7 +35,7 @@ class _OAuthCallbackHandler(BaseHTTPRequestHandler):
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")
@@ -89,7 +80,6 @@ def authenticate_user(request: Request):
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,
@@ -107,9 +97,6 @@ def save_credentials(creds, project_id=None):
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
@@ -122,23 +109,17 @@ def save_credentials(creds, project_id=None):
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):
@@ -146,56 +127,41 @@ def get_credentials():
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": {
@@ -226,7 +192,6 @@ def get_credentials():
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
@@ -259,16 +224,11 @@ def onboard_user(creds, project_id):
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",
@@ -293,7 +253,6 @@ def onboard_user(creds, project_id):
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"):
@@ -315,7 +274,6 @@ def onboard_user(creds, project_id):
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,
@@ -332,16 +290,15 @@ def onboard_user(creds, project_id):
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."""
@@ -352,14 +309,12 @@ def get_user_project_id(creds):
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
 
@@ -370,22 +325,16 @@ def get_user_project_id(creds):
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}",
@@ -409,12 +358,9 @@ def get_user_project_id(creds):
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
 
13
  from google.auth.transport.requests import Request as GoogleAuthRequest
14
 
15
  from .utils import get_user_agent, get_client_metadata
16
+ from .config import (
17
+ CLIENT_ID, CLIENT_SECRET, SCOPES, CREDENTIAL_FILE,
18
+ CODE_ASSIST_ENDPOINT, GEMINI_AUTH_PASSWORD
19
+ )
 
 
 
 
 
 
 
 
 
20
 
21
  # --- Global State ---
22
  credentials = None
 
35
  self.send_response(200)
36
  self.send_header("Content-type", "text/html")
37
  self.end_headers()
38
+ self.wfile.write(b"<h1>OAuth authentication successful!</h1><p>You can close this window. Please check the proxy server logs to verify that onboarding completed successfully. No need to restart the proxy.</p>")
39
  else:
40
  self.send_response(400)
41
  self.send_header("Content-type", "text/html")
 
80
  )
81
 
82
  def save_credentials(creds, project_id=None):
 
83
 
84
  creds_data = {
85
  "client_id": CLIENT_ID,
 
97
  else:
98
  expiry_utc = creds.expiry
99
  creds_data["expiry"] = expiry_utc.isoformat()
 
 
 
100
 
101
  if project_id:
102
  creds_data["project_id"] = project_id
 
109
  except Exception:
110
  pass
111
 
 
112
 
113
  with open(CREDENTIAL_FILE, "w") as f:
114
  json.dump(creds_data, f, indent=2)
115
 
 
116
 
117
  def get_credentials():
118
  """Loads credentials matching gemini-cli OAuth2 flow."""
119
  global credentials
120
 
121
  if credentials and credentials.token:
 
 
122
  return credentials
 
 
123
 
124
  env_creds = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
125
  if env_creds and os.path.exists(env_creds):
 
127
  with open(env_creds, "r") as f:
128
  creds_data = json.load(f)
129
  credentials = Credentials.from_authorized_user_info(creds_data, SCOPES)
130
+
 
 
131
  if credentials.refresh_token:
 
132
  try:
133
  credentials.refresh(GoogleAuthRequest())
 
134
  except Exception as refresh_error:
135
+ pass # Use credentials as-is if refresh fails
 
 
136
 
137
  return credentials
138
  except Exception as e:
139
+ pass # Fall through to file-based credentials
140
 
141
  if os.path.exists(CREDENTIAL_FILE):
142
  try:
143
  with open(CREDENTIAL_FILE, "r") as f:
144
  creds_data = json.load(f)
145
 
 
146
 
147
  if "access_token" in creds_data and "token" not in creds_data:
148
  creds_data["token"] = creds_data["access_token"]
 
149
 
150
  if "scope" in creds_data and "scopes" not in creds_data:
151
  creds_data["scopes"] = creds_data["scope"].split()
 
152
 
153
  credentials = Credentials.from_authorized_user_info(creds_data, SCOPES)
154
+
 
 
155
  if credentials.refresh_token:
 
156
  try:
157
  credentials.refresh(GoogleAuthRequest())
158
  save_credentials(credentials)
 
159
  except Exception as refresh_error:
160
+ pass # Use credentials as-is if refresh fails
 
 
161
 
162
  return credentials
163
  except Exception as e:
164
+ pass # Fall through to new login
165
 
166
  client_config = {
167
  "installed": {
 
192
 
193
  auth_code = _OAuthCallbackHandler.auth_code
194
  if not auth_code:
 
195
  return None
196
 
197
  import oauthlib.oauth2.rfc6749.parameters
 
224
  return
225
 
226
  if creds.expired and creds.refresh_token:
 
227
  try:
228
  creds.refresh(GoogleAuthRequest())
229
  save_credentials(creds)
 
230
  except Exception as e:
231
+ raise Exception(f"Failed to refresh credentials during onboarding: {str(e)}")
 
 
 
232
  headers = {
233
  "Authorization": f"Bearer {creds.token}",
234
  "Content-Type": "application/json",
 
253
  tier = None
254
  if load_data.get("currentTier"):
255
  tier = load_data["currentTier"]
 
256
  else:
257
  for allowed_tier in load_data.get("allowedTiers", []):
258
  if allowed_tier.get("isDefault"):
 
274
  onboarding_complete = True
275
  return
276
 
 
277
  onboard_req_payload = {
278
  "tierId": tier.get("id"),
279
  "cloudaicompanionProject": project_id,
 
290
  lro_data = onboard_resp.json()
291
 
292
  if lro_data.get("done"):
 
293
  onboarding_complete = True
294
  break
295
 
 
296
  time.sleep(5)
297
 
298
  except requests.exceptions.HTTPError as e:
299
+ raise Exception(f"User onboarding failed. Please check your Google Cloud project permissions and try again. Error: {e.response.text if hasattr(e, 'response') else str(e)}")
300
+ except Exception as e:
301
+ raise Exception(f"User onboarding failed due to an unexpected error: {str(e)}")
302
 
303
  def get_user_project_id(creds):
304
  """Gets the user's project ID matching gemini-cli setupUser logic."""
 
309
  env_project_id = os.getenv("GOOGLE_CLOUD_PROJECT")
310
  if env_project_id:
311
  user_project_id = env_project_id
 
312
  save_credentials(creds, user_project_id)
313
  return user_project_id
314
 
315
  gemini_env_project_id = os.getenv("GEMINI_PROJECT_ID")
316
  if gemini_env_project_id:
317
  user_project_id = gemini_env_project_id
 
318
  save_credentials(creds, user_project_id)
319
  return user_project_id
320
 
 
325
  cached_project_id = creds_data.get("project_id")
326
  if cached_project_id:
327
  user_project_id = cached_project_id
 
328
  return user_project_id
329
  except Exception as e:
330
+ pass
331
 
 
 
332
  if creds.expired and creds.refresh_token:
 
333
  try:
334
  creds.refresh(GoogleAuthRequest())
335
  save_credentials(creds)
 
336
  except Exception as e:
337
+ raise Exception(f"Failed to refresh credentials while getting project ID: {str(e)}")
 
338
 
339
  headers = {
340
  "Authorization": f"Bearer {creds.token}",
 
358
  user_project_id = data.get("cloudaicompanionProject")
359
  if not user_project_id:
360
  raise ValueError("Could not find 'cloudaicompanionProject' in loadCodeAssist response.")
361
+
 
362
  save_credentials(creds, user_project_id)
 
363
 
364
  return user_project_id
365
  except requests.exceptions.HTTPError as e:
 
366
  raise
src/config.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration constants for the Geminicli2api proxy server.
3
+ Centralizes all configuration to avoid duplication across modules.
4
+ """
5
+ import os
6
+
7
+ # API Endpoints
8
+ CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"
9
+
10
+ # Client Configuration
11
+ CLI_VERSION = "0.1.5" # Match current gemini-cli version
12
+
13
+ # OAuth Configuration
14
+ CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
15
+ CLIENT_SECRET = "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"
16
+ SCOPES = [
17
+ "https://www.googleapis.com/auth/cloud-platform",
18
+ "https://www.googleapis.com/auth/userinfo.email",
19
+ "https://www.googleapis.com/auth/userinfo.profile",
20
+ ]
21
+
22
+ # File Paths
23
+ SCRIPT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
24
+ CREDENTIAL_FILE = os.path.join(SCRIPT_DIR, "oauth_creds.json")
25
+
26
+ # Authentication
27
+ GEMINI_AUTH_PASSWORD = os.getenv("GEMINI_AUTH_PASSWORD", "123456")
28
+
29
+ # Default Safety Settings for Google API
30
+ DEFAULT_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
+ # Supported Models (for /v1beta/models endpoint)
39
+ SUPPORTED_MODELS = [
40
+ {
41
+ "name": "models/gemini-1.5-pro",
42
+ "version": "001",
43
+ "displayName": "Gemini 1.5 Pro",
44
+ "description": "Mid-size multimodal model that supports up to 2 million tokens",
45
+ "inputTokenLimit": 2097152,
46
+ "outputTokenLimit": 8192,
47
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
48
+ "temperature": 1.0,
49
+ "maxTemperature": 2.0,
50
+ "topP": 0.95,
51
+ "topK": 64
52
+ },
53
+ {
54
+ "name": "models/gemini-1.5-flash",
55
+ "version": "001",
56
+ "displayName": "Gemini 1.5 Flash",
57
+ "description": "Fast and versatile multimodal model for scaling across diverse tasks",
58
+ "inputTokenLimit": 1048576,
59
+ "outputTokenLimit": 8192,
60
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
61
+ "temperature": 1.0,
62
+ "maxTemperature": 2.0,
63
+ "topP": 0.95,
64
+ "topK": 64
65
+ },
66
+ {
67
+ "name": "models/gemini-2.5-pro-preview-05-06",
68
+ "version": "001",
69
+ "displayName": "Gemini 2.5 Pro Preview 05-06",
70
+ "description": "Preview version of Gemini 2.5 Pro from May 6th",
71
+ "inputTokenLimit": 1048576,
72
+ "outputTokenLimit": 8192,
73
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
74
+ "temperature": 1.0,
75
+ "maxTemperature": 2.0,
76
+ "topP": 0.95,
77
+ "topK": 64
78
+ },
79
+ {
80
+ "name": "models/gemini-2.5-pro-preview-06-05",
81
+ "version": "001",
82
+ "displayName": "Gemini 2.5 Pro Preview 06-05",
83
+ "description": "Preview version of Gemini 2.5 Pro from June 5th",
84
+ "inputTokenLimit": 1048576,
85
+ "outputTokenLimit": 8192,
86
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
87
+ "temperature": 1.0,
88
+ "maxTemperature": 2.0,
89
+ "topP": 0.95,
90
+ "topK": 64
91
+ },
92
+ {
93
+ "name": "models/gemini-2.5-pro",
94
+ "version": "001",
95
+ "displayName": "Gemini 2.5 Pro",
96
+ "description": "Advanced multimodal model with enhanced capabilities",
97
+ "inputTokenLimit": 1048576,
98
+ "outputTokenLimit": 8192,
99
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
100
+ "temperature": 1.0,
101
+ "maxTemperature": 2.0,
102
+ "topP": 0.95,
103
+ "topK": 64
104
+ },
105
+ {
106
+ "name": "models/gemini-2.5-flash-preview-05-20",
107
+ "version": "001",
108
+ "displayName": "Gemini 2.5 Flash Preview 05-20",
109
+ "description": "Preview version of Gemini 2.5 Flash from May 20th",
110
+ "inputTokenLimit": 1048576,
111
+ "outputTokenLimit": 8192,
112
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
113
+ "temperature": 1.0,
114
+ "maxTemperature": 2.0,
115
+ "topP": 0.95,
116
+ "topK": 64
117
+ },
118
+ {
119
+ "name": "models/gemini-2.5-flash",
120
+ "version": "001",
121
+ "displayName": "Gemini 2.5 Flash",
122
+ "description": "Fast and efficient multimodal model with latest improvements",
123
+ "inputTokenLimit": 1048576,
124
+ "outputTokenLimit": 8192,
125
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
126
+ "temperature": 1.0,
127
+ "maxTemperature": 2.0,
128
+ "topP": 0.95,
129
+ "topK": 64
130
+ },
131
+ {
132
+ "name": "models/gemini-2.0-flash",
133
+ "version": "001",
134
+ "displayName": "Gemini 2.0 Flash",
135
+ "description": "Latest generation fast multimodal model",
136
+ "inputTokenLimit": 1048576,
137
+ "outputTokenLimit": 8192,
138
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
139
+ "temperature": 1.0,
140
+ "maxTemperature": 2.0,
141
+ "topP": 0.95,
142
+ "topK": 64
143
+ },
144
+ {
145
+ "name": "models/gemini-2.0-flash-preview-image-generation",
146
+ "version": "001",
147
+ "displayName": "Gemini 2.0 Flash Preview Image Generation",
148
+ "description": "Preview version with image generation capabilities",
149
+ "inputTokenLimit": 32000,
150
+ "outputTokenLimit": 8192,
151
+ "supportedGenerationMethods": ["generateContent", "streamGenerateContent"],
152
+ "temperature": 1.0,
153
+ "maxTemperature": 2.0,
154
+ "topP": 0.95,
155
+ "topK": 64
156
+ },
157
+ {
158
+ "name": "models/gemini-embedding-001",
159
+ "version": "001",
160
+ "displayName": "Gemini Embedding 001",
161
+ "description": "Text embedding model for semantic similarity and search",
162
+ "inputTokenLimit": 2048,
163
+ "outputTokenLimit": 1,
164
+ "supportedGenerationMethods": ["embedContent"],
165
+ "temperature": 0.0,
166
+ "maxTemperature": 0.0,
167
+ "topP": 1.0,
168
+ "topK": 1
169
+ }
170
+ ]
src/gemini.py DELETED
@@ -1,228 +0,0 @@
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 DELETED
@@ -1,68 +0,0 @@
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 DELETED
@@ -1,73 +0,0 @@
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/gemini_routes.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gemini API Routes - Handles native Gemini API endpoints.
3
+ This module provides native Gemini API endpoints that proxy directly to Google's API
4
+ without any format transformations.
5
+ """
6
+ import json
7
+ from fastapi import APIRouter, Request, Response, Depends
8
+
9
+ from .auth import authenticate_user
10
+ from .google_api_client import send_gemini_request, build_gemini_payload_from_native
11
+ from .config import SUPPORTED_MODELS
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ @router.get("/v1beta/models")
17
+ async def gemini_list_models(request: Request, username: str = Depends(authenticate_user)):
18
+ """
19
+ Native Gemini models endpoint.
20
+ Returns available models in Gemini format, matching the official Gemini API.
21
+ """
22
+
23
+ models_response = {
24
+ "models": SUPPORTED_MODELS
25
+ }
26
+
27
+ return Response(
28
+ content=json.dumps(models_response),
29
+ status_code=200,
30
+ media_type="application/json; charset=utf-8"
31
+ )
32
+
33
+
34
+ @router.api_route("/{full_path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
35
+ async def gemini_proxy(request: Request, full_path: str, username: str = Depends(authenticate_user)):
36
+ """
37
+ Native Gemini API proxy endpoint.
38
+ Handles all native Gemini API calls by proxying them directly to Google's API.
39
+
40
+ This endpoint handles paths like:
41
+ - /v1beta/models/{model}/generateContent
42
+ - /v1beta/models/{model}/streamGenerateContent
43
+ - /v1/models/{model}/generateContent
44
+ - etc.
45
+ """
46
+
47
+ # Get the request body
48
+ post_data = await request.body()
49
+
50
+ # Determine if this is a streaming request
51
+ is_streaming = "stream" in full_path.lower()
52
+
53
+ # Extract model name from the path
54
+ # Paths typically look like: v1beta/models/gemini-1.5-pro/generateContent
55
+ model_name = _extract_model_from_path(full_path)
56
+
57
+ if not model_name:
58
+ return Response(
59
+ content=json.dumps({
60
+ "error": {
61
+ "message": f"Could not extract model name from path: {full_path}",
62
+ "code": 400
63
+ }
64
+ }),
65
+ status_code=400,
66
+ media_type="application/json"
67
+ )
68
+
69
+ # Parse the incoming request
70
+ try:
71
+ if post_data:
72
+ incoming_request = json.loads(post_data)
73
+ else:
74
+ incoming_request = {}
75
+ except json.JSONDecodeError:
76
+ return Response(
77
+ content=json.dumps({
78
+ "error": {
79
+ "message": "Invalid JSON in request body",
80
+ "code": 400
81
+ }
82
+ }),
83
+ status_code=400,
84
+ media_type="application/json"
85
+ )
86
+
87
+ # Build the payload for Google API
88
+ gemini_payload = build_gemini_payload_from_native(incoming_request, model_name)
89
+
90
+ # Send the request to Google API
91
+ response = send_gemini_request(gemini_payload, is_streaming=is_streaming)
92
+
93
+ return response
94
+
95
+
96
+ def _extract_model_from_path(path: str) -> str:
97
+ """
98
+ Extract the model name from a Gemini API path.
99
+
100
+ Examples:
101
+ - "v1beta/models/gemini-1.5-pro/generateContent" -> "gemini-1.5-pro"
102
+ - "v1/models/gemini-2.0-flash/streamGenerateContent" -> "gemini-2.0-flash"
103
+
104
+ Args:
105
+ path: The API path
106
+
107
+ Returns:
108
+ Model name (just the model name, not prefixed with "models/") or None if not found
109
+ """
110
+ parts = path.split('/')
111
+
112
+ # Look for the pattern: .../models/{model_name}/...
113
+ try:
114
+ models_index = parts.index('models')
115
+ if models_index + 1 < len(parts):
116
+ model_name = parts[models_index + 1]
117
+ # Remove any action suffix like ":streamGenerateContent" or ":generateContent"
118
+ if ':' in model_name:
119
+ model_name = model_name.split(':')[0]
120
+ # Return just the model name without "models/" prefix
121
+ return model_name
122
+ except ValueError:
123
+ pass
124
+
125
+ # If we can't find the pattern, return None
126
+ return None
127
+
128
+
129
+ @router.get("/v1/models")
130
+ async def gemini_list_models_v1(request: Request, username: str = Depends(authenticate_user)):
131
+ """
132
+ Alternative models endpoint for v1 API version.
133
+ Some clients might use /v1/models instead of /v1beta/models.
134
+ """
135
+ return await gemini_list_models(request, username)
136
+
137
+
138
+ # Health check endpoint
139
+ @router.get("/health")
140
+ async def health_check():
141
+ """
142
+ Simple health check endpoint.
143
+ """
144
+ return {"status": "healthy", "service": "geminicli2api"}
src/google_api_client.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Google API Client - Handles all communication with Google's Gemini API.
3
+ This module is used by both OpenAI compatibility layer and native Gemini endpoints.
4
+ """
5
+ import json
6
+ import requests
7
+ from fastapi import Response
8
+ from fastapi.responses import StreamingResponse
9
+ from google.auth.transport.requests import Request as GoogleAuthRequest
10
+
11
+ from .auth import get_credentials, save_credentials, get_user_project_id, onboard_user
12
+ from .utils import get_user_agent
13
+ from .config import CODE_ASSIST_ENDPOINT, DEFAULT_SAFETY_SETTINGS
14
+
15
+
16
+ def send_gemini_request(payload: dict, is_streaming: bool = False) -> Response:
17
+ """
18
+ Send a request to Google's Gemini API.
19
+
20
+ Args:
21
+ payload: The request payload in Gemini format
22
+ is_streaming: Whether this is a streaming request
23
+
24
+ Returns:
25
+ FastAPI Response object
26
+ """
27
+ # Get and validate credentials
28
+ creds = get_credentials()
29
+ if not creds:
30
+ return Response(
31
+ content="Authentication failed. Please restart the proxy to log in.",
32
+ status_code=500
33
+ )
34
+
35
+
36
+ # Refresh credentials if needed
37
+ if creds.expired and creds.refresh_token:
38
+ try:
39
+ creds.refresh(GoogleAuthRequest())
40
+ save_credentials(creds)
41
+ except Exception as e:
42
+ return Response(
43
+ content="Token refresh failed. Please restart the proxy to re-authenticate.",
44
+ status_code=500
45
+ )
46
+ elif not creds.token:
47
+ return Response(
48
+ content="No access token. Please restart the proxy to re-authenticate.",
49
+ status_code=500
50
+ )
51
+
52
+ # Get project ID and onboard user
53
+ proj_id = get_user_project_id(creds)
54
+ if not proj_id:
55
+ return Response(content="Failed to get user project ID.", status_code=500)
56
+
57
+ onboard_user(creds, proj_id)
58
+
59
+ # Build the final payload with project info
60
+ final_payload = {
61
+ "model": payload.get("model"),
62
+ "project": proj_id,
63
+ "request": payload.get("request", {})
64
+ }
65
+
66
+ # Determine the action and URL
67
+ action = "streamGenerateContent" if is_streaming else "generateContent"
68
+ target_url = f"{CODE_ASSIST_ENDPOINT}/v1internal:{action}"
69
+ if is_streaming:
70
+ target_url += "?alt=sse"
71
+
72
+ # Build request headers
73
+ request_headers = {
74
+ "Authorization": f"Bearer {creds.token}",
75
+ "Content-Type": "application/json",
76
+ "User-Agent": get_user_agent(),
77
+ }
78
+
79
+ final_post_data = json.dumps(final_payload)
80
+
81
+ # Send the request
82
+ if is_streaming:
83
+ resp = requests.post(target_url, data=final_post_data, headers=request_headers, stream=True)
84
+ return _handle_streaming_response(resp)
85
+ else:
86
+ resp = requests.post(target_url, data=final_post_data, headers=request_headers)
87
+ return _handle_non_streaming_response(resp)
88
+
89
+
90
+ def _handle_streaming_response(resp) -> StreamingResponse:
91
+ """Handle streaming response from Google API."""
92
+ import asyncio
93
+
94
+ async def stream_generator():
95
+ try:
96
+ with resp:
97
+ resp.raise_for_status()
98
+
99
+
100
+ for chunk in resp.iter_lines():
101
+ if chunk:
102
+ if not isinstance(chunk, str):
103
+ chunk = chunk.decode('utf-8')
104
+
105
+
106
+ if chunk.startswith('data: '):
107
+ chunk = chunk[len('data: '):]
108
+
109
+ try:
110
+ obj = json.loads(chunk)
111
+
112
+ if "response" in obj:
113
+ response_chunk = obj["response"]
114
+ response_json = json.dumps(response_chunk, separators=(',', ':'))
115
+ response_line = f"data: {response_json}\n\n"
116
+ yield response_line
117
+ await asyncio.sleep(0)
118
+ except json.JSONDecodeError:
119
+ continue
120
+
121
+ except requests.exceptions.RequestException as e:
122
+ yield f'data: {{"error": {{"message": "Upstream request failed: {str(e)}"}}}}\n\n'.encode('utf-8')
123
+ except Exception as e:
124
+ yield f'data: {{"error": {{"message": "An unexpected error occurred: {str(e)}"}}}}\n\n'.encode('utf-8')
125
+
126
+ response_headers = {
127
+ "Content-Type": "text/event-stream",
128
+ "Content-Disposition": "attachment",
129
+ "Vary": "Origin, X-Origin, Referer",
130
+ "X-XSS-Protection": "0",
131
+ "X-Frame-Options": "SAMEORIGIN",
132
+ "X-Content-Type-Options": "nosniff",
133
+ "Server": "ESF"
134
+ }
135
+
136
+ return StreamingResponse(
137
+ stream_generator(),
138
+ media_type="text/event-stream",
139
+ headers=response_headers
140
+ )
141
+
142
+
143
+ def _handle_non_streaming_response(resp) -> Response:
144
+ """Handle non-streaming response from Google API."""
145
+ if resp.status_code == 200:
146
+ try:
147
+ google_api_response = resp.text
148
+ if google_api_response.startswith('data: '):
149
+ google_api_response = google_api_response[len('data: '):]
150
+ google_api_response = json.loads(google_api_response)
151
+ standard_gemini_response = google_api_response.get("response")
152
+ return Response(
153
+ content=json.dumps(standard_gemini_response),
154
+ status_code=200,
155
+ media_type="application/json; charset=utf-8"
156
+ )
157
+ except (json.JSONDecodeError, AttributeError) as e:
158
+ return Response(
159
+ content=resp.content,
160
+ status_code=resp.status_code,
161
+ media_type=resp.headers.get("Content-Type")
162
+ )
163
+ else:
164
+ return Response(
165
+ content=resp.content,
166
+ status_code=resp.status_code,
167
+ media_type=resp.headers.get("Content-Type")
168
+ )
169
+
170
+
171
+ def build_gemini_payload_from_openai(openai_payload: dict) -> dict:
172
+ """
173
+ Build a Gemini API payload from an OpenAI-transformed request.
174
+ This is used when OpenAI requests are converted to Gemini format.
175
+ """
176
+ # Extract model from the payload
177
+ model = openai_payload.get("model")
178
+
179
+ # Get safety settings or use defaults
180
+ safety_settings = openai_payload.get("safetySettings", DEFAULT_SAFETY_SETTINGS)
181
+
182
+ # Build the request portion
183
+ request_data = {
184
+ "contents": openai_payload.get("contents"),
185
+ "systemInstruction": openai_payload.get("systemInstruction"),
186
+ "cachedContent": openai_payload.get("cachedContent"),
187
+ "tools": openai_payload.get("tools"),
188
+ "toolConfig": openai_payload.get("toolConfig"),
189
+ "safetySettings": safety_settings,
190
+ "generationConfig": openai_payload.get("generationConfig", {}),
191
+ }
192
+
193
+ # Remove any keys with None values
194
+ request_data = {k: v for k, v in request_data.items() if v is not None}
195
+
196
+ return {
197
+ "model": model,
198
+ "request": request_data
199
+ }
200
+
201
+
202
+ def build_gemini_payload_from_native(native_request: dict, model_from_path: str) -> dict:
203
+ """
204
+ Build a Gemini API payload from a native Gemini request.
205
+ This is used for direct Gemini API calls.
206
+ """
207
+ # Add default safety settings if not provided
208
+ if "safetySettings" not in native_request:
209
+ native_request["safetySettings"] = DEFAULT_SAFETY_SETTINGS
210
+
211
+ return {
212
+ "model": model_from_path,
213
+ "request": native_request
214
+ }
src/main.py CHANGED
@@ -1,7 +1,7 @@
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()
@@ -17,18 +17,23 @@ app.add_middleware(
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):
 
1
  from fastapi import FastAPI, Request, Response
2
  from fastapi.middleware.cors import CORSMiddleware
3
+ from .gemini_routes import router as gemini_router
4
+ from .openai_routes import router as openai_router
5
  from .auth import get_credentials, get_user_project_id, onboard_user
6
 
7
  app = FastAPI()
 
17
 
18
  @app.on_event("startup")
19
  async def startup_event():
20
+ try:
21
+ creds = get_credentials()
22
+ if creds:
23
+ try:
24
+ proj_id = get_user_project_id(creds)
25
+ if proj_id:
26
+ onboard_user(creds, proj_id)
27
+ print("Gemini proxy server started")
28
+ print("Authentication required - Password: see .env file")
29
+ except Exception as e:
30
+ print(f"Setup failed: {str(e)}")
31
+ print("Server started but may not function properly until setup issues are resolved.")
32
+ else:
33
+ print("Could not obtain credentials. Please authenticate and restart the server.")
34
+ except Exception as e:
35
+ print(f"Startup error: {str(e)}")
36
+ print("Server may not function properly.")
37
 
38
  @app.options("/{full_path:path}")
39
  async def handle_preflight(request: Request, full_path: str):
src/models.py CHANGED
@@ -13,6 +13,15 @@ class OpenAIChatCompletionRequest(BaseModel):
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
 
13
  temperature: Optional[float] = None
14
  top_p: Optional[float] = None
15
  max_tokens: Optional[int] = None
16
+ stop: Optional[Union[str, List[str]]] = None
17
+ frequency_penalty: Optional[float] = None
18
+ presence_penalty: Optional[float] = None
19
+ n: Optional[int] = None
20
+ seed: Optional[int] = None
21
+ response_format: Optional[Dict[str, Any]] = None
22
+
23
+ class Config:
24
+ extra = "allow" # Allow additional fields not explicitly defined
25
 
26
  class OpenAIChatCompletionChoice(BaseModel):
27
  index: int
src/openai.py DELETED
@@ -1,184 +0,0 @@
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/openai_routes.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OpenAI API Routes - Handles OpenAI-compatible endpoints.
3
+ This module provides OpenAI-compatible endpoints that transform requests/responses
4
+ and delegate to the Google API client.
5
+ """
6
+ import json
7
+ import uuid
8
+ import asyncio
9
+ from fastapi import APIRouter, Request, Response, Depends
10
+ from fastapi.responses import StreamingResponse
11
+
12
+ from .auth import authenticate_user
13
+ from .models import OpenAIChatCompletionRequest
14
+ from .openai_transformers import (
15
+ openai_request_to_gemini,
16
+ gemini_response_to_openai,
17
+ gemini_stream_chunk_to_openai
18
+ )
19
+ from .google_api_client import send_gemini_request, build_gemini_payload_from_openai
20
+
21
+ router = APIRouter()
22
+
23
+
24
+ @router.post("/v1/chat/completions")
25
+ async def openai_chat_completions(
26
+ request: OpenAIChatCompletionRequest,
27
+ http_request: Request,
28
+ username: str = Depends(authenticate_user)
29
+ ):
30
+ """
31
+ OpenAI-compatible chat completions endpoint.
32
+ Transforms OpenAI requests to Gemini format, sends to Google API,
33
+ and transforms responses back to OpenAI format.
34
+ """
35
+
36
+ # Transform OpenAI request to Gemini format
37
+ gemini_request_data = openai_request_to_gemini(request)
38
+
39
+ # Build the payload for Google API
40
+ gemini_payload = build_gemini_payload_from_openai(gemini_request_data)
41
+
42
+ if request.stream:
43
+ # Handle streaming response
44
+ async def openai_stream_generator():
45
+ response = send_gemini_request(gemini_payload, is_streaming=True)
46
+
47
+ if isinstance(response, StreamingResponse):
48
+ response_id = "chatcmpl-" + str(uuid.uuid4())
49
+
50
+ async for chunk in response.body_iterator:
51
+ if isinstance(chunk, bytes):
52
+ chunk = chunk.decode('utf-8')
53
+
54
+ if chunk.startswith('data: '):
55
+ try:
56
+ # Parse the Gemini streaming chunk
57
+ chunk_data = chunk[6:] # Remove 'data: ' prefix
58
+ gemini_chunk = json.loads(chunk_data)
59
+
60
+ # Transform to OpenAI format
61
+ openai_chunk = gemini_stream_chunk_to_openai(
62
+ gemini_chunk,
63
+ request.model,
64
+ response_id
65
+ )
66
+
67
+ # Send as OpenAI streaming format
68
+ yield f"data: {json.dumps(openai_chunk)}\n\n"
69
+ await asyncio.sleep(0)
70
+
71
+ except (json.JSONDecodeError, KeyError, UnicodeDecodeError) as e:
72
+ continue
73
+
74
+ # Send the final [DONE] marker
75
+ yield "data: [DONE]\n\n"
76
+ else:
77
+ # Error case - forward the error response
78
+ error_data = {
79
+ "error": {
80
+ "message": "Streaming request failed",
81
+ "type": "api_error"
82
+ }
83
+ }
84
+ yield f"data: {json.dumps(error_data)}\n\n"
85
+ yield "data: [DONE]\n\n"
86
+
87
+ return StreamingResponse(
88
+ openai_stream_generator(),
89
+ media_type="text/event-stream"
90
+ )
91
+
92
+ else:
93
+ # Handle non-streaming response
94
+ response = send_gemini_request(gemini_payload, is_streaming=False)
95
+
96
+ if isinstance(response, Response) and response.status_code != 200:
97
+ # Forward error responses as-is
98
+ return response
99
+
100
+ try:
101
+ # Parse Gemini response and transform to OpenAI format
102
+ gemini_response = json.loads(response.body)
103
+ openai_response = gemini_response_to_openai(gemini_response, request.model)
104
+
105
+ return openai_response
106
+
107
+ except (json.JSONDecodeError, AttributeError) as e:
108
+ return Response(
109
+ content=json.dumps({
110
+ "error": {
111
+ "message": "Failed to process response",
112
+ "type": "api_error"
113
+ }
114
+ }),
115
+ status_code=500,
116
+ media_type="application/json"
117
+ )
118
+
119
+
120
+ @router.get("/v1/models")
121
+ async def openai_list_models(username: str = Depends(authenticate_user)):
122
+ """
123
+ OpenAI-compatible models endpoint.
124
+ Returns available models in OpenAI format.
125
+ """
126
+
127
+ # Convert our Gemini models to OpenAI format
128
+ from .config import SUPPORTED_MODELS
129
+
130
+ openai_models = []
131
+ for model in SUPPORTED_MODELS:
132
+ openai_models.append({
133
+ "id": model["name"],
134
+ "object": "model",
135
+ "created": 1677610602, # Static timestamp
136
+ "owned_by": "google",
137
+ "permission": [
138
+ {
139
+ "id": "modelperm-" + model["name"].replace("/", "-"),
140
+ "object": "model_permission",
141
+ "created": 1677610602,
142
+ "allow_create_engine": False,
143
+ "allow_sampling": True,
144
+ "allow_logprobs": False,
145
+ "allow_search_indices": False,
146
+ "allow_view": True,
147
+ "allow_fine_tuning": False,
148
+ "organization": "*",
149
+ "group": None,
150
+ "is_blocking": False
151
+ }
152
+ ],
153
+ "root": model["name"],
154
+ "parent": None
155
+ })
156
+
157
+ return {
158
+ "object": "list",
159
+ "data": openai_models
160
+ }
161
+
162
+
163
+ # Test endpoint for debugging (can be removed in production)
164
+ @router.post("/v1/test")
165
+ async def openai_test_endpoint(
166
+ request: OpenAIChatCompletionRequest,
167
+ username: str = Depends(authenticate_user)
168
+ ):
169
+ """
170
+ Test endpoint for debugging OpenAI transformations.
171
+ """
172
+
173
+ # Transform the request and return the result for inspection
174
+ gemini_request_data = openai_request_to_gemini(request)
175
+
176
+ return {
177
+ "original_openai_request": request.dict(),
178
+ "transformed_gemini_request": gemini_request_data,
179
+ "message": "Transformation successful"
180
+ }
src/openai_transformers.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OpenAI Format Transformers - Handles conversion between OpenAI and Gemini API formats.
3
+ This module contains all the logic for transforming requests and responses between the two formats.
4
+ """
5
+ import json
6
+ import time
7
+ import uuid
8
+ from typing import Dict, Any
9
+
10
+ from .models import OpenAIChatCompletionRequest, OpenAIChatCompletionResponse
11
+ from .config import DEFAULT_SAFETY_SETTINGS
12
+
13
+
14
+ def openai_request_to_gemini(openai_request: OpenAIChatCompletionRequest) -> Dict[str, Any]:
15
+ """
16
+ Transform an OpenAI chat completion request to Gemini format.
17
+
18
+ Args:
19
+ openai_request: OpenAI format request
20
+
21
+ Returns:
22
+ Dictionary in Gemini API format
23
+ """
24
+ contents = []
25
+
26
+ # Process each message in the conversation
27
+ for message in openai_request.messages:
28
+ role = message.role
29
+
30
+ # Map OpenAI roles to Gemini roles
31
+ if role == "assistant":
32
+ role = "model"
33
+ elif role == "system":
34
+ role = "user" # Gemini treats system messages as user messages
35
+
36
+ # Handle different content types (string vs list of parts)
37
+ if isinstance(message.content, list):
38
+ parts = []
39
+ for part in message.content:
40
+ if part.get("type") == "text":
41
+ parts.append({"text": part.get("text", "")})
42
+ elif part.get("type") == "image_url":
43
+ image_url = part.get("image_url", {}).get("url")
44
+ if image_url:
45
+ # Parse data URI: "data:image/jpeg;base64,{base64_image}"
46
+ try:
47
+ mime_type, base64_data = image_url.split(";")
48
+ _, mime_type = mime_type.split(":")
49
+ _, base64_data = base64_data.split(",")
50
+ parts.append({
51
+ "inlineData": {
52
+ "mimeType": mime_type,
53
+ "data": base64_data
54
+ }
55
+ })
56
+ except ValueError:
57
+ continue
58
+ contents.append({"role": role, "parts": parts})
59
+ else:
60
+ # Simple text content
61
+ contents.append({"role": role, "parts": [{"text": message.content}]})
62
+
63
+ # Map OpenAI generation parameters to Gemini format
64
+ generation_config = {}
65
+ if openai_request.temperature is not None:
66
+ generation_config["temperature"] = openai_request.temperature
67
+ if openai_request.top_p is not None:
68
+ generation_config["topP"] = openai_request.top_p
69
+ if openai_request.max_tokens is not None:
70
+ generation_config["maxOutputTokens"] = openai_request.max_tokens
71
+ if openai_request.stop is not None:
72
+ # Gemini supports stop sequences
73
+ if isinstance(openai_request.stop, str):
74
+ generation_config["stopSequences"] = [openai_request.stop]
75
+ elif isinstance(openai_request.stop, list):
76
+ generation_config["stopSequences"] = openai_request.stop
77
+ if openai_request.frequency_penalty is not None:
78
+ # Map frequency_penalty to Gemini's frequencyPenalty
79
+ generation_config["frequencyPenalty"] = openai_request.frequency_penalty
80
+ if openai_request.presence_penalty is not None:
81
+ # Map presence_penalty to Gemini's presencePenalty
82
+ generation_config["presencePenalty"] = openai_request.presence_penalty
83
+ if openai_request.n is not None:
84
+ # Map n (number of completions) to Gemini's candidateCount
85
+ generation_config["candidateCount"] = openai_request.n
86
+ if openai_request.seed is not None:
87
+ # Gemini supports seed for reproducible outputs
88
+ generation_config["seed"] = openai_request.seed
89
+ if openai_request.response_format is not None:
90
+ # Handle JSON mode if specified
91
+ if openai_request.response_format.get("type") == "json_object":
92
+ generation_config["responseMimeType"] = "application/json"
93
+
94
+ return {
95
+ "contents": contents,
96
+ "generationConfig": generation_config,
97
+ "safetySettings": DEFAULT_SAFETY_SETTINGS,
98
+ "model": openai_request.model
99
+ }
100
+
101
+
102
+ def gemini_response_to_openai(gemini_response: Dict[str, Any], model: str) -> Dict[str, Any]:
103
+ """
104
+ Transform a Gemini API response to OpenAI chat completion format.
105
+
106
+ Args:
107
+ gemini_response: Response from Gemini API
108
+ model: Model name to include in response
109
+
110
+ Returns:
111
+ Dictionary in OpenAI chat completion format
112
+ """
113
+ choices = []
114
+
115
+ for candidate in gemini_response.get("candidates", []):
116
+ role = candidate.get("content", {}).get("role", "assistant")
117
+
118
+ # Map Gemini roles back to OpenAI roles
119
+ if role == "model":
120
+ role = "assistant"
121
+
122
+ # Extract text content from parts
123
+ parts = candidate.get("content", {}).get("parts", [])
124
+ content = ""
125
+ if parts and len(parts) > 0:
126
+ content = parts[0].get("text", "")
127
+
128
+ choices.append({
129
+ "index": candidate.get("index", 0),
130
+ "message": {
131
+ "role": role,
132
+ "content": content,
133
+ },
134
+ "finish_reason": _map_finish_reason(candidate.get("finishReason")),
135
+ })
136
+
137
+ return {
138
+ "id": str(uuid.uuid4()),
139
+ "object": "chat.completion",
140
+ "created": int(time.time()),
141
+ "model": model,
142
+ "choices": choices,
143
+ }
144
+
145
+
146
+ def gemini_stream_chunk_to_openai(gemini_chunk: Dict[str, Any], model: str, response_id: str) -> Dict[str, Any]:
147
+ """
148
+ Transform a Gemini streaming response chunk to OpenAI streaming format.
149
+
150
+ Args:
151
+ gemini_chunk: Single chunk from Gemini streaming response
152
+ model: Model name to include in response
153
+ response_id: Consistent ID for this streaming response
154
+
155
+ Returns:
156
+ Dictionary in OpenAI streaming format
157
+ """
158
+ choices = []
159
+
160
+ for candidate in gemini_chunk.get("candidates", []):
161
+ role = candidate.get("content", {}).get("role", "assistant")
162
+
163
+ # Map Gemini roles back to OpenAI roles
164
+ if role == "model":
165
+ role = "assistant"
166
+
167
+ # Extract text content from parts
168
+ parts = candidate.get("content", {}).get("parts", [])
169
+ content = ""
170
+ if parts and len(parts) > 0:
171
+ content = parts[0].get("text", "")
172
+
173
+ choices.append({
174
+ "index": candidate.get("index", 0),
175
+ "delta": {
176
+ "content": content,
177
+ },
178
+ "finish_reason": _map_finish_reason(candidate.get("finishReason")),
179
+ })
180
+
181
+ return {
182
+ "id": response_id,
183
+ "object": "chat.completion.chunk",
184
+ "created": int(time.time()),
185
+ "model": model,
186
+ "choices": choices,
187
+ }
188
+
189
+
190
+ def _map_finish_reason(gemini_reason: str) -> str:
191
+ """
192
+ Map Gemini finish reasons to OpenAI finish reasons.
193
+
194
+ Args:
195
+ gemini_reason: Finish reason from Gemini API
196
+
197
+ Returns:
198
+ OpenAI-compatible finish reason
199
+ """
200
+ if gemini_reason == "STOP":
201
+ return "stop"
202
+ elif gemini_reason == "MAX_TOKENS":
203
+ return "length"
204
+ elif gemini_reason in ["SAFETY", "RECITATION"]:
205
+ return "content_filter"
206
+ else:
207
+ return None
src/utils.py CHANGED
@@ -1,6 +1,5 @@
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."""
 
1
  import platform
2
+ from .config import CLI_VERSION
 
3
 
4
  def get_user_agent():
5
  """Generate User-Agent string matching gemini-cli format."""