aiecs 1.7.17__py3-none-any.whl → 1.8.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aiecs might be problematic. Click here for more details.

@@ -1,7 +1,8 @@
1
1
  import json
2
2
  import logging
3
3
  import os
4
- from typing import Optional, List, AsyncGenerator
4
+ import base64
5
+ from typing import Optional, List, AsyncGenerator, Dict, Any
5
6
 
6
7
  from google import genai
7
8
  from google.genai import types
@@ -14,6 +15,7 @@ from aiecs.llm.clients.base_client import (
14
15
  RateLimitError,
15
16
  )
16
17
  from aiecs.config.config import get_settings
18
+ from aiecs.llm.utils.image_utils import parse_image_source, ImageContent
17
19
 
18
20
  logger = logging.getLogger(__name__)
19
21
 
@@ -91,6 +93,33 @@ class GoogleAIClient(BaseLLMClient):
91
93
  parts = []
92
94
  if msg.content:
93
95
  parts.append(types.Part(text=msg.content))
96
+
97
+ # Add images if present
98
+ if msg.images:
99
+ for image_source in msg.images:
100
+ image_content = parse_image_source(image_source)
101
+
102
+ if image_content.is_url():
103
+ # For URLs, use inline_data with downloaded content
104
+ # Note: Google AI SDK may support URL directly, but we'll use base64 for compatibility
105
+ try:
106
+ import urllib.request
107
+ with urllib.request.urlopen(image_content.get_url()) as response:
108
+ image_bytes = response.read()
109
+ parts.append(types.Part.from_bytes(
110
+ data=image_bytes,
111
+ mime_type=image_content.mime_type
112
+ ))
113
+ except Exception as e:
114
+ logger.warning(f"Failed to download image from URL: {e}")
115
+ else:
116
+ # Convert to bytes for inline_data
117
+ base64_data = image_content.get_base64_data()
118
+ image_bytes = base64.b64decode(base64_data)
119
+ parts.append(types.Part.from_bytes(
120
+ data=image_bytes,
121
+ mime_type=image_content.mime_type
122
+ ))
94
123
 
95
124
  for tool_call in msg.tool_calls:
96
125
  func = tool_call.get("function", {})
@@ -120,10 +149,42 @@ class GoogleAIClient(BaseLLMClient):
120
149
  # Handle regular messages (user, assistant without tool_calls)
121
150
  else:
122
151
  role = "model" if msg.role == "assistant" else msg.role
152
+ parts = []
153
+
154
+ # Add text content if present
123
155
  if msg.content:
156
+ parts.append(types.Part(text=msg.content))
157
+
158
+ # Add images if present
159
+ if msg.images:
160
+ for image_source in msg.images:
161
+ image_content = parse_image_source(image_source)
162
+
163
+ if image_content.is_url():
164
+ # Download URL and convert to bytes
165
+ try:
166
+ import urllib.request
167
+ with urllib.request.urlopen(image_content.get_url()) as response:
168
+ image_bytes = response.read()
169
+ parts.append(types.Part.from_bytes(
170
+ data=image_bytes,
171
+ mime_type=image_content.mime_type
172
+ ))
173
+ except Exception as e:
174
+ logger.warning(f"Failed to download image from URL: {e}")
175
+ else:
176
+ # Convert to bytes for inline_data
177
+ base64_data = image_content.get_base64_data()
178
+ image_bytes = base64.b64decode(base64_data)
179
+ parts.append(types.Part.from_bytes(
180
+ data=image_bytes,
181
+ mime_type=image_content.mime_type
182
+ ))
183
+
184
+ if parts:
124
185
  contents.append(types.Content(
125
186
  role=role,
126
- parts=[types.Part(text=msg.content)]
187
+ parts=parts
127
188
  ))
128
189
 
129
190
  return contents
@@ -134,10 +195,30 @@ class GoogleAIClient(BaseLLMClient):
134
195
  model: Optional[str] = None,
135
196
  temperature: float = 0.7,
136
197
  max_tokens: Optional[int] = None,
198
+ context: Optional[Dict[str, Any]] = None,
137
199
  system_instruction: Optional[str] = None,
138
200
  **kwargs,
139
201
  ) -> LLMResponse:
140
- """Generate text using Google AI (google.genai SDK)"""
202
+ """
203
+ Generate text using Google AI (google.genai SDK).
204
+
205
+ Args:
206
+ messages: List of conversation messages
207
+ model: Model name (optional, uses default if not provided)
208
+ temperature: Sampling temperature (0.0 to 1.0)
209
+ max_tokens: Maximum tokens to generate
210
+ context: Optional context dictionary containing metadata such as:
211
+ - user_id: User identifier for tracking/billing
212
+ - tenant_id: Tenant identifier for multi-tenant setups
213
+ - request_id: Request identifier for tracing
214
+ - session_id: Session identifier
215
+ - Any other custom metadata for observability or middleware
216
+ system_instruction: System instruction for the model
217
+ **kwargs: Additional provider-specific parameters
218
+
219
+ Returns:
220
+ LLMResponse with generated text and metadata
221
+ """
141
222
  client = self._init_google_ai()
142
223
 
143
224
  # Get model name from config if not provided
@@ -237,10 +318,30 @@ class GoogleAIClient(BaseLLMClient):
237
318
  model: Optional[str] = None,
238
319
  temperature: float = 0.7,
239
320
  max_tokens: Optional[int] = None,
321
+ context: Optional[Dict[str, Any]] = None,
240
322
  system_instruction: Optional[str] = None,
241
323
  **kwargs,
242
324
  ) -> AsyncGenerator[str, None]:
243
- """Stream text generation using Google AI (google.genai SDK)"""
325
+ """
326
+ Stream text generation using Google AI (google.genai SDK).
327
+
328
+ Args:
329
+ messages: List of conversation messages
330
+ model: Model name (optional, uses default if not provided)
331
+ temperature: Sampling temperature (0.0 to 1.0)
332
+ max_tokens: Maximum tokens to generate
333
+ context: Optional context dictionary containing metadata such as:
334
+ - user_id: User identifier for tracking/billing
335
+ - tenant_id: Tenant identifier for multi-tenant setups
336
+ - request_id: Request identifier for tracing
337
+ - session_id: Session identifier
338
+ - Any other custom metadata for observability or middleware
339
+ system_instruction: System instruction for the model
340
+ **kwargs: Additional provider-specific parameters
341
+
342
+ Yields:
343
+ Text tokens as they are generated
344
+ """
244
345
  client = self._init_google_ai()
245
346
 
246
347
  # Get model name from config if not provided
@@ -52,6 +52,7 @@ class OpenAIClient(BaseLLMClient, OpenAICompatibleFunctionCallingMixin):
52
52
  model: Optional[str] = None,
53
53
  temperature: float = 0.7,
54
54
  max_tokens: Optional[int] = None,
55
+ context: Optional[Dict[str, Any]] = None,
55
56
  functions: Optional[List[Dict[str, Any]]] = None,
56
57
  tools: Optional[List[Dict[str, Any]]] = None,
57
58
  tool_choice: Optional[Any] = None,
@@ -65,6 +66,11 @@ class OpenAIClient(BaseLLMClient, OpenAICompatibleFunctionCallingMixin):
65
66
  model: Model name (optional)
66
67
  temperature: Temperature for generation
67
68
  max_tokens: Maximum tokens to generate
69
+ context: Optional context dictionary containing metadata such as:
70
+ - user_id: User identifier for tracking/billing
71
+ - tenant_id: Tenant identifier for multi-tenant setups
72
+ - request_id: Request identifier for tracing
73
+ - session_id: Session identifier
68
74
  functions: List of function schemas (legacy format)
69
75
  tools: List of tool schemas (new format, recommended)
70
76
  tool_choice: Tool choice strategy ("auto", "none", or specific tool)
@@ -103,6 +109,7 @@ class OpenAIClient(BaseLLMClient, OpenAICompatibleFunctionCallingMixin):
103
109
  model: Optional[str] = None,
104
110
  temperature: float = 0.7,
105
111
  max_tokens: Optional[int] = None,
112
+ context: Optional[Dict[str, Any]] = None,
106
113
  functions: Optional[List[Dict[str, Any]]] = None,
107
114
  tools: Optional[List[Dict[str, Any]]] = None,
108
115
  tool_choice: Optional[Any] = None,
@@ -117,6 +124,11 @@ class OpenAIClient(BaseLLMClient, OpenAICompatibleFunctionCallingMixin):
117
124
  model: Model name (optional)
118
125
  temperature: Temperature for generation
119
126
  max_tokens: Maximum tokens to generate
127
+ context: Optional context dictionary containing metadata such as:
128
+ - user_id: User identifier for tracking/billing
129
+ - tenant_id: Tenant identifier for multi-tenant setups
130
+ - request_id: Request identifier for tracing
131
+ - session_id: Session identifier
120
132
  functions: List of function schemas (legacy format)
121
133
  tools: List of tool schemas (new format, recommended)
122
134
  tool_choice: Tool choice strategy ("auto", "none", or specific tool)
@@ -11,6 +11,7 @@ from dataclasses import dataclass
11
11
  from openai import AsyncOpenAI
12
12
 
13
13
  from .base_client import LLMMessage, LLMResponse
14
+ from aiecs.llm.utils.image_utils import parse_image_source, ImageContent
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
@@ -49,7 +50,7 @@ class OpenAICompatibleFunctionCallingMixin:
49
50
 
50
51
  def _convert_messages_to_openai_format(self, messages: List[LLMMessage]) -> List[Dict[str, Any]]:
51
52
  """
52
- Convert LLMMessage list to OpenAI message format (support tool calls).
53
+ Convert LLMMessage list to OpenAI message format (support tool calls and vision).
53
54
 
54
55
  Args:
55
56
  messages: List of LLMMessage objects
@@ -60,8 +61,47 @@ class OpenAICompatibleFunctionCallingMixin:
60
61
  openai_messages = []
61
62
  for msg in messages:
62
63
  msg_dict: Dict[str, Any] = {"role": msg.role}
63
- if msg.content is not None:
64
+
65
+ # Handle multimodal content (text + images)
66
+ if msg.images:
67
+ # Build content array with text and images
68
+ content_array = []
69
+
70
+ # Add text content if present
71
+ if msg.content:
72
+ content_array.append({"type": "text", "text": msg.content})
73
+
74
+ # Add images
75
+ for image_source in msg.images:
76
+ image_content = parse_image_source(image_source)
77
+
78
+ if image_content.is_url():
79
+ # Use URL directly
80
+ content_array.append({
81
+ "type": "image_url",
82
+ "image_url": {
83
+ "url": image_content.get_url(),
84
+ "detail": image_content.detail,
85
+ }
86
+ })
87
+ else:
88
+ # Convert to base64 data URI
89
+ base64_data = image_content.get_base64_data()
90
+ mime_type = image_content.mime_type
91
+ data_uri = f"data:{mime_type};base64,{base64_data}"
92
+ content_array.append({
93
+ "type": "image_url",
94
+ "image_url": {
95
+ "url": data_uri,
96
+ "detail": image_content.detail,
97
+ }
98
+ })
99
+
100
+ msg_dict["content"] = content_array
101
+ elif msg.content is not None:
102
+ # Text-only content
64
103
  msg_dict["content"] = msg.content
104
+
65
105
  if msg.tool_calls:
66
106
  msg_dict["tool_calls"] = msg.tool_calls
67
107
  if msg.tool_call_id:
@@ -0,0 +1,272 @@
1
+ from openai import AsyncOpenAI
2
+ from aiecs.config.config import get_settings
3
+ from aiecs.llm.clients.base_client import (
4
+ BaseLLMClient,
5
+ LLMMessage,
6
+ LLMResponse,
7
+ ProviderNotAvailableError,
8
+ RateLimitError,
9
+ )
10
+ from aiecs.llm.clients.openai_compatible_mixin import (
11
+ OpenAICompatibleFunctionCallingMixin,
12
+ StreamChunk,
13
+ )
14
+ from tenacity import (
15
+ retry,
16
+ stop_after_attempt,
17
+ wait_exponential,
18
+ retry_if_exception_type,
19
+ )
20
+ import logging
21
+ from typing import Dict, Optional, List, AsyncGenerator, cast, Any
22
+
23
+ # Lazy import to avoid circular dependency
24
+
25
+
26
+ def _get_config_loader():
27
+ """Lazy import of config loader to avoid circular dependency"""
28
+ from aiecs.llm.config import get_llm_config_loader
29
+
30
+ return get_llm_config_loader()
31
+
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class OpenRouterClient(BaseLLMClient, OpenAICompatibleFunctionCallingMixin):
37
+ """OpenRouter provider client using OpenAI-compatible API"""
38
+
39
+ def __init__(self) -> None:
40
+ super().__init__("OpenRouter")
41
+ self.settings = get_settings()
42
+ self._openai_client: Optional[AsyncOpenAI] = None
43
+ self._model_map: Optional[Dict[str, str]] = None
44
+
45
+ def _get_openai_client(self) -> AsyncOpenAI:
46
+ """Lazy initialization of OpenAI client for OpenRouter"""
47
+ if not self._openai_client:
48
+ api_key = self._get_api_key()
49
+ self._openai_client = AsyncOpenAI(
50
+ api_key=api_key,
51
+ base_url="https://openrouter.ai/api/v1",
52
+ timeout=360.0,
53
+ )
54
+ return self._openai_client
55
+
56
+ def _get_api_key(self) -> str:
57
+ """Get API key from settings"""
58
+ api_key = getattr(self.settings, "openrouter_api_key", None)
59
+ if not api_key:
60
+ raise ProviderNotAvailableError("OpenRouter API key not configured. Set OPENROUTER_API_KEY.")
61
+ return api_key
62
+
63
+ def _get_model_map(self) -> Dict[str, str]:
64
+ """Get model mappings from configuration"""
65
+ if self._model_map is None:
66
+ try:
67
+ loader = _get_config_loader()
68
+ provider_config = loader.get_provider_config("OpenRouter")
69
+ if provider_config and provider_config.model_mappings:
70
+ self._model_map = provider_config.model_mappings
71
+ else:
72
+ self._model_map = {}
73
+ except Exception as e:
74
+ self.logger.warning(f"Failed to load model mappings from config: {e}")
75
+ self._model_map = {}
76
+ return self._model_map
77
+
78
+ def _get_extra_headers(self, **kwargs) -> Dict[str, str]:
79
+ """
80
+ Get extra headers for OpenRouter API.
81
+
82
+ Supports HTTP-Referer and X-Title headers from kwargs or settings.
83
+
84
+ Args:
85
+ **kwargs: May contain http_referer and x_title
86
+
87
+ Returns:
88
+ Dictionary with extra headers
89
+ """
90
+ extra_headers: Dict[str, str] = {}
91
+
92
+ # Get from kwargs first, then from settings
93
+ http_referer = kwargs.get("http_referer") or getattr(self.settings, "openrouter_http_referer", None)
94
+ x_title = kwargs.get("x_title") or getattr(self.settings, "openrouter_x_title", None)
95
+
96
+ if http_referer:
97
+ extra_headers["HTTP-Referer"] = http_referer
98
+ if x_title:
99
+ extra_headers["X-Title"] = x_title
100
+
101
+ return extra_headers
102
+
103
+ @retry(
104
+ stop=stop_after_attempt(3),
105
+ wait=wait_exponential(multiplier=1, min=4, max=10),
106
+ retry=retry_if_exception_type((Exception, RateLimitError)),
107
+ )
108
+ async def generate_text(
109
+ self,
110
+ messages: List[LLMMessage],
111
+ model: Optional[str] = None,
112
+ temperature: float = 0.7,
113
+ max_tokens: Optional[int] = None,
114
+ functions: Optional[List[Dict[str, Any]]] = None,
115
+ tools: Optional[List[Dict[str, Any]]] = None,
116
+ tool_choice: Optional[Any] = None,
117
+ **kwargs,
118
+ ) -> LLMResponse:
119
+ """
120
+ Generate text using OpenRouter API via OpenAI library.
121
+
122
+ OpenRouter API is OpenAI-compatible, so it supports Function Calling and Vision.
123
+
124
+ Args:
125
+ messages: List of LLM messages
126
+ model: Model name (optional, uses default from config if not provided)
127
+ temperature: Temperature for generation
128
+ max_tokens: Maximum tokens to generate
129
+ functions: List of function schemas (legacy format)
130
+ tools: List of tool schemas (new format, recommended)
131
+ tool_choice: Tool choice strategy ("auto", "none", or specific tool)
132
+ http_referer: Optional HTTP-Referer header for OpenRouter rankings
133
+ x_title: Optional X-Title header for OpenRouter rankings
134
+ **kwargs: Additional arguments passed to OpenRouter API
135
+
136
+ Returns:
137
+ LLMResponse with content and optional function_call information
138
+ """
139
+ # Check API key availability
140
+ api_key = self._get_api_key()
141
+ if not api_key:
142
+ raise ProviderNotAvailableError("OpenRouter API key is not configured.")
143
+
144
+ client = self._get_openai_client()
145
+
146
+ # Get model name from config if not provided
147
+ selected_model = model or self._get_default_model() or "openai/gpt-4o"
148
+
149
+ # Get model mappings from config
150
+ model_map = self._get_model_map()
151
+ api_model = model_map.get(selected_model, selected_model)
152
+
153
+ # Extract extra headers from kwargs
154
+ extra_headers = self._get_extra_headers(**kwargs)
155
+
156
+ # Remove extra header kwargs to avoid passing them to API
157
+ kwargs_clean = {k: v for k, v in kwargs.items() if k not in ("http_referer", "x_title")}
158
+
159
+ # Add extra_headers to kwargs if present
160
+ if extra_headers:
161
+ kwargs_clean["extra_headers"] = extra_headers
162
+
163
+ try:
164
+ # Use mixin method for Function Calling support
165
+ response = await self._generate_text_with_function_calling(
166
+ client=client,
167
+ messages=messages,
168
+ model=api_model,
169
+ temperature=temperature,
170
+ max_tokens=max_tokens,
171
+ functions=functions,
172
+ tools=tools,
173
+ tool_choice=tool_choice,
174
+ **kwargs_clean,
175
+ )
176
+
177
+ # Override provider and model name for OpenRouter
178
+ response.provider = self.provider_name
179
+ response.model = selected_model
180
+
181
+ return response
182
+
183
+ except Exception as e:
184
+ if "rate limit" in str(e).lower() or "429" in str(e):
185
+ raise RateLimitError(f"OpenRouter rate limit exceeded: {str(e)}")
186
+ logger.error(f"OpenRouter API error: {str(e)}")
187
+ raise
188
+
189
+ async def stream_text( # type: ignore[override]
190
+ self,
191
+ messages: List[LLMMessage],
192
+ model: Optional[str] = None,
193
+ temperature: float = 0.7,
194
+ max_tokens: Optional[int] = None,
195
+ functions: Optional[List[Dict[str, Any]]] = None,
196
+ tools: Optional[List[Dict[str, Any]]] = None,
197
+ tool_choice: Optional[Any] = None,
198
+ return_chunks: bool = False,
199
+ **kwargs,
200
+ ) -> AsyncGenerator[Any, None]:
201
+ """
202
+ Stream text using OpenRouter API via OpenAI library.
203
+
204
+ OpenRouter API is OpenAI-compatible, so it supports Function Calling and Vision.
205
+
206
+ Args:
207
+ messages: List of LLM messages
208
+ model: Model name (optional, uses default from config if not provided)
209
+ temperature: Temperature for generation
210
+ max_tokens: Maximum tokens to generate
211
+ functions: List of function schemas (legacy format)
212
+ tools: List of tool schemas (new format, recommended)
213
+ tool_choice: Tool choice strategy ("auto", "none", or specific tool)
214
+ return_chunks: If True, returns StreamChunk objects with tool_calls info; if False, returns str tokens only
215
+ http_referer: Optional HTTP-Referer header for OpenRouter rankings
216
+ x_title: Optional X-Title header for OpenRouter rankings
217
+ **kwargs: Additional arguments passed to OpenRouter API
218
+
219
+ Yields:
220
+ str or StreamChunk: Text tokens as they are generated, or StreamChunk objects if return_chunks=True
221
+ """
222
+ # Check API key availability
223
+ api_key = self._get_api_key()
224
+ if not api_key:
225
+ raise ProviderNotAvailableError("OpenRouter API key is not configured.")
226
+
227
+ client = self._get_openai_client()
228
+
229
+ # Get model name from config if not provided
230
+ selected_model = model or self._get_default_model() or "openai/gpt-4o"
231
+
232
+ # Get model mappings from config
233
+ model_map = self._get_model_map()
234
+ api_model = model_map.get(selected_model, selected_model)
235
+
236
+ # Extract extra headers from kwargs
237
+ extra_headers = self._get_extra_headers(**kwargs)
238
+
239
+ # Remove extra header kwargs to avoid passing them to API
240
+ kwargs_clean = {k: v for k, v in kwargs.items() if k not in ("http_referer", "x_title")}
241
+
242
+ # Add extra_headers to kwargs if present
243
+ if extra_headers:
244
+ kwargs_clean["extra_headers"] = extra_headers
245
+
246
+ try:
247
+ # Use mixin method for Function Calling support
248
+ async for chunk in self._stream_text_with_function_calling(
249
+ client=client,
250
+ messages=messages,
251
+ model=api_model,
252
+ temperature=temperature,
253
+ max_tokens=max_tokens,
254
+ functions=functions,
255
+ tools=tools,
256
+ tool_choice=tool_choice,
257
+ return_chunks=return_chunks,
258
+ **kwargs_clean,
259
+ ):
260
+ yield chunk
261
+
262
+ except Exception as e:
263
+ if "rate limit" in str(e).lower() or "429" in str(e):
264
+ raise RateLimitError(f"OpenRouter rate limit exceeded: {str(e)}")
265
+ logger.error(f"OpenRouter API streaming error: {str(e)}")
266
+ raise
267
+
268
+ async def close(self):
269
+ """Clean up resources"""
270
+ if self._openai_client:
271
+ await self._openai_client.close()
272
+ self._openai_client = None
@@ -4,6 +4,7 @@ import logging
4
4
  import os
5
5
  import warnings
6
6
  import hashlib
7
+ import base64
7
8
  from typing import Dict, Any, Optional, List, AsyncGenerator, Union
8
9
  import vertexai
9
10
  from vertexai.generative_models import (
@@ -16,6 +17,8 @@ from vertexai.generative_models import (
16
17
  Part,
17
18
  )
18
19
 
20
+ from aiecs.llm.utils.image_utils import parse_image_source, ImageContent
21
+
19
22
  logger = logging.getLogger(__name__)
20
23
 
21
24
  # Try to import CachedContent for prompt caching support
@@ -450,6 +453,24 @@ class VertexAIClient(BaseLLMClient, GoogleFunctionCallingMixin):
450
453
  parts = []
451
454
  if msg.content:
452
455
  parts.append(Part.from_text(msg.content))
456
+
457
+ # Add images if present
458
+ if msg.images:
459
+ for image_source in msg.images:
460
+ image_content = parse_image_source(image_source)
461
+
462
+ if image_content.is_url():
463
+ parts.append(Part.from_uri(
464
+ uri=image_content.get_url(),
465
+ mime_type=image_content.mime_type
466
+ ))
467
+ else:
468
+ base64_data = image_content.get_base64_data()
469
+ image_bytes = base64.b64decode(base64_data)
470
+ parts.append(Part.from_bytes(
471
+ data=image_bytes,
472
+ mime_type=image_content.mime_type
473
+ ))
453
474
 
454
475
  for tool_call in msg.tool_calls:
455
476
  func = tool_call.get("function", {})
@@ -481,11 +502,34 @@ class VertexAIClient(BaseLLMClient, GoogleFunctionCallingMixin):
481
502
  # Handle regular messages (user, assistant without tool_calls)
482
503
  else:
483
504
  role = "model" if msg.role == "assistant" else msg.role
505
+ parts = []
506
+
507
+ # Add text content if present
484
508
  if msg.content:
485
- contents.append(Content(
486
- role=role,
487
- parts=[Part.from_text(msg.content)]
488
- ))
509
+ parts.append(Part.from_text(msg.content))
510
+
511
+ # Add images if present
512
+ if msg.images:
513
+ for image_source in msg.images:
514
+ image_content = parse_image_source(image_source)
515
+
516
+ if image_content.is_url():
517
+ # Use Part.from_uri for URLs
518
+ parts.append(Part.from_uri(
519
+ uri=image_content.get_url(),
520
+ mime_type=image_content.mime_type
521
+ ))
522
+ else:
523
+ # Convert to bytes for inline_data
524
+ base64_data = image_content.get_base64_data()
525
+ image_bytes = base64.b64decode(base64_data)
526
+ parts.append(Part.from_bytes(
527
+ data=image_bytes,
528
+ mime_type=image_content.mime_type
529
+ ))
530
+
531
+ if parts:
532
+ contents.append(Content(role=role, parts=parts))
489
533
 
490
534
  return contents
491
535
 
@@ -495,13 +539,36 @@ class VertexAIClient(BaseLLMClient, GoogleFunctionCallingMixin):
495
539
  model: Optional[str] = None,
496
540
  temperature: float = 0.7,
497
541
  max_tokens: Optional[int] = None,
542
+ context: Optional[Dict[str, Any]] = None,
498
543
  functions: Optional[List[Dict[str, Any]]] = None,
499
544
  tools: Optional[List[Dict[str, Any]]] = None,
500
545
  tool_choice: Optional[Any] = None,
501
546
  system_instruction: Optional[str] = None,
502
547
  **kwargs,
503
548
  ) -> LLMResponse:
504
- """Generate text using Vertex AI"""
549
+ """
550
+ Generate text using Vertex AI.
551
+
552
+ Args:
553
+ messages: List of conversation messages
554
+ model: Model name (optional, uses default if not provided)
555
+ temperature: Sampling temperature (0.0 to 1.0)
556
+ max_tokens: Maximum tokens to generate
557
+ context: Optional context dictionary containing metadata such as:
558
+ - user_id: User identifier for tracking/billing
559
+ - tenant_id: Tenant identifier for multi-tenant setups
560
+ - request_id: Request identifier for tracing
561
+ - session_id: Session identifier
562
+ - Any other custom metadata for observability or middleware
563
+ functions: List of function schemas (legacy format)
564
+ tools: List of tool schemas (new format, recommended)
565
+ tool_choice: Tool choice strategy
566
+ system_instruction: System instruction for the model
567
+ **kwargs: Additional provider-specific parameters
568
+
569
+ Returns:
570
+ LLMResponse with generated text and metadata
571
+ """
505
572
  self._init_vertex_ai()
506
573
 
507
574
  # Get model name from config if not provided
@@ -889,6 +956,7 @@ class VertexAIClient(BaseLLMClient, GoogleFunctionCallingMixin):
889
956
  model: Optional[str] = None,
890
957
  temperature: float = 0.7,
891
958
  max_tokens: Optional[int] = None,
959
+ context: Optional[Dict[str, Any]] = None,
892
960
  functions: Optional[List[Dict[str, Any]]] = None,
893
961
  tools: Optional[List[Dict[str, Any]]] = None,
894
962
  tool_choice: Optional[Any] = None,
@@ -904,6 +972,12 @@ class VertexAIClient(BaseLLMClient, GoogleFunctionCallingMixin):
904
972
  model: Model name (optional)
905
973
  temperature: Temperature for generation
906
974
  max_tokens: Maximum tokens to generate
975
+ context: Optional context dictionary containing metadata such as:
976
+ - user_id: User identifier for tracking/billing
977
+ - tenant_id: Tenant identifier for multi-tenant setups
978
+ - request_id: Request identifier for tracing
979
+ - session_id: Session identifier
980
+ - Any other custom metadata for observability or middleware
907
981
  functions: List of function schemas (legacy format)
908
982
  tools: List of tool schemas (new format)
909
983
  tool_choice: Tool choice strategy (not used for Google Vertex AI)