mcp-mesh 0.8.1__tar.gz → 0.9.0b2__tar.gz

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 (87) hide show
  1. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/.gitignore +9 -1
  2. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/PKG-INFO +2 -2
  3. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/__init__.py +1 -1
  4. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/mesh_llm_agent.py +17 -10
  5. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/mesh_llm_agent_injector.py +27 -10
  6. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/provider_handlers/__init__.py +2 -0
  7. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/provider_handlers/base_provider_handler.py +177 -0
  8. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/provider_handlers/claude_handler.py +129 -102
  9. mcp_mesh-0.9.0b2/_mcp_mesh/engine/provider_handlers/gemini_handler.py +351 -0
  10. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/provider_handlers/generic_handler.py +31 -0
  11. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/provider_handlers/openai_handler.py +4 -1
  12. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +18 -0
  13. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/tracing/trace_context_helper.py +5 -3
  14. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/mesh/helpers.py +39 -46
  15. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/pyproject.toml +5 -5
  16. mcp_mesh-0.8.1/_mcp_mesh/engine/provider_handlers/gemini_handler.py +0 -181
  17. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/LICENSE +0 -0
  18. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/README.md +0 -0
  19. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/__init__.py +0 -0
  20. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/async_mcp_client.py +0 -0
  21. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/base_injector.py +0 -0
  22. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/decorator_registry.py +0 -0
  23. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/dependency_injector.py +0 -0
  24. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/http_wrapper.py +0 -0
  25. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/llm_config.py +0 -0
  26. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/llm_errors.py +0 -0
  27. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/provider_handlers/provider_handler_registry.py +0 -0
  28. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/response_parser.py +0 -0
  29. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/self_dependency_proxy.py +0 -0
  30. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/session_aware_client.py +0 -0
  31. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/session_manager.py +0 -0
  32. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/signature_analyzer.py +0 -0
  33. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/tool_executor.py +0 -0
  34. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/tool_schema_builder.py +0 -0
  35. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/engine/unified_mcp_proxy.py +0 -0
  36. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/__init__.py +0 -0
  37. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/api_heartbeat/__init__.py +0 -0
  38. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +0 -0
  39. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +0 -0
  40. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/api_startup/__init__.py +0 -0
  41. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/api_startup/api_pipeline.py +0 -0
  42. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/api_startup/api_server_setup.py +0 -0
  43. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/api_startup/fastapi_discovery.py +0 -0
  44. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/api_startup/middleware_integration.py +0 -0
  45. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/api_startup/route_collection.py +0 -0
  46. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/api_startup/route_integration.py +0 -0
  47. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_heartbeat/__init__.py +0 -0
  48. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/__init__.py +0 -0
  49. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/configuration.py +0 -0
  50. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/decorator_collection.py +0 -0
  51. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +0 -0
  52. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +0 -0
  53. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +0 -0
  54. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +0 -0
  55. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/lifespan_factory.py +0 -0
  56. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/server_discovery.py +0 -0
  57. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +0 -0
  58. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +0 -0
  59. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/shared/__init__.py +0 -0
  60. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/shared/base_step.py +0 -0
  61. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -0
  62. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/pipeline/shared/pipeline_types.py +0 -0
  63. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/reload.py +0 -0
  64. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/reload_runner.py +0 -0
  65. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/__init__.py +0 -0
  66. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/config_resolver.py +0 -0
  67. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/content_extractor.py +0 -0
  68. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/defaults.py +0 -0
  69. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/fast_heartbeat_status.py +0 -0
  70. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/fastapi_middleware_manager.py +0 -0
  71. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/health_check_manager.py +0 -0
  72. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/host_resolver.py +0 -0
  73. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/logging_config.py +0 -0
  74. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/server_discovery.py +0 -0
  75. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/simple_shutdown.py +0 -0
  76. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/sse_parser.py +0 -0
  77. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/shared/support_types.py +0 -0
  78. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/tracing/agent_context_helper.py +0 -0
  79. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/tracing/context.py +0 -0
  80. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/tracing/execution_tracer.py +0 -0
  81. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/tracing/fastapi_tracing_middleware.py +0 -0
  82. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/tracing/redis_metadata_publisher.py +0 -0
  83. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/tracing/utils.py +0 -0
  84. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/_mcp_mesh/utils/fastmcp_schema_extractor.py +0 -0
  85. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/mesh/__init__.py +0 -0
  86. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/mesh/decorators.py +0 -0
  87. {mcp_mesh-0.8.1 → mcp_mesh-0.9.0b2}/mesh/types.py +0 -0
@@ -120,7 +120,7 @@ celerybeat.pid
120
120
  *.sage.py
121
121
 
122
122
  # Environments
123
- .env
123
+ **/.env
124
124
  .venv
125
125
  env/
126
126
  venv/
@@ -163,6 +163,12 @@ cython_debug/
163
163
  # VS Code
164
164
  .vscode/
165
165
 
166
+ # Eclipse / Maven IDE files
167
+ .classpath
168
+ .project
169
+ .settings/
170
+ .factorypath
171
+
166
172
  # Other IDEs/Tools
167
173
  .emigo_repomap/
168
174
  .windsurf/
@@ -175,6 +181,7 @@ logs/
175
181
  .logs/
176
182
  capability_store/
177
183
  prompts/
184
+ !examples/**/prompts/
178
185
  *.db
179
186
  *.pid
180
187
 
@@ -255,6 +262,7 @@ rust-core-implementation.org
255
262
  src/runtime/core/index.js
256
263
  src/runtime/core/index.d.ts
257
264
  test/
265
+ !src/runtime/java/**/src/test/
258
266
  scaffold-test/
259
267
 
260
268
  # Test suite build outputs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-mesh
3
- Version: 0.8.1
3
+ Version: 0.9.0b2
4
4
  Summary: Kubernetes-native platform for distributed MCP applications
5
5
  Project-URL: Homepage, https://github.com/dhyansraj/mcp-mesh
6
6
  Project-URL: Documentation, https://github.com/dhyansraj/mcp-mesh/tree/main/docs
@@ -32,7 +32,7 @@ Requires-Dist: fastmcp<3.0.0,>=2.8.0
32
32
  Requires-Dist: httpx<1.0.0,>=0.25.0
33
33
  Requires-Dist: jinja2>=3.1.0
34
34
  Requires-Dist: litellm>=1.30.0
35
- Requires-Dist: mcp-mesh-core>=0.8.1
35
+ Requires-Dist: mcp-mesh-core>=0.9.0b2
36
36
  Requires-Dist: mcp<2.0.0,>=1.9.0
37
37
  Requires-Dist: prometheus-client<1.0.0,>=0.19.0
38
38
  Requires-Dist: pydantic<3.0.0,>=2.4.0
@@ -31,7 +31,7 @@ from .engine.decorator_registry import (
31
31
  get_decorator_stats,
32
32
  )
33
33
 
34
- __version__ = "0.8.1"
34
+ __version__ = "0.9.0b2"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -9,7 +9,7 @@ import json
9
9
  import logging
10
10
  import time
11
11
  from pathlib import Path
12
- from typing import Any, Dict, List, Literal, Optional, Union
12
+ from typing import Any, Literal, Optional, Union
13
13
 
14
14
  from pydantic import BaseModel
15
15
 
@@ -539,8 +539,8 @@ IMPORTANT TOOL CALLING RULES:
539
539
 
540
540
  logger.debug(
541
541
  f"📥 Received response from mesh provider: "
542
- f"content={message_dict.get('content', '')[:200]}..., "
543
- f"tool_calls={len(message_dict.get('tool_calls', []))}"
542
+ f"content={(message_dict.get('content') or '')[:200]}..., "
543
+ f"tool_calls={len(message_dict.get('tool_calls') or [])}"
544
544
  )
545
545
 
546
546
  return MockResponse(message_dict)
@@ -618,13 +618,20 @@ IMPORTANT TOOL CALLING RULES:
618
618
  # Render base system prompt (from template or literal) with effective context
619
619
  base_system_prompt = self._render_system_prompt(effective_context)
620
620
 
621
- # Phase 2: Use provider handler to format system prompt
622
- # This allows vendor-specific optimizations (e.g., OpenAI skips JSON instructions)
623
- system_content = self._provider_handler.format_system_prompt(
624
- base_prompt=base_system_prompt,
625
- tool_schemas=self._tool_schemas,
626
- output_type=self.output_type,
627
- )
621
+ # Phase 2: Format system prompt
622
+ if self._is_mesh_delegated:
623
+ # Delegate path: Just use base prompt + basic tool instructions
624
+ # Provider will add vendor-specific formatting
625
+ system_content = base_system_prompt
626
+ if self._tool_schemas:
627
+ system_content += "\n\nYou have access to tools. Use them when needed to gather information."
628
+ else:
629
+ # Direct path: Use vendor handler for vendor-specific optimizations
630
+ system_content = self._provider_handler.format_system_prompt(
631
+ base_prompt=base_system_prompt,
632
+ tool_schemas=self._tool_schemas,
633
+ output_type=self.output_type,
634
+ )
628
635
 
629
636
  # Debug: Log system prompt (truncated for privacy)
630
637
  logger.debug(
@@ -123,11 +123,14 @@ class MeshLlmAgentInjector(BaseInjector):
123
123
 
124
124
  # Set factory for per-call context agent creation (template support)
125
125
  if config.get("is_template", False):
126
+
126
127
  def create_context_agent(
127
128
  context_value: Any, _func_id: str = function_id
128
129
  ) -> MeshLlmAgent:
129
130
  """Factory to create MeshLlmAgent with context for template rendering."""
130
- return self._create_llm_agent(_func_id, context_value=context_value)
131
+ return self._create_llm_agent(
132
+ _func_id, context_value=context_value
133
+ )
131
134
 
132
135
  wrapper._mesh_create_context_agent = create_context_agent
133
136
  logger.info(
@@ -262,17 +265,25 @@ class MeshLlmAgentInjector(BaseInjector):
262
265
  filter_config = llm_metadata.config.get("filter")
263
266
  has_filter = filter_config is not None and len(filter_config) > 0
264
267
 
268
+ # CRITICAL FIX: Always set config if not already set (prevents KeyError during race condition)
269
+ # When provider arrives before tools, config must be available for inject_llm_agent
270
+ # to create the LLM agent with context. Without this, _create_llm_agent fails with KeyError.
271
+ if "config" not in self._llm_agents[function_id] and llm_metadata:
272
+ self._llm_agents[function_id]["config"] = llm_metadata.config
273
+ self._llm_agents[function_id]["output_type"] = llm_metadata.output_type
274
+ self._llm_agents[function_id]["param_name"] = llm_metadata.param_name
275
+ self._llm_agents[function_id]["function"] = llm_metadata.function
276
+ logger.debug(
277
+ f"📋 Set config from DecoratorRegistry for '{function_id}' (awaiting tools)"
278
+ )
279
+
265
280
  # If no filter specified, initialize empty tools data so we can create LLM agent without tools
266
281
  # This supports simple LLM calls (text generation) that don't need tool calling
267
282
  if not has_filter and "tools_metadata" not in self._llm_agents[function_id]:
268
283
  self._llm_agents[function_id].update(
269
284
  {
270
- "config": llm_metadata.config if llm_metadata else {},
271
- "output_type": llm_metadata.output_type if llm_metadata else None,
272
- "param_name": llm_metadata.param_name if llm_metadata else "llm",
273
285
  "tools_metadata": [], # No tools for simple LLM calls
274
286
  "tools_proxies": {}, # No tool proxies needed
275
- "function": llm_metadata.function if llm_metadata else None,
276
287
  }
277
288
  )
278
289
  logger.info(
@@ -568,7 +579,9 @@ class MeshLlmAgentInjector(BaseInjector):
568
579
  )
569
580
 
570
581
  # Check if templates are enabled
571
- is_template = config_dict.get("is_template", False) if config_dict else False
582
+ is_template = (
583
+ config_dict.get("is_template", False) if config_dict else False
584
+ )
572
585
 
573
586
  if is_template and config_dict:
574
587
  # Templates enabled - create per-call agent with context
@@ -739,13 +752,17 @@ class MeshLlmAgentInjector(BaseInjector):
739
752
  )
740
753
 
741
754
  # Create MeshLlmAgent with both metadata and proxies
755
+ # Use .get() with defaults for tools_metadata/proxies to handle race condition
756
+ # where provider arrives before tools (filter-based agents)
742
757
  llm_agent = MeshLlmAgent(
743
758
  config=llm_config,
744
- filtered_tools=llm_agent_data[
745
- "tools_metadata"
746
- ], # Metadata for schema building
759
+ filtered_tools=llm_agent_data.get(
760
+ "tools_metadata", []
761
+ ), # Metadata for schema building (empty if tools not yet received)
747
762
  output_type=llm_agent_data["output_type"],
748
- tool_proxies=llm_agent_data["tools_proxies"], # Proxies for execution
763
+ tool_proxies=llm_agent_data.get(
764
+ "tools_proxies", {}
765
+ ), # Proxies for execution (empty if tools not yet received)
749
766
  template_path=template_path if is_template else None,
750
767
  context_value=context_value if is_template else None,
751
768
  provider_proxy=llm_agent_data.get(
@@ -9,6 +9,7 @@ from .base_provider_handler import (
9
9
  BASE_TOOL_INSTRUCTIONS,
10
10
  CLAUDE_ANTI_XML_INSTRUCTION,
11
11
  BaseProviderHandler,
12
+ is_simple_schema,
12
13
  make_schema_strict,
13
14
  )
14
15
  from .claude_handler import ClaudeHandler
@@ -22,6 +23,7 @@ __all__ = [
22
23
  "BASE_TOOL_INSTRUCTIONS",
23
24
  "CLAUDE_ANTI_XML_INSTRUCTION",
24
25
  # Utilities
26
+ "is_simple_schema",
25
27
  "make_schema_strict",
26
28
  # Handlers
27
29
  "BaseProviderHandler",
@@ -6,9 +6,12 @@ that customize how different LLM vendors (Claude, OpenAI, Gemini, etc.) are call
6
6
  """
7
7
 
8
8
  import copy
9
+ import logging
9
10
  from abc import ABC, abstractmethod
10
11
  from typing import Any, Optional
11
12
 
13
+ logger = logging.getLogger(__name__)
14
+
12
15
  from pydantic import BaseModel
13
16
 
14
17
  # ============================================================================
@@ -61,6 +64,144 @@ def make_schema_strict(
61
64
  return result
62
65
 
63
66
 
67
+ def is_simple_schema(schema: dict[str, Any]) -> bool:
68
+ """
69
+ Check if a JSON schema is simple enough for hint mode.
70
+
71
+ Simple schema criteria:
72
+ - Less than 5 fields
73
+ - All fields are basic types (str, int, float, bool, list)
74
+ - No nested Pydantic models ($ref or nested objects with properties)
75
+
76
+ This is used by provider handlers to decide between hint mode
77
+ (prompt-based JSON instructions) and strict mode (response_format).
78
+
79
+ Args:
80
+ schema: JSON schema dict
81
+
82
+ Returns:
83
+ True if schema is simple, False otherwise
84
+ """
85
+ try:
86
+ properties = schema.get("properties", {})
87
+
88
+ # Check field count
89
+ if len(properties) >= 5:
90
+ return False
91
+
92
+ # Check for nested objects or complex types
93
+ for field_name, field_schema in properties.items():
94
+ field_type = field_schema.get("type")
95
+
96
+ # Check for nested objects (indicates nested Pydantic model)
97
+ if field_type == "object" and "properties" in field_schema:
98
+ return False
99
+
100
+ # Check for $ref (nested model reference)
101
+ if "$ref" in field_schema:
102
+ return False
103
+
104
+ # Check array items for complex types
105
+ if field_type == "array":
106
+ items = field_schema.get("items", {})
107
+ if items.get("type") == "object" or "$ref" in items:
108
+ return False
109
+
110
+ return True
111
+ except Exception:
112
+ return False
113
+
114
+
115
+ def sanitize_schema_for_structured_output(schema: dict[str, Any]) -> dict[str, Any]:
116
+ """
117
+ Sanitize a JSON schema by removing validation keywords unsupported by LLM APIs.
118
+
119
+ LLM structured output APIs (Claude, OpenAI, Gemini) typically only support
120
+ the structural parts of JSON Schema, not validation constraints. This function
121
+ removes unsupported keywords to ensure uniform behavior across all providers.
122
+
123
+ Removed keywords:
124
+ - minimum, maximum (number range)
125
+ - exclusiveMinimum, exclusiveMaximum (exclusive bounds)
126
+ - minLength, maxLength (string length)
127
+ - minItems, maxItems (array size)
128
+ - pattern (regex validation)
129
+ - multipleOf (divisibility)
130
+
131
+ Args:
132
+ schema: JSON schema dict (will not be mutated)
133
+
134
+ Returns:
135
+ New schema with unsupported validation keywords removed
136
+ """
137
+ result = copy.deepcopy(schema)
138
+ _strip_unsupported_keywords_recursive(result)
139
+ logger.debug(
140
+ "Sanitized schema for structured output (removed validation-only keywords)"
141
+ )
142
+ return result
143
+
144
+
145
+ # Keywords that are validation-only and not supported by LLM structured output APIs
146
+ _UNSUPPORTED_SCHEMA_KEYWORDS = {
147
+ "minimum",
148
+ "maximum",
149
+ "exclusiveMinimum",
150
+ "exclusiveMaximum",
151
+ "minLength",
152
+ "maxLength",
153
+ "minItems",
154
+ "maxItems",
155
+ "pattern",
156
+ "multipleOf",
157
+ }
158
+
159
+
160
+ def _strip_unsupported_keywords_recursive(obj: Any) -> None:
161
+ """
162
+ Recursively strip unsupported validation keywords from a schema object.
163
+
164
+ Args:
165
+ obj: Schema object to process (mutated in place)
166
+ """
167
+ if not isinstance(obj, dict):
168
+ return
169
+
170
+ # Remove unsupported keywords at this level
171
+ for keyword in _UNSUPPORTED_SCHEMA_KEYWORDS:
172
+ obj.pop(keyword, None)
173
+
174
+ # Process $defs (Pydantic uses this for nested models)
175
+ if "$defs" in obj:
176
+ for def_schema in obj["$defs"].values():
177
+ _strip_unsupported_keywords_recursive(def_schema)
178
+
179
+ # Process properties
180
+ if "properties" in obj:
181
+ for prop_schema in obj["properties"].values():
182
+ _strip_unsupported_keywords_recursive(prop_schema)
183
+
184
+ # Process items (for arrays)
185
+ if "items" in obj:
186
+ items = obj["items"]
187
+ if isinstance(items, dict):
188
+ _strip_unsupported_keywords_recursive(items)
189
+ elif isinstance(items, list):
190
+ for item in items:
191
+ _strip_unsupported_keywords_recursive(item)
192
+
193
+ # Process prefixItems (tuple validation)
194
+ if "prefixItems" in obj:
195
+ for item in obj["prefixItems"]:
196
+ _strip_unsupported_keywords_recursive(item)
197
+
198
+ # Process anyOf, oneOf, allOf
199
+ for key in ("anyOf", "oneOf", "allOf"):
200
+ if key in obj:
201
+ for item in obj[key]:
202
+ _strip_unsupported_keywords_recursive(item)
203
+
204
+
64
205
  def _add_strict_constraints_recursive(obj: Any, add_all_required: bool) -> None:
65
206
  """
66
207
  Recursively add strict constraints to a schema object.
@@ -223,6 +364,42 @@ class BaseProviderHandler(ABC):
223
364
  "json_mode": False,
224
365
  }
225
366
 
367
+ def apply_structured_output(
368
+ self,
369
+ output_schema: dict[str, Any],
370
+ output_type_name: Optional[str],
371
+ model_params: dict[str, Any],
372
+ ) -> dict[str, Any]:
373
+ """
374
+ Apply vendor-specific structured output handling to model params.
375
+
376
+ This is used by LLM providers (via mesh) when they receive an output_schema
377
+ from a consumer. Each vendor can customize how structured output is enforced.
378
+
379
+ Default behavior: Apply response_format with strict schema.
380
+ Override in subclasses for vendor-specific behavior (e.g., Claude hint mode).
381
+
382
+ Args:
383
+ output_schema: JSON schema dict from consumer
384
+ output_type_name: Name of the output type (e.g., "AnalysisResult")
385
+ model_params: Current model parameters dict (will be modified)
386
+
387
+ Returns:
388
+ Modified model_params with structured output settings applied
389
+ """
390
+ # Sanitize schema first to remove unsupported validation keywords
391
+ sanitized_schema = sanitize_schema_for_structured_output(output_schema)
392
+ strict_schema = make_schema_strict(sanitized_schema, add_all_required=True)
393
+ model_params["response_format"] = {
394
+ "type": "json_schema",
395
+ "json_schema": {
396
+ "name": output_type_name or "Response",
397
+ "schema": strict_schema,
398
+ "strict": True,
399
+ },
400
+ }
401
+ return model_params
402
+
226
403
  def __repr__(self) -> str:
227
404
  """String representation of handler."""
228
405
  return f"{self.__class__.__name__}(vendor='{self.vendor}')"