bibibi12345 commited on
Commit
7aa95a1
·
1 Parent(s): 42fe818

added direct api call to vertex express with leaked project id

Browse files
Files changed (1) hide show
  1. app/direct_vertex_client.py +260 -0
app/direct_vertex_client.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp
2
+ import asyncio
3
+ import json
4
+ import re
5
+ from typing import Dict, Any, List, Union, Optional, AsyncGenerator
6
+ import time
7
+
8
+ # Global cache for project IDs: {api_key: project_id}
9
+ PROJECT_ID_CACHE: Dict[str, str] = {}
10
+
11
+
12
+ class DirectVertexClient:
13
+ """
14
+ A client that connects to Vertex AI using direct URLs instead of the SDK.
15
+ Mimics the interface of genai.Client for seamless integration.
16
+ """
17
+
18
+ def __init__(self, api_key: str):
19
+ self.api_key = api_key
20
+ self.project_id: Optional[str] = None
21
+ self.base_url = "https://aiplatform.googleapis.com/v1"
22
+ self.session: Optional[aiohttp.ClientSession] = None
23
+ # Mimic the model_name attribute that might be accessed
24
+ self.model_name = "direct_vertex_client"
25
+
26
+ # Create nested structure to mimic genai.Client interface
27
+ self.aio = self._AioNamespace(self)
28
+
29
+ class _AioNamespace:
30
+ def __init__(self, parent):
31
+ self.parent = parent
32
+ self.models = self._ModelsNamespace(parent)
33
+
34
+ class _ModelsNamespace:
35
+ def __init__(self, parent):
36
+ self.parent = parent
37
+
38
+ async def generate_content(self, model: str, contents: Any, config: Dict[str, Any]) -> Any:
39
+ """Non-streaming content generation"""
40
+ return await self.parent._generate_content(model, contents, config, stream=False)
41
+
42
+ def generate_content_stream(self, model: str, contents: Any, config: Dict[str, Any]):
43
+ """Streaming content generation - returns an awaitable that yields an async generator"""
44
+ return self.parent._generate_content_stream(model, contents, config)
45
+
46
+ async def _ensure_session(self):
47
+ """Ensure aiohttp session is created"""
48
+ if self.session is None:
49
+ self.session = aiohttp.ClientSession()
50
+
51
+ async def close(self):
52
+ """Clean up resources"""
53
+ if self.session:
54
+ await self.session.close()
55
+ self.session = None
56
+
57
+ async def discover_project_id(self) -> None:
58
+ """Discover project ID by triggering an intentional error"""
59
+ # Check cache first
60
+ if self.api_key in PROJECT_ID_CACHE:
61
+ self.project_id = PROJECT_ID_CACHE[self.api_key]
62
+ print(f"INFO: Using cached project ID: {self.project_id}")
63
+ return
64
+
65
+ await self._ensure_session()
66
+
67
+ # Use a non-existent model to trigger error
68
+ error_url = f"{self.base_url}/publishers/google/models/gemini-2.7-pro-preview-05-06:streamGenerateContent?key={self.api_key}"
69
+
70
+ try:
71
+ # Send minimal request to trigger error
72
+ payload = {
73
+ "contents": [{"role": "user", "parts": [{"text": "test"}]}]
74
+ }
75
+
76
+ async with self.session.post(error_url, json=payload) as response:
77
+ response_text = await response.text()
78
+
79
+ try:
80
+ # Try to parse as JSON first
81
+ error_data = json.loads(response_text)
82
+
83
+ # Handle array response format
84
+ if isinstance(error_data, list) and len(error_data) > 0:
85
+ error_data = error_data[0]
86
+
87
+ if "error" in error_data:
88
+ error_message = error_data["error"].get("message", "")
89
+ # Extract project ID from error message
90
+ # Pattern: "projects/39982734461/locations/..."
91
+ match = re.search(r'projects/(\d+)/locations/', error_message)
92
+ if match:
93
+ self.project_id = match.group(1)
94
+ PROJECT_ID_CACHE[self.api_key] = self.project_id
95
+ print(f"INFO: Discovered project ID: {self.project_id}")
96
+ return
97
+ except json.JSONDecodeError:
98
+ # If not JSON, try to find project ID in raw text
99
+ match = re.search(r'projects/(\d+)/locations/', response_text)
100
+ if match:
101
+ self.project_id = match.group(1)
102
+ PROJECT_ID_CACHE[self.api_key] = self.project_id
103
+ print(f"INFO: Discovered project ID from raw response: {self.project_id}")
104
+ return
105
+
106
+ raise Exception(f"Failed to discover project ID. Status: {response.status}, Response: {response_text[:500]}")
107
+
108
+ except Exception as e:
109
+ print(f"ERROR: Failed to discover project ID: {e}")
110
+ raise
111
+
112
+
113
+ async def _generate_content(self, model: str, contents: Any, config: Dict[str, Any], stream: bool = False) -> Any:
114
+ """Internal method for content generation"""
115
+ if not self.project_id:
116
+ raise ValueError("Project ID not discovered. Call discover_project_id() first.")
117
+
118
+ await self._ensure_session()
119
+
120
+ # Build URL
121
+ endpoint = "streamGenerateContent" if stream else "generateContent"
122
+ url = f"{self.base_url}/projects/{self.project_id}/locations/global/publishers/google/models/{model}:{endpoint}?key={self.api_key}"
123
+
124
+ # The contents and config are already in the correct format
125
+ # But config parameters need to be nested under generationConfig for the REST API
126
+ payload = {
127
+ "contents": contents
128
+ }
129
+
130
+ # Extract specific config sections
131
+ if "system_instruction" in config:
132
+ payload["systemInstruction"] = config["system_instruction"]
133
+
134
+ if "safety_settings" in config:
135
+ payload["safetySettings"] = config["safety_settings"]
136
+
137
+ if "tools" in config:
138
+ payload["tools"] = config["tools"]
139
+
140
+ # All other config goes under generationConfig
141
+ generation_config = {}
142
+ for key, value in config.items():
143
+ if key not in ["system_instruction", "safety_settings", "tools"]:
144
+ generation_config[key] = value
145
+
146
+ if generation_config:
147
+ payload["generationConfig"] = generation_config
148
+
149
+ try:
150
+ async with self.session.post(url, json=payload) as response:
151
+ if response.status != 200:
152
+ error_data = await response.json()
153
+ error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status}")
154
+ raise Exception(f"Vertex AI API error: {error_msg}")
155
+
156
+ # Get the JSON response
157
+ response_data = await response.json()
158
+
159
+ # Convert dict to object with attributes for compatibility
160
+ return self._dict_to_obj(response_data)
161
+
162
+ except Exception as e:
163
+ print(f"ERROR: Direct Vertex API call failed: {e}")
164
+ raise
165
+
166
+ def _dict_to_obj(self, data):
167
+ """Convert a dict to an object with attributes"""
168
+ if isinstance(data, dict):
169
+ # Create a simple object that allows attribute access
170
+ class AttrDict:
171
+ def __init__(self, d):
172
+ for key, value in d.items():
173
+ setattr(self, key, self._convert_value(value))
174
+
175
+ def _convert_value(self, value):
176
+ if isinstance(value, dict):
177
+ return AttrDict(value)
178
+ elif isinstance(value, list):
179
+ return [self._convert_value(item) for item in value]
180
+ else:
181
+ return value
182
+
183
+ return AttrDict(data)
184
+ elif isinstance(data, list):
185
+ return [self._dict_to_obj(item) for item in data]
186
+ else:
187
+ return data
188
+
189
+ async def _generate_content_stream(self, model: str, contents: Any, config: Dict[str, Any]) -> AsyncGenerator:
190
+ """Internal method for streaming content generation"""
191
+ if not self.project_id:
192
+ raise ValueError("Project ID not discovered. Call discover_project_id() first.")
193
+
194
+ await self._ensure_session()
195
+
196
+ # Build URL for streaming
197
+ url = f"{self.base_url}/projects/{self.project_id}/locations/global/publishers/google/models/{model}:streamGenerateContent?key={self.api_key}"
198
+
199
+ # The contents and config are already in the correct format
200
+ # But config parameters need to be nested under generationConfig for the REST API
201
+ payload = {
202
+ "contents": contents
203
+ }
204
+
205
+ # Extract specific config sections
206
+ if "system_instruction" in config:
207
+ payload["systemInstruction"] = config["system_instruction"]
208
+
209
+ if "safety_settings" in config:
210
+ payload["safetySettings"] = config["safety_settings"]
211
+
212
+ if "tools" in config:
213
+ payload["tools"] = config["tools"]
214
+
215
+ # All other config goes under generationConfig
216
+ generation_config = {}
217
+ for key, value in config.items():
218
+ if key not in ["system_instruction", "safety_settings", "tools"]:
219
+ generation_config[key] = value
220
+
221
+ if generation_config:
222
+ payload["generationConfig"] = generation_config
223
+
224
+ try:
225
+ async with self.session.post(url, json=payload) as response:
226
+ if response.status != 200:
227
+ error_data = await response.json()
228
+ error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status}")
229
+ raise Exception(f"Vertex AI API error: {error_msg}")
230
+
231
+ # The Vertex AI streaming endpoint returns Server-Sent Events
232
+ # We need to parse these and yield them as objects
233
+ buffer = ""
234
+ async for chunk in response.content.iter_any():
235
+ buffer += chunk.decode('utf-8')
236
+
237
+ # Process complete SSE messages
238
+ while '\n\n' in buffer:
239
+ message, buffer = buffer.split('\n\n', 1)
240
+
241
+ if not message.strip():
242
+ continue
243
+
244
+ # Parse SSE format
245
+ if message.startswith('data: '):
246
+ data_str = message[6:]
247
+
248
+ if data_str.strip() == '[DONE]':
249
+ return
250
+
251
+ try:
252
+ # Parse JSON and convert to object
253
+ chunk_data = json.loads(data_str)
254
+ yield self._dict_to_obj(chunk_data)
255
+ except json.JSONDecodeError:
256
+ continue
257
+
258
+ except Exception as e:
259
+ print(f"ERROR: Direct Vertex streaming API call failed: {e}")
260
+ raise