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.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/engine/dependency_injector.py +4 -6
- _mcp_mesh/engine/http_wrapper.py +69 -10
- _mcp_mesh/engine/mesh_llm_agent.py +4 -7
- _mcp_mesh/engine/mesh_llm_agent_injector.py +2 -1
- _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
- _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
- _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
- _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
- _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
- _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
- _mcp_mesh/engine/response_parser.py +61 -15
- _mcp_mesh/engine/unified_mcp_proxy.py +18 -34
- _mcp_mesh/pipeline/__init__.py +9 -20
- _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
- _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
- _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +425 -0
- _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
- _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
- _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
- _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
- _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
- _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
- _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +695 -0
- _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
- _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +5 -6
- _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +21 -9
- _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
- _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
- _mcp_mesh/reload.py +1 -3
- _mcp_mesh/shared/__init__.py +2 -8
- _mcp_mesh/shared/config_resolver.py +124 -80
- _mcp_mesh/shared/defaults.py +89 -14
- _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
- _mcp_mesh/shared/host_resolver.py +8 -46
- _mcp_mesh/shared/server_discovery.py +115 -86
- _mcp_mesh/shared/simple_shutdown.py +44 -86
- _mcp_mesh/tracing/execution_tracer.py +2 -6
- _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
- _mcp_mesh/tracing/trace_context_helper.py +3 -13
- _mcp_mesh/tracing/utils.py +29 -15
- _mcp_mesh/utils/fastmcp_schema_extractor.py +2 -1
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/METADATA +2 -1
- mcp_mesh-0.8.0b1.dist-info/RECORD +85 -0
- mesh/__init__.py +2 -1
- mesh/decorators.py +89 -5
- _mcp_mesh/generated/.openapi-generator/FILES +0 -50
- _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
- _mcp_mesh/generated/.openapi-generator-ignore +0 -15
- _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
- _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
- _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
- _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
- _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
- _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
- _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
- _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
- _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
- _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
- _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
- _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
- _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
- _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
- _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
- _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
- _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
- _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
- _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
- _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
- _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
- _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
- _mcp_mesh/shared/registry_client_wrapper.py +0 -515
- mcp_mesh-0.7.21.dist-info/RECORD +0 -152
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
_mcp_mesh/engine/http_wrapper.py
CHANGED
|
@@ -387,8 +387,13 @@ class HttpMcpWrapper:
|
|
|
387
387
|
self.logger = logger
|
|
388
388
|
|
|
389
389
|
async def dispatch(self, request: Request, call_next):
|
|
390
|
-
#
|
|
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
|
-
#
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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:
|
|
47
|
-
tools: Optional[
|
|
152
|
+
messages: list[dict[str, Any]],
|
|
153
|
+
tools: Optional[list[dict[str, Any]]],
|
|
48
154
|
output_type: type[BaseModel],
|
|
49
|
-
**kwargs: Any
|
|
50
|
-
) ->
|
|
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[
|
|
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) ->
|
|
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
|
|
20
|
+
from typing import Any, Optional
|
|
21
21
|
|
|
22
22
|
from pydantic import BaseModel
|
|
23
23
|
|
|
24
|
-
from .base_provider_handler import
|
|
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 =
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
|