mcp-mesh 0.6.0__py3-none-any.whl → 0.6.2__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.
Files changed (50) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/decorator_registry.py +26 -2
  3. _mcp_mesh/engine/dependency_injector.py +14 -1
  4. _mcp_mesh/engine/llm_config.py +11 -7
  5. _mcp_mesh/engine/mesh_llm_agent.py +247 -61
  6. _mcp_mesh/engine/mesh_llm_agent_injector.py +174 -0
  7. _mcp_mesh/engine/provider_handlers/__init__.py +20 -0
  8. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +122 -0
  9. _mcp_mesh/engine/provider_handlers/claude_handler.py +138 -0
  10. _mcp_mesh/engine/provider_handlers/generic_handler.py +156 -0
  11. _mcp_mesh/engine/provider_handlers/openai_handler.py +163 -0
  12. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +167 -0
  13. _mcp_mesh/engine/response_parser.py +3 -38
  14. _mcp_mesh/engine/tool_schema_builder.py +3 -2
  15. _mcp_mesh/generated/.openapi-generator/FILES +3 -0
  16. _mcp_mesh/generated/.openapi-generator-ignore +0 -1
  17. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +51 -97
  18. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +42 -72
  19. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +11 -1
  20. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +108 -0
  21. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +95 -0
  22. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +37 -58
  23. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +32 -63
  24. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +30 -29
  25. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +41 -59
  26. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +51 -98
  27. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +70 -85
  28. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +51 -84
  29. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +112 -0
  30. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +54 -21
  31. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +43 -26
  32. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +3 -3
  33. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +35 -10
  34. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +1 -1
  35. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +77 -39
  36. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +118 -35
  37. _mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +1 -1
  38. _mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +48 -3
  39. _mcp_mesh/pipeline/mcp_startup/server_discovery.py +77 -48
  40. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +2 -2
  41. _mcp_mesh/shared/health_check_cache.py +246 -0
  42. _mcp_mesh/shared/registry_client_wrapper.py +29 -2
  43. {mcp_mesh-0.6.0.dist-info → mcp_mesh-0.6.2.dist-info}/METADATA +1 -1
  44. {mcp_mesh-0.6.0.dist-info → mcp_mesh-0.6.2.dist-info}/RECORD +50 -39
  45. mesh/__init__.py +12 -2
  46. mesh/decorators.py +105 -39
  47. mesh/helpers.py +259 -0
  48. mesh/types.py +53 -4
  49. {mcp_mesh-0.6.0.dist-info → mcp_mesh-0.6.2.dist-info}/WHEEL +0 -0
  50. {mcp_mesh-0.6.0.dist-info → mcp_mesh-0.6.2.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py CHANGED
@@ -31,7 +31,7 @@ from .engine.decorator_registry import (
31
31
  get_decorator_stats,
32
32
  )
33
33
 
34
- __version__ = "0.6.0"
34
+ __version__ = "0.6.2"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -117,11 +117,13 @@ class DecoratorRegistry:
117
117
  if func_name in cls._mesh_tools:
118
118
  old_func = cls._mesh_tools[func_name].function
119
119
  cls._mesh_tools[func_name].function = new_func
120
- print(
120
+ logger.debug(
121
121
  f"🔄 DecoratorRegistry: Updated '{func_name}' from {hex(id(old_func))} to {hex(id(new_func))}"
122
122
  )
123
123
  else:
124
- print(f"⚠️ DecoratorRegistry: Function '{func_name}' not found for update")
124
+ logger.debug(
125
+ f"⚠️ DecoratorRegistry: Function '{func_name}' not found for update"
126
+ )
125
127
 
126
128
  @classmethod
127
129
  def register_mesh_resource(cls, func: Callable, metadata: dict[str, Any]) -> None:
@@ -623,6 +625,28 @@ class DecoratorRegistry:
623
625
  cls._immediate_uvicorn_server = None
624
626
  logger.debug("🔄 REGISTRY: Cleared immediate uvicorn server reference")
625
627
 
628
+ # Health check result storage
629
+ _health_check_result: dict | None = None
630
+
631
+ @classmethod
632
+ def store_health_check_result(cls, result: dict) -> None:
633
+ """Store health check result for /health endpoint."""
634
+ cls._health_check_result = result
635
+ logger.debug(
636
+ f"💾 REGISTRY: Stored health check result: {result.get('status', 'unknown')}"
637
+ )
638
+
639
+ @classmethod
640
+ def get_health_check_result(cls) -> dict | None:
641
+ """Get stored health check result."""
642
+ return cls._health_check_result
643
+
644
+ @classmethod
645
+ def clear_health_check_result(cls) -> None:
646
+ """Clear stored health check result."""
647
+ cls._health_check_result = None
648
+ logger.debug("🗑️ REGISTRY: Cleared health check result")
649
+
626
650
  @classmethod
627
651
  def store_fastmcp_lifespan(cls, lifespan: Any) -> None:
628
652
  """
@@ -132,7 +132,7 @@ class DependencyInjector:
132
132
  instance: Proxy instance to register
133
133
  """
134
134
  async with self._lock:
135
- logger.info(f"📦 Registering dependency: {name}")
135
+ logger.debug(f"📦 Registering dependency: {name}")
136
136
  self._dependencies[name] = instance
137
137
 
138
138
  # Notify all functions that depend on this (using composite keys)
@@ -302,6 +302,19 @@ class DependencyInjector:
302
302
  )
303
303
  self._llm_injector.process_llm_tools(llm_tools)
304
304
 
305
+ def process_llm_providers(self, llm_providers: dict[str, dict[str, Any]]) -> None:
306
+ """
307
+ Process llm_providers from registry response and delegate to MeshLlmAgentInjector (v0.6.1).
308
+
309
+ Args:
310
+ llm_providers: Dict mapping function_name -> ResolvedLLMProvider
311
+ Format: {"function_name": {"agent_id": "...", "endpoint": "...", ...}}
312
+ """
313
+ logger.info(
314
+ f"🔌 DependencyInjector processing llm_providers for {len(llm_providers)} functions"
315
+ )
316
+ self._llm_injector.process_llm_providers(llm_providers)
317
+
305
318
  def update_llm_tools(self, llm_tools: dict[str, list[dict[str, Any]]]) -> None:
306
319
  """
307
320
  Update llm_tools when topology changes (heartbeat updates).
@@ -5,7 +5,7 @@ Consolidates LLM-related configuration into a single type-safe structure.
5
5
  """
6
6
 
7
7
  from dataclasses import dataclass
8
- from typing import Optional
8
+ from typing import Any, Dict, Optional, Union
9
9
 
10
10
 
11
11
  @dataclass
@@ -14,16 +14,18 @@ class LLMConfig:
14
14
  Configuration for MeshLlmAgent.
15
15
 
16
16
  Consolidates provider, model, and runtime settings into a single type-safe structure.
17
+ Supports both direct LiteLLM providers (string) and mesh delegation (dict).
17
18
  """
18
19
 
19
- provider: str = "claude"
20
- """LLM provider (e.g., 'claude', 'openai', 'gemini')"""
20
+ provider: Union[str, Dict[str, Any]] = "claude"
21
+ """LLM provider - string for direct LiteLLM (e.g., 'claude', 'openai') or dict for mesh delegation
22
+ Mesh delegation format: {"capability": "llm", "tags": ["claude"], "version": ">=1.0.0"}"""
21
23
 
22
24
  model: str = "claude-3-5-sonnet-20241022"
23
- """Model name for the provider"""
25
+ """Model name for the provider (only used with string provider for direct LiteLLM)"""
24
26
 
25
27
  api_key: str = ""
26
- """API key for the provider (uses environment variable if empty)"""
28
+ """API key for the provider (uses environment variable if empty, only used with string provider)"""
27
29
 
28
30
  max_iterations: int = 10
29
31
  """Maximum iterations for the agentic loop"""
@@ -37,5 +39,7 @@ class LLMConfig:
37
39
  raise ValueError("max_iterations must be >= 1")
38
40
  if not self.provider:
39
41
  raise ValueError("provider cannot be empty")
40
- if not self.model:
41
- raise ValueError("model cannot be empty")
42
+
43
+ # Only validate model for string providers (not needed for mesh delegation)
44
+ if isinstance(self.provider, str) and not self.model:
45
+ raise ValueError("model cannot be empty when using string provider")
@@ -8,7 +8,7 @@ import asyncio
8
8
  import json
9
9
  import logging
10
10
  from pathlib import Path
11
- from typing import Any, Optional
11
+ from typing import Any, Dict, List, Optional, Union
12
12
 
13
13
  from pydantic import BaseModel
14
14
 
@@ -19,6 +19,7 @@ from .llm_errors import (
19
19
  ResponseParseError,
20
20
  ToolExecutionError,
21
21
  )
22
+ from .provider_handlers import ProviderHandlerRegistry
22
23
  from .response_parser import ResponseParser
23
24
  from .tool_executor import ToolExecutor
24
25
  from .tool_schema_builder import ToolSchemaBuilder
@@ -61,6 +62,8 @@ class MeshLlmAgent:
61
62
  tool_proxies: Optional[dict[str, Any]] = None,
62
63
  template_path: Optional[str] = None,
63
64
  context_value: Optional[Any] = None,
65
+ provider_proxy: Optional[Any] = None,
66
+ vendor: Optional[str] = None,
64
67
  ):
65
68
  """
66
69
  Initialize MeshLlmAgent proxy.
@@ -72,6 +75,8 @@ class MeshLlmAgent:
72
75
  tool_proxies: Optional map of function_name -> proxy for tool execution
73
76
  template_path: Optional path to Jinja2 template file for system prompt
74
77
  context_value: Optional context for template rendering (MeshContextModel, dict, or None)
78
+ provider_proxy: Optional pre-resolved provider proxy for mesh delegation
79
+ vendor: Optional vendor name for handler selection (e.g., "anthropic", "openai")
75
80
  """
76
81
  self.config = config
77
82
  self.provider = config.provider
@@ -84,6 +89,10 @@ class MeshLlmAgent:
84
89
  self.system_prompt = config.system_prompt # Public attribute for tests
85
90
  self._iteration_count = 0
86
91
 
92
+ # Detect if using mesh delegation (provider is dict)
93
+ self._is_mesh_delegated = isinstance(self.provider, dict)
94
+ self._mesh_provider_proxy = provider_proxy # Pre-resolved by heartbeat
95
+
87
96
  # Template rendering support (Phase 3)
88
97
  self._template_path = template_path
89
98
  self._context_value = context_value
@@ -96,7 +105,15 @@ class MeshLlmAgent:
96
105
  # Build tool schemas for LLM (OpenAI format used by LiteLLM)
97
106
  self._tool_schemas = ToolSchemaBuilder.build_schemas(self.tools_metadata)
98
107
 
99
- # Cache tool calling instructions to prevent XML-style invocations
108
+ # Phase 2: Get provider-specific handler
109
+ # This enables vendor-optimized behavior (e.g., OpenAI response_format)
110
+ self._provider_handler = ProviderHandlerRegistry.get_handler(vendor)
111
+ logger.debug(
112
+ f"🎯 Using provider handler: {self._provider_handler} for vendor: {vendor}"
113
+ )
114
+
115
+ # DEPRECATED: Legacy cached instructions (now handled by provider handlers)
116
+ # Kept for backward compatibility with tests
100
117
  self._cached_tool_instructions = """
101
118
 
102
119
  IMPORTANT TOOL CALLING RULES:
@@ -109,8 +126,6 @@ IMPORTANT TOOL CALLING RULES:
109
126
  - Once you have gathered all necessary information, provide your final response
110
127
  """
111
128
 
112
- # Cache JSON schema instructions (output_type never changes after init)
113
- # This avoids regenerating the schema on every __call__
114
129
  schema = self.output_type.model_json_schema()
115
130
  schema_str = json.dumps(schema, indent=2)
116
131
  self._cached_json_instructions = (
@@ -120,7 +135,7 @@ IMPORTANT TOOL CALLING RULES:
120
135
 
121
136
  logger.debug(
122
137
  f"🤖 MeshLlmAgent initialized: provider={config.provider}, model={config.model}, "
123
- f"tools={len(filtered_tools)}, max_iterations={config.max_iterations}"
138
+ f"tools={len(filtered_tools)}, max_iterations={config.max_iterations}, handler={self._provider_handler}"
124
139
  )
125
140
 
126
141
  def set_system_prompt(self, prompt: str) -> None:
@@ -205,9 +220,7 @@ IMPORTANT TOOL CALLING RULES:
205
220
  return {}
206
221
 
207
222
  # Check if it's a MeshContextModel (has model_dump method)
208
- if hasattr(context_value, "model_dump") and callable(
209
- context_value.model_dump
210
- ):
223
+ if hasattr(context_value, "model_dump") and callable(context_value.model_dump):
211
224
  return context_value.model_dump()
212
225
 
213
226
  # Check if it's a dict
@@ -254,12 +267,153 @@ IMPORTANT TOOL CALLING RULES:
254
267
  # Otherwise, use literal system prompt from config
255
268
  return self.system_prompt or ""
256
269
 
257
- async def __call__(self, message: str, **kwargs) -> Any:
270
+ async def _get_mesh_provider(self) -> Any:
271
+ """
272
+ Get the mesh provider proxy (already resolved during heartbeat).
273
+
274
+ Returns:
275
+ UnifiedMCPProxy for the mesh provider agent
276
+
277
+ Raises:
278
+ RuntimeError: If provider proxy not resolved
279
+ """
280
+ if self._mesh_provider_proxy is None:
281
+ raise RuntimeError(
282
+ f"Mesh provider not resolved. Provider filter: {self.provider}. "
283
+ f"The provider should have been resolved during heartbeat. "
284
+ f"Check that a matching provider is registered in the mesh."
285
+ )
286
+
287
+ return self._mesh_provider_proxy
288
+
289
+ async def _call_mesh_provider(
290
+ self, messages: list, tools: list | None = None, **kwargs
291
+ ) -> Any:
292
+ """
293
+ Call mesh-delegated LLM provider agent.
294
+
295
+ Args:
296
+ messages: List of message dicts
297
+ tools: Optional list of tool schemas
298
+ **kwargs: Additional model parameters
299
+
300
+ Returns:
301
+ LiteLLM-compatible response object
302
+
303
+ Raises:
304
+ RuntimeError: If provider proxy not available or invocation fails
305
+ """
306
+ # Get the pre-resolved provider proxy
307
+ provider_proxy = await self._get_mesh_provider()
308
+
309
+ # Import MeshLlmRequest type
310
+ from mesh.types import MeshLlmRequest
311
+
312
+ # Build MeshLlmRequest
313
+ request = MeshLlmRequest(
314
+ messages=messages, tools=tools, model_params=kwargs if kwargs else None
315
+ )
316
+
317
+ logger.debug(
318
+ f"📤 Delegating to mesh provider: {len(messages)} messages, {len(tools) if tools else 0} tools"
319
+ )
320
+
321
+ # Call provider's process_chat tool
322
+ try:
323
+ # provider_proxy is UnifiedMCPProxy, call it with request dict
324
+ # Convert dataclass to dict for MCP call
325
+ request_dict = {
326
+ "messages": request.messages,
327
+ "tools": request.tools,
328
+ "model_params": request.model_params,
329
+ "context": request.context,
330
+ "request_id": request.request_id,
331
+ "caller_agent": request.caller_agent,
332
+ }
333
+
334
+ result = await provider_proxy(request=request_dict)
335
+
336
+ # Result is a message dict with content, role, and optionally tool_calls
337
+ # Parse it to create LiteLLM-compatible response
338
+ message_dict = result
339
+
340
+ # Create mock LiteLLM response structure
341
+ # This mimics litellm.completion() response format
342
+ class MockToolCall:
343
+ """Mock tool call object matching LiteLLM structure."""
344
+
345
+ def __init__(self, tc_dict):
346
+ self.id = tc_dict["id"]
347
+ self.type = tc_dict["type"]
348
+ # Create function object
349
+ self.function = type(
350
+ "Function",
351
+ (),
352
+ {
353
+ "name": tc_dict["function"]["name"],
354
+ "arguments": tc_dict["function"]["arguments"],
355
+ },
356
+ )()
357
+
358
+ class MockMessage:
359
+ def __init__(self, message_dict):
360
+ self.content = message_dict.get("content")
361
+ self.role = message_dict.get("role", "assistant")
362
+ # Extract tool_calls if present (critical for agentic loop!)
363
+ self.tool_calls = None
364
+ if "tool_calls" in message_dict and message_dict["tool_calls"]:
365
+ self.tool_calls = [
366
+ MockToolCall(tc) for tc in message_dict["tool_calls"]
367
+ ]
368
+
369
+ def model_dump(self):
370
+ dump = {"role": self.role, "content": self.content}
371
+ if self.tool_calls:
372
+ dump["tool_calls"] = [
373
+ {
374
+ "id": tc.id,
375
+ "type": tc.type,
376
+ "function": {
377
+ "name": tc.function.name,
378
+ "arguments": tc.function.arguments,
379
+ },
380
+ }
381
+ for tc in self.tool_calls
382
+ ]
383
+ return dump
384
+
385
+ class MockChoice:
386
+ def __init__(self, message):
387
+ self.message = message
388
+ self.finish_reason = "stop"
389
+
390
+ class MockResponse:
391
+ def __init__(self, message_dict):
392
+ self.choices = [MockChoice(MockMessage(message_dict))]
393
+
394
+ logger.debug(
395
+ f"📥 Received response from mesh provider: "
396
+ f"content={message_dict.get('content', '')[:200]}..., "
397
+ f"tool_calls={len(message_dict.get('tool_calls', []))}"
398
+ )
399
+
400
+ return MockResponse(message_dict)
401
+
402
+ except Exception as e:
403
+ logger.error(f"❌ Mesh provider call failed: {e}")
404
+ raise RuntimeError(f"Mesh LLM provider invocation failed: {e}") from e
405
+
406
+ async def __call__(
407
+ self, message: Union[str, list[dict[str, Any]]], **kwargs
408
+ ) -> Any:
258
409
  """
259
410
  Execute automatic agentic loop and return typed response.
260
411
 
261
412
  Args:
262
- message: User message to process
413
+ message: Either:
414
+ - str: Single user message (will be wrapped in messages array)
415
+ - List[Dict[str, Any]]: Full conversation history with messages
416
+ in format [{"role": "user|assistant|system", "content": "..."}]
263
417
  **kwargs: Additional arguments passed to LLM
264
418
 
265
419
  Returns:
@@ -278,29 +432,46 @@ IMPORTANT TOOL CALLING RULES:
278
432
  "litellm is required for MeshLlmAgent. Install with: pip install litellm"
279
433
  )
280
434
 
281
- # Build initial messages
282
- messages = []
283
-
284
- # Render system prompt (from template or literal)
435
+ # Render base system prompt (from template or literal)
285
436
  base_system_prompt = self._render_system_prompt()
286
437
 
287
- # Build system prompt with tool calling and JSON schema instructions
288
- system_content = base_system_prompt
438
+ # Phase 2: Use provider handler to format system prompt
439
+ # This allows vendor-specific optimizations (e.g., OpenAI skips JSON instructions)
440
+ system_content = self._provider_handler.format_system_prompt(
441
+ base_prompt=base_system_prompt,
442
+ tool_schemas=self._tool_schemas,
443
+ output_type=self.output_type,
444
+ )
289
445
 
290
- # Add tool calling instructions if tools are available
291
- if self._tool_schemas:
292
- system_content += self._cached_tool_instructions
446
+ # Debug: Log system prompt (truncated for privacy)
447
+ logger.debug(
448
+ f"📝 System prompt (formatted by {self._provider_handler}): {system_content[:200]}..."
449
+ )
293
450
 
294
- # Add JSON schema instructions for final response
295
- system_content += self._cached_json_instructions
451
+ # Build messages array based on input type
452
+ if isinstance(message, list):
453
+ # Multi-turn conversation - use provided messages array
454
+ messages = message.copy()
296
455
 
297
- # Debug: Log system prompt (truncated for privacy)
298
- logger.debug(f"📝 System prompt: {system_content[:200]}...")
456
+ # Ensure system prompt is prepended if not already present
457
+ if not messages or messages[0].get("role") != "system":
458
+ messages.insert(0, {"role": "system", "content": system_content})
459
+ else:
460
+ # Replace existing system message with our constructed one
461
+ messages[0] = {"role": "system", "content": system_content}
299
462
 
300
- messages.append({"role": "system", "content": system_content})
301
- messages.append({"role": "user", "content": message})
463
+ # Log conversation history
464
+ logger.info(
465
+ f"🚀 Starting agentic loop with {len(messages)} messages in history"
466
+ )
467
+ else:
468
+ # Single-turn - build messages array from string
469
+ messages = [
470
+ {"role": "system", "content": system_content},
471
+ {"role": "user", "content": message},
472
+ ]
302
473
 
303
- logger.info(f"🚀 Starting agentic loop for message: {message[:100]}...")
474
+ logger.info(f"🚀 Starting agentic loop for message: {message[:100]}...")
304
475
 
305
476
  # Agentic loop
306
477
  while self._iteration_count < self.max_iterations:
@@ -310,21 +481,61 @@ IMPORTANT TOOL CALLING RULES:
310
481
  )
311
482
 
312
483
  try:
313
- # Call LLM with tools
484
+ # Call LLM (either direct LiteLLM or mesh-delegated)
314
485
  try:
315
- response = await asyncio.to_thread(
316
- completion,
317
- model=self.model,
318
- messages=messages,
319
- tools=self._tool_schemas if self._tool_schemas else None,
320
- api_key=self.api_key,
321
- **kwargs,
322
- )
486
+ if self._is_mesh_delegated:
487
+ # Mesh delegation: use provider handler to prepare vendor-specific request
488
+ # Phase 2: Handler prepares params including response_format for OpenAI, etc.
489
+ request_params = self._provider_handler.prepare_request(
490
+ messages=messages,
491
+ tools=self._tool_schemas if self._tool_schemas else None,
492
+ output_type=self.output_type,
493
+ **kwargs,
494
+ )
495
+
496
+ # Extract model_params to send to provider
497
+ # Don't send messages/tools (already separate params) or model/api_key (provider has them)
498
+ model_params = {
499
+ k: v
500
+ for k, v in request_params.items()
501
+ if k not in ["messages", "tools", "model", "api_key"]
502
+ }
503
+
504
+ logger.debug(
505
+ f"📤 Delegating to mesh provider with handler-prepared params: "
506
+ f"keys={list(model_params.keys())}"
507
+ )
508
+
509
+ response = await self._call_mesh_provider(
510
+ messages=messages,
511
+ tools=self._tool_schemas if self._tool_schemas else None,
512
+ **model_params, # Now includes response_format!
513
+ )
514
+ else:
515
+ # Direct LiteLLM call
516
+ # Phase 2: Use provider handler to prepare vendor-specific request
517
+ request_params = self._provider_handler.prepare_request(
518
+ messages=messages,
519
+ tools=self._tool_schemas if self._tool_schemas else None,
520
+ output_type=self.output_type,
521
+ **kwargs,
522
+ )
523
+
524
+ # Add model and API key (common to all vendors)
525
+ request_params["model"] = self.model
526
+ request_params["api_key"] = self.api_key
527
+
528
+ logger.debug(
529
+ f"📤 Calling LLM with handler-prepared params: "
530
+ f"keys={list(request_params.keys())}"
531
+ )
532
+
533
+ response = await asyncio.to_thread(completion, **request_params)
323
534
  except Exception as e:
324
535
  # Any exception from completion call is an LLM API error
325
536
  logger.error(f"❌ LLM API error: {e}")
326
537
  raise LLMAPIError(
327
- provider=self.provider,
538
+ provider=str(self.provider),
328
539
  model=self.model,
329
540
  original_error=e,
330
541
  ) from e
@@ -359,31 +570,6 @@ IMPORTANT TOOL CALLING RULES:
359
570
  f"📥 Raw LLM response: {assistant_message.content[:500]}..."
360
571
  )
361
572
 
362
- # REMOVE_LATER: Debug full LLM response
363
- logger.warning(
364
- f"🔍 REMOVE_LATER: assistant_message type: {type(assistant_message)}"
365
- )
366
- logger.warning(
367
- f"🔍 REMOVE_LATER: assistant_message.content type: {type(assistant_message.content)}"
368
- )
369
- logger.warning(
370
- f"🔍 REMOVE_LATER: assistant_message.content is None: {assistant_message.content is None}"
371
- )
372
- if assistant_message.content:
373
- logger.warning(
374
- f"🔍 REMOVE_LATER: Full LLM response length: {len(assistant_message.content)}"
375
- )
376
- logger.warning(
377
- f"🔍 REMOVE_LATER: Full LLM response: {assistant_message.content!r}"
378
- )
379
- else:
380
- logger.warning(
381
- "🔍 REMOVE_LATER: assistant_message.content is empty or None!"
382
- )
383
- logger.warning(
384
- f"🔍 REMOVE_LATER: Full assistant_message: {assistant_message}"
385
- )
386
-
387
573
  return self._parse_response(assistant_message.content)
388
574
 
389
575
  except LLMAPIError: