mcp-mesh 0.7.21__py3-none-any.whl → 0.8.0b1__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 (121) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/dependency_injector.py +4 -6
  3. _mcp_mesh/engine/http_wrapper.py +69 -10
  4. _mcp_mesh/engine/mesh_llm_agent.py +4 -7
  5. _mcp_mesh/engine/mesh_llm_agent_injector.py +2 -1
  6. _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
  7. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
  8. _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
  9. _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
  10. _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
  11. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
  12. _mcp_mesh/engine/response_parser.py +61 -15
  13. _mcp_mesh/engine/unified_mcp_proxy.py +18 -34
  14. _mcp_mesh/pipeline/__init__.py +9 -20
  15. _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
  16. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
  17. _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +425 -0
  18. _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
  19. _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
  20. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
  21. _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
  22. _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
  23. _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
  24. _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +695 -0
  25. _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
  26. _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
  27. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +5 -6
  28. _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
  29. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +21 -9
  30. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
  31. _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
  32. _mcp_mesh/reload.py +1 -3
  33. _mcp_mesh/shared/__init__.py +2 -8
  34. _mcp_mesh/shared/config_resolver.py +124 -80
  35. _mcp_mesh/shared/defaults.py +89 -14
  36. _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
  37. _mcp_mesh/shared/host_resolver.py +8 -46
  38. _mcp_mesh/shared/server_discovery.py +115 -86
  39. _mcp_mesh/shared/simple_shutdown.py +44 -86
  40. _mcp_mesh/tracing/execution_tracer.py +2 -6
  41. _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
  42. _mcp_mesh/tracing/trace_context_helper.py +3 -13
  43. _mcp_mesh/tracing/utils.py +29 -15
  44. _mcp_mesh/utils/fastmcp_schema_extractor.py +2 -1
  45. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/METADATA +2 -1
  46. mcp_mesh-0.8.0b1.dist-info/RECORD +85 -0
  47. mesh/__init__.py +2 -1
  48. mesh/decorators.py +89 -5
  49. _mcp_mesh/generated/.openapi-generator/FILES +0 -50
  50. _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
  51. _mcp_mesh/generated/.openapi-generator-ignore +0 -15
  52. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
  53. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
  54. _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
  55. _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
  56. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
  57. _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
  58. _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
  59. _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
  60. _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
  61. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
  62. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
  63. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
  64. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
  65. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
  66. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
  67. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
  68. _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
  69. _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
  70. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
  71. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
  72. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
  73. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
  74. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
  75. _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
  76. _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
  77. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
  78. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
  79. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
  80. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
  81. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
  82. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
  83. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
  84. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
  85. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
  86. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
  87. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
  88. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
  89. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
  90. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
  91. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
  92. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
  93. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
  94. _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
  95. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
  96. _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
  97. _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
  98. _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
  99. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
  100. _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
  101. _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
  102. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
  103. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
  104. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
  105. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
  106. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
  107. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
  108. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
  109. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
  110. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
  111. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
  112. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
  113. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
  114. _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
  115. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
  116. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
  117. _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
  118. _mcp_mesh/shared/registry_client_wrapper.py +0 -515
  119. mcp_mesh-0.7.21.dist-info/RECORD +0 -152
  120. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/WHEEL +0 -0
  121. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.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.7.21"
34
+ __version__ = "0.8.0b1"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -14,12 +14,10 @@ import weakref
14
14
  from collections.abc import Callable
15
15
  from typing import Any
16
16
 
17
- from ..shared.logging_config import (
18
- format_log_value,
19
- format_result_summary,
20
- get_trace_prefix,
21
- )
22
- from .signature_analyzer import get_mesh_agent_positions, has_llm_agent_parameter
17
+ from ..shared.logging_config import (format_log_value, format_result_summary,
18
+ get_trace_prefix)
19
+ from .signature_analyzer import (get_mesh_agent_positions,
20
+ has_llm_agent_parameter)
23
21
 
24
22
  logger = logging.getLogger(__name__)
25
23
 
@@ -387,8 +387,13 @@ class HttpMcpWrapper:
387
387
  self.logger = logger
388
388
 
389
389
  async def dispatch(self, request: Request, call_next):
390
- # Extract and set trace context from headers for distributed tracing
390
+ # Read body once for processing
391
+ body = await request.body()
392
+ modified_body = body
393
+
394
+ # Extract and set trace context from headers and arguments
391
395
  try:
396
+ from ..tracing.context import TraceContext
392
397
  from ..tracing.trace_context_helper import TraceContextHelper
393
398
 
394
399
  # DEBUG: Log incoming headers for trace propagation debugging
@@ -399,22 +404,67 @@ class HttpMcpWrapper:
399
404
  f"X-Parent-Span={parent_span_header}, path={request.url.path}"
400
405
  )
401
406
 
402
- # Use helper class for trace context extraction and setup
403
- trace_context = (
404
- await TraceContextHelper.extract_trace_context_from_request(
405
- request
407
+ # Extract trace context from both headers AND arguments
408
+ trace_id = trace_id_header
409
+ parent_span = parent_span_header
410
+
411
+ # Try extracting from JSON-RPC body arguments as fallback
412
+ # Also strip trace fields from arguments to avoid Pydantic validation errors
413
+ if body:
414
+ try:
415
+ payload = json.loads(body.decode("utf-8"))
416
+ if payload.get("method") == "tools/call":
417
+ arguments = payload.get("params", {}).get(
418
+ "arguments", {}
419
+ )
420
+
421
+ # Extract trace context from arguments (TypeScript uses _trace_id/_parent_span)
422
+ if not trace_id and arguments.get("_trace_id"):
423
+ trace_id = arguments.get("_trace_id")
424
+ if not parent_span and arguments.get("_parent_span"):
425
+ parent_span = arguments.get("_parent_span")
426
+
427
+ # Strip trace context fields from arguments before passing to FastMCP
428
+ if (
429
+ "_trace_id" in arguments
430
+ or "_parent_span" in arguments
431
+ ):
432
+ arguments.pop("_trace_id", None)
433
+ arguments.pop("_parent_span", None)
434
+ # Update payload with cleaned arguments
435
+ modified_body = json.dumps(payload).encode("utf-8")
436
+ self.logger.debug(
437
+ f"🔗 Stripped trace fields from arguments, "
438
+ f"trace_id={trace_id[:8] if trace_id else None}..."
439
+ )
440
+ except Exception as e:
441
+ self.logger.debug(
442
+ f"Failed to process body for trace context: {e}"
443
+ )
444
+
445
+ # Setup trace context if we have a trace_id
446
+ if trace_id:
447
+ trace_context = {
448
+ "trace_id": trace_id,
449
+ "parent_span": parent_span,
450
+ }
451
+ TraceContextHelper.setup_request_trace_context(
452
+ trace_context, self.logger
406
453
  )
407
- )
408
- TraceContextHelper.setup_request_trace_context(
409
- trace_context, self.logger
410
- )
411
454
  except Exception as e:
412
455
  # Never fail request due to tracing issues
413
456
  self.logger.warning(f"Failed to set trace context: {e}")
414
457
  pass
415
458
 
459
+ # Create a new request scope with the modified body
460
+ async def receive():
461
+ return {"type": "http.request", "body": modified_body}
462
+
463
+ # Update request with modified receive
464
+ request._receive = receive
465
+
416
466
  # Extract session ID from request
417
- session_id = await self.http_wrapper._extract_session_id(request)
467
+ session_id = await self.http_wrapper._extract_session_id_from_body(body)
418
468
 
419
469
  if session_id:
420
470
  # Check for existing session assignment
@@ -458,6 +508,15 @@ class HttpMcpWrapper:
458
508
  # Try extracting from JSON-RPC body
459
509
  try:
460
510
  body = await request.body()
511
+ return await self._extract_session_id_from_body(body)
512
+ except Exception:
513
+ pass
514
+
515
+ return None
516
+
517
+ async def _extract_session_id_from_body(self, body: bytes) -> str:
518
+ """Extract session ID from already-read request body."""
519
+ try:
461
520
  if body:
462
521
  payload = json.loads(body.decode("utf-8"))
463
522
  if payload.get("method") == "tools/call":
@@ -14,12 +14,8 @@ from typing import Any, Dict, List, Literal, Optional, Union
14
14
  from pydantic import BaseModel
15
15
 
16
16
  from .llm_config import LLMConfig
17
- from .llm_errors import (
18
- LLMAPIError,
19
- MaxIterationsError,
20
- ResponseParseError,
21
- ToolExecutionError,
22
- )
17
+ from .llm_errors import (LLMAPIError, MaxIterationsError, ResponseParseError,
18
+ ToolExecutionError)
23
19
  from .provider_handlers import ProviderHandlerRegistry
24
20
  from .response_parser import ResponseParser
25
21
  from .tool_executor import ToolExecutor
@@ -27,7 +23,8 @@ from .tool_schema_builder import ToolSchemaBuilder
27
23
 
28
24
  # Import Jinja2 for template rendering
29
25
  try:
30
- from jinja2 import Environment, FileSystemLoader, Template, TemplateSyntaxError
26
+ from jinja2 import (Environment, FileSystemLoader, Template,
27
+ TemplateSyntaxError)
31
28
  except ImportError:
32
29
  Environment = None
33
30
  FileSystemLoader = None
@@ -431,7 +431,8 @@ class MeshLlmAgentInjector(BaseInjector):
431
431
  if is_template:
432
432
  # Templates enabled - create per-call agent with context
433
433
  # Import signature analyzer for context detection
434
- from .signature_analyzer import get_context_parameter_name
434
+ from .signature_analyzer import \
435
+ get_context_parameter_name
435
436
 
436
437
  # Detect context parameter
437
438
  context_param_name = config_dict.get("context_param")
@@ -5,15 +5,28 @@ This package provides vendor-specific customization for different LLM providers
5
5
  (Claude, OpenAI, Gemini, etc.) to optimize API calls and response handling.
6
6
  """
7
7
 
8
- from .base_provider_handler import BaseProviderHandler
8
+ from .base_provider_handler import (
9
+ BASE_TOOL_INSTRUCTIONS,
10
+ CLAUDE_ANTI_XML_INSTRUCTION,
11
+ BaseProviderHandler,
12
+ make_schema_strict,
13
+ )
9
14
  from .claude_handler import ClaudeHandler
15
+ from .gemini_handler import GeminiHandler
10
16
  from .generic_handler import GenericHandler
11
17
  from .openai_handler import OpenAIHandler
12
18
  from .provider_handler_registry import ProviderHandlerRegistry
13
19
 
14
20
  __all__ = [
21
+ # Constants
22
+ "BASE_TOOL_INSTRUCTIONS",
23
+ "CLAUDE_ANTI_XML_INSTRUCTION",
24
+ # Utilities
25
+ "make_schema_strict",
26
+ # Handlers
15
27
  "BaseProviderHandler",
16
28
  "ClaudeHandler",
29
+ "GeminiHandler",
17
30
  "OpenAIHandler",
18
31
  "GenericHandler",
19
32
  "ProviderHandlerRegistry",
@@ -5,11 +5,117 @@ This module defines the abstract base class for provider-specific handlers
5
5
  that customize how different LLM vendors (Claude, OpenAI, Gemini, etc.) are called.
6
6
  """
7
7
 
8
+ import copy
8
9
  from abc import ABC, abstractmethod
9
- from typing import Any, Dict, List, Optional
10
+ from typing import Any, Optional
10
11
 
11
12
  from pydantic import BaseModel
12
13
 
14
+ # ============================================================================
15
+ # Shared Constants
16
+ # ============================================================================
17
+
18
+ # Base tool calling instructions shared across all providers.
19
+ # Claude handler adds anti-XML instruction on top of this.
20
+ BASE_TOOL_INSTRUCTIONS = """
21
+ IMPORTANT TOOL CALLING RULES:
22
+ - You have access to tools that you can call to gather information
23
+ - Make ONE tool call at a time
24
+ - After receiving tool results, you can make additional calls if needed
25
+ - Once you have all needed information, provide your final response
26
+ """
27
+
28
+ # Anti-XML instruction for Claude (prevents <invoke> style tool calls).
29
+ CLAUDE_ANTI_XML_INSTRUCTION = (
30
+ '- NEVER use XML-style syntax like <invoke name="tool_name"/>'
31
+ )
32
+
33
+
34
+ # ============================================================================
35
+ # Shared Schema Utilities
36
+ # ============================================================================
37
+
38
+
39
+ def make_schema_strict(
40
+ schema: dict[str, Any],
41
+ add_all_required: bool = True,
42
+ ) -> dict[str, Any]:
43
+ """
44
+ Make a JSON schema strict for structured output.
45
+
46
+ This is a shared utility used by OpenAI, Gemini, and Claude handlers.
47
+ Adds additionalProperties: false to all object types and optionally
48
+ ensures 'required' includes all property keys.
49
+
50
+ Args:
51
+ schema: JSON schema to make strict
52
+ add_all_required: If True, set 'required' to include ALL property keys.
53
+ OpenAI and Gemini require this; Claude does not.
54
+ Default: True
55
+
56
+ Returns:
57
+ New schema with strict constraints (original not mutated)
58
+ """
59
+ result = copy.deepcopy(schema)
60
+ _add_strict_constraints_recursive(result, add_all_required)
61
+ return result
62
+
63
+
64
+ def _add_strict_constraints_recursive(obj: Any, add_all_required: bool) -> None:
65
+ """
66
+ Recursively add strict constraints to a schema object.
67
+
68
+ Args:
69
+ obj: Schema object to process (mutated in place)
70
+ add_all_required: Whether to set required to all property keys
71
+ """
72
+ if not isinstance(obj, dict):
73
+ return
74
+
75
+ # If this is an object type, add additionalProperties: false
76
+ if obj.get("type") == "object":
77
+ obj["additionalProperties"] = False
78
+
79
+ # Optionally set required to include all property keys
80
+ if add_all_required and "properties" in obj:
81
+ obj["required"] = list(obj["properties"].keys())
82
+
83
+ # Process $defs (Pydantic uses this for nested models)
84
+ if "$defs" in obj:
85
+ for def_schema in obj["$defs"].values():
86
+ _add_strict_constraints_recursive(def_schema, add_all_required)
87
+
88
+ # Process properties
89
+ if "properties" in obj:
90
+ for prop_schema in obj["properties"].values():
91
+ _add_strict_constraints_recursive(prop_schema, add_all_required)
92
+
93
+ # Process items (for arrays)
94
+ # items can be an object (single schema) or a list (tuple validation in older drafts)
95
+ if "items" in obj:
96
+ items = obj["items"]
97
+ if isinstance(items, dict):
98
+ _add_strict_constraints_recursive(items, add_all_required)
99
+ elif isinstance(items, list):
100
+ for item in items:
101
+ _add_strict_constraints_recursive(item, add_all_required)
102
+
103
+ # Process prefixItems (tuple validation in JSON Schema draft 2020-12)
104
+ if "prefixItems" in obj:
105
+ for item in obj["prefixItems"]:
106
+ _add_strict_constraints_recursive(item, add_all_required)
107
+
108
+ # Process anyOf, oneOf, allOf
109
+ for key in ("anyOf", "oneOf", "allOf"):
110
+ if key in obj:
111
+ for item in obj[key]:
112
+ _add_strict_constraints_recursive(item, add_all_required)
113
+
114
+
115
+ # ============================================================================
116
+ # Base Provider Handler
117
+ # ============================================================================
118
+
13
119
 
14
120
  class BaseProviderHandler(ABC):
15
121
  """
@@ -43,11 +149,11 @@ class BaseProviderHandler(ABC):
43
149
  @abstractmethod
44
150
  def prepare_request(
45
151
  self,
46
- messages: List[Dict[str, Any]],
47
- tools: Optional[List[Dict[str, Any]]],
152
+ messages: list[dict[str, Any]],
153
+ tools: Optional[list[dict[str, Any]]],
48
154
  output_type: type[BaseModel],
49
- **kwargs: Any
50
- ) -> Dict[str, Any]:
155
+ **kwargs: Any,
156
+ ) -> dict[str, Any]:
51
157
  """
52
158
  Prepare vendor-specific request parameters.
53
159
 
@@ -74,8 +180,8 @@ class BaseProviderHandler(ABC):
74
180
  def format_system_prompt(
75
181
  self,
76
182
  base_prompt: str,
77
- tool_schemas: Optional[List[Dict[str, Any]]],
78
- output_type: type[BaseModel]
183
+ tool_schemas: Optional[list[dict[str, Any]]],
184
+ output_type: type[BaseModel],
79
185
  ) -> str:
80
186
  """
81
187
  Format system prompt for vendor-specific requirements.
@@ -95,7 +201,7 @@ class BaseProviderHandler(ABC):
95
201
  """
96
202
  pass
97
203
 
98
- def get_vendor_capabilities(self) -> Dict[str, bool]:
204
+ def get_vendor_capabilities(self) -> dict[str, bool]:
99
205
  """
100
206
  Return vendor-specific capability flags.
101
207
 
@@ -17,11 +17,16 @@ Features:
17
17
 
18
18
  import json
19
19
  import logging
20
- from typing import Any, Optional, get_args, get_origin
20
+ from typing import Any, Optional
21
21
 
22
22
  from pydantic import BaseModel
23
23
 
24
- from .base_provider_handler import BaseProviderHandler
24
+ from .base_provider_handler import (
25
+ BASE_TOOL_INSTRUCTIONS,
26
+ BaseProviderHandler,
27
+ CLAUDE_ANTI_XML_INSTRUCTION,
28
+ make_schema_strict,
29
+ )
25
30
 
26
31
  logger = logging.getLogger(__name__)
27
32
 
@@ -141,51 +146,6 @@ class ClaudeHandler(BaseProviderHandler):
141
146
  # Default to strict for unknown types
142
147
  return OUTPUT_MODE_STRICT
143
148
 
144
- def _make_schema_strict(self, schema: dict[str, Any]) -> dict[str, Any]:
145
- """
146
- Make a JSON schema strict for Claude's structured output.
147
-
148
- Claude requires additionalProperties: false on all object types.
149
- This recursively processes the schema to add this constraint.
150
-
151
- Args:
152
- schema: JSON schema dict
153
-
154
- Returns:
155
- Schema with additionalProperties: false on all objects
156
- """
157
- if not isinstance(schema, dict):
158
- return schema
159
-
160
- result = schema.copy()
161
-
162
- # If this is an object type, add additionalProperties: false
163
- if result.get("type") == "object":
164
- result["additionalProperties"] = False
165
-
166
- # Recursively process nested schemas
167
- if "properties" in result:
168
- result["properties"] = {
169
- k: self._make_schema_strict(v) for k, v in result["properties"].items()
170
- }
171
-
172
- # Process $defs (Pydantic uses this for nested models)
173
- if "$defs" in result:
174
- result["$defs"] = {
175
- k: self._make_schema_strict(v) for k, v in result["$defs"].items()
176
- }
177
-
178
- # Process items for arrays
179
- if "items" in result:
180
- result["items"] = self._make_schema_strict(result["items"])
181
-
182
- # Process anyOf, oneOf, allOf
183
- for key in ["anyOf", "oneOf", "allOf"]:
184
- if key in result:
185
- result[key] = [self._make_schema_strict(s) for s in result[key]]
186
-
187
- return result
188
-
189
149
  def _apply_prompt_caching(
190
150
  self, messages: list[dict[str, Any]]
191
151
  ) -> list[dict[str, Any]]:
@@ -302,9 +262,10 @@ class ClaudeHandler(BaseProviderHandler):
302
262
  # Only add response_format in "strict" mode
303
263
  if determined_mode == OUTPUT_MODE_STRICT:
304
264
  # Claude requires additionalProperties: false on all object types
265
+ # Unlike OpenAI/Gemini, Claude doesn't require all properties in 'required'
305
266
  if isinstance(output_type, type) and issubclass(output_type, BaseModel):
306
267
  schema = output_type.model_json_schema()
307
- strict_schema = self._make_schema_strict(schema)
268
+ strict_schema = make_schema_strict(schema, add_all_required=False)
308
269
  request_params["response_format"] = {
309
270
  "type": "json_schema",
310
271
  "json_schema": {
@@ -346,15 +307,12 @@ class ClaudeHandler(BaseProviderHandler):
346
307
  # Add tool calling instructions if tools available
347
308
  # These prevent Claude from using XML-style <invoke> syntax
348
309
  if tool_schemas:
349
- system_content += """
350
-
351
- IMPORTANT TOOL CALLING RULES:
352
- - You have access to tools that you can call to gather information
353
- - Make ONE tool call at a time
354
- - NEVER use XML-style syntax like <invoke name="tool_name"/>
355
- - After receiving tool results, you can make additional calls if needed
356
- - Once you have all needed information, provide your final response
357
- """
310
+ # Use base instructions but insert anti-XML rule for Claude
311
+ instructions = BASE_TOOL_INSTRUCTIONS.replace(
312
+ "- Make ONE tool call at a time",
313
+ f"- Make ONE tool call at a time\n{CLAUDE_ANTI_XML_INSTRUCTION}",
314
+ )
315
+ system_content += instructions
358
316
 
359
317
  # Add output format instructions based on mode
360
318
  if determined_mode == OUTPUT_MODE_TEXT:
@@ -0,0 +1,181 @@
1
+ """
2
+ Gemini/Google provider handler.
3
+
4
+ Optimized for Gemini models (Gemini 2.0 Flash, Gemini 1.5 Pro, etc.)
5
+ using Google's best practices for tool calling and structured output.
6
+
7
+ Features:
8
+ - Native structured output via response_format (similar to OpenAI)
9
+ - Native function calling support
10
+ - Support for Gemini 2.x and 3.x models
11
+ - Large context windows (up to 2M tokens)
12
+
13
+ Reference:
14
+ - https://docs.litellm.ai/docs/providers/gemini
15
+ - https://ai.google.dev/gemini-api/docs
16
+ """
17
+
18
+ import logging
19
+ from typing import Any, Optional
20
+
21
+ from pydantic import BaseModel
22
+
23
+ from .base_provider_handler import (
24
+ BASE_TOOL_INSTRUCTIONS,
25
+ BaseProviderHandler,
26
+ make_schema_strict,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class GeminiHandler(BaseProviderHandler):
33
+ """
34
+ Provider handler for Google Gemini models.
35
+
36
+ Gemini Characteristics:
37
+ - Native structured output via response_format parameter (LiteLLM translates)
38
+ - Native function calling support
39
+ - Large context windows (1M-2M tokens)
40
+ - Multimodal support (text, images, video, audio)
41
+ - Works well with concise, focused prompts
42
+
43
+ Key Similarities with OpenAI:
44
+ - Uses response_format for structured output (via LiteLLM translation)
45
+ - Native function calling format
46
+ - Similar schema enforcement requirements
47
+
48
+ Supported Models (via LiteLLM):
49
+ - gemini/gemini-2.0-flash (fast, efficient)
50
+ - gemini/gemini-2.0-flash-lite (fastest, most efficient)
51
+ - gemini/gemini-1.5-pro (high capability)
52
+ - gemini/gemini-1.5-flash (balanced)
53
+ - gemini/gemini-3-flash-preview (reasoning support)
54
+ - gemini/gemini-3-pro-preview (advanced reasoning)
55
+
56
+ Reference:
57
+ https://docs.litellm.ai/docs/providers/gemini
58
+ """
59
+
60
+ def __init__(self):
61
+ """Initialize Gemini handler."""
62
+ super().__init__(vendor="gemini")
63
+
64
+ def prepare_request(
65
+ self,
66
+ messages: list[dict[str, Any]],
67
+ tools: Optional[list[dict[str, Any]]],
68
+ output_type: type,
69
+ **kwargs: Any,
70
+ ) -> dict[str, Any]:
71
+ """
72
+ Prepare request parameters for Gemini API via LiteLLM.
73
+
74
+ Gemini Strategy:
75
+ - Use response_format parameter for structured JSON output
76
+ - LiteLLM handles translation to Gemini's native format
77
+ - Skip structured output for str return types (text mode)
78
+
79
+ Args:
80
+ messages: List of message dicts
81
+ tools: Optional list of tool schemas
82
+ output_type: Return type (str or Pydantic model)
83
+ **kwargs: Additional model parameters
84
+
85
+ Returns:
86
+ Dictionary of parameters for litellm.completion()
87
+ """
88
+ # Build base request
89
+ request_params = {
90
+ "messages": messages,
91
+ **kwargs, # Pass through temperature, max_tokens, etc.
92
+ }
93
+
94
+ # Add tools if provided
95
+ # LiteLLM will convert OpenAI tool format to Gemini's function_declarations
96
+ if tools:
97
+ request_params["tools"] = tools
98
+
99
+ # Skip structured output for str return type (text mode)
100
+ if output_type is str:
101
+ return request_params
102
+
103
+ # Only add response_format for Pydantic models
104
+ if not (isinstance(output_type, type) and issubclass(output_type, BaseModel)):
105
+ return request_params
106
+
107
+ # Add response_format for structured output
108
+ # LiteLLM translates this to Gemini's native format
109
+ schema = output_type.model_json_schema()
110
+
111
+ # Transform schema for strict mode compliance
112
+ # Gemini requires additionalProperties: false and all properties in required
113
+ schema = make_schema_strict(schema, add_all_required=True)
114
+
115
+ # Gemini structured output format (via LiteLLM)
116
+ request_params["response_format"] = {
117
+ "type": "json_schema",
118
+ "json_schema": {
119
+ "name": output_type.__name__,
120
+ "schema": schema,
121
+ "strict": True, # Enforce schema compliance
122
+ },
123
+ }
124
+
125
+ return request_params
126
+
127
+ def format_system_prompt(
128
+ self,
129
+ base_prompt: str,
130
+ tool_schemas: Optional[list[dict[str, Any]]],
131
+ output_type: type,
132
+ ) -> str:
133
+ """
134
+ Format system prompt for Gemini (concise approach).
135
+
136
+ Gemini Strategy:
137
+ 1. Use base prompt as-is
138
+ 2. Add tool calling instructions if tools present
139
+ 3. Minimal JSON instructions (response_format handles structure)
140
+ 4. Keep prompt concise - Gemini works well with clear, direct prompts
141
+
142
+ Args:
143
+ base_prompt: Base system prompt
144
+ tool_schemas: Optional tool schemas
145
+ output_type: Expected response type (str or Pydantic model)
146
+
147
+ Returns:
148
+ Formatted system prompt optimized for Gemini
149
+ """
150
+ system_content = base_prompt
151
+
152
+ # Add tool calling instructions if tools available
153
+ if tool_schemas:
154
+ system_content += BASE_TOOL_INSTRUCTIONS
155
+
156
+ # Skip JSON note for str return type (text mode)
157
+ if output_type is str:
158
+ return system_content
159
+
160
+ # Add brief JSON note (response_format handles enforcement)
161
+ if isinstance(output_type, type) and issubclass(output_type, BaseModel):
162
+ system_content += f"\n\nYour final response will be structured as JSON matching the {output_type.__name__} format."
163
+
164
+ return system_content
165
+
166
+ def get_vendor_capabilities(self) -> dict[str, bool]:
167
+ """
168
+ Return Gemini-specific capabilities.
169
+
170
+ Returns:
171
+ Capability flags for Gemini
172
+ """
173
+ return {
174
+ "native_tool_calling": True, # Gemini has native function calling
175
+ "structured_output": True, # Supports structured output via response_format
176
+ "streaming": True, # Supports streaming
177
+ "vision": True, # Gemini supports multimodal (images, video, audio)
178
+ "json_mode": True, # Native JSON mode via response_format
179
+ "large_context": True, # Up to 2M tokens context window
180
+ }
181
+