mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(mcp): fix the type-discipline gate (LIT002/LIT012) on tolerant_result.py
LIT002 flags mutable dict/list literal construction; LIT012 flags a TypedDict field with no ReadOnly qualifier. Neither of these dicts is ever mutated after construction, so: - _TextContentBlock's fields are now ReadOnly[...] (PEP 705), matching the ReadOnly-TypedDict convention already used elsewhere in this codebase (e.g. litellm/router_strategy/tag_based_routing.py). - Both _as_text_block return sites construct _TextContentBlock via its own keyword constructor instead of a dict literal, which isn't a builtin mutable-constructor name and isn't flagged. - _block_is_valid's validation payload and _degrade_invalid_content_blocks's return value are now wrapped in MappingProxyType, the sanctioned pattern for a construction that must stay dynamic; the list/list-comprehension each contained are switched to a tuple/generator, since a mutable value nested inside a frozen wrapper's argument still counts on its own. Verified against the real scripts/type_discipline_gate.py (now reports every LIT rule within its ceiling), scripts/check_type_discipline.py directly (zero LIT002/LIT012 on this file), ruff, and the full existing + new test suite, including an end-to-end run of TolerantClientSession.call_tool over real anyio streams with the same malformed payload used elsewhere in this PR, to confirm pydantic accepts the MappingProxyType return for whole-model construction, not just the isolated per-block check.
This commit is contained in:
parent
a7894dbb37
commit
fcec049e91
1 changed files with 15 additions and 9 deletions
|
|
@ -15,17 +15,19 @@ import base64
|
|||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from datetime import timedelta
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Final, Literal, TypedDict # noqa: TID251 # matches ClientSession.call_tool's real signature
|
||||
|
||||
from mcp import ClientSession, types
|
||||
from mcp.shared.session import ProgressFnT
|
||||
from mcp.types import CallToolResult as MCPCallToolResult
|
||||
from pydantic import BaseModel, ConfigDict, ValidationError, model_validator
|
||||
from typing_extensions import ReadOnly
|
||||
|
||||
|
||||
def _block_is_valid(block: object) -> bool:
|
||||
try:
|
||||
MCPCallToolResult.model_validate({"content": [block]})
|
||||
MCPCallToolResult.model_validate(MappingProxyType({"content": (block,)}))
|
||||
except ValidationError:
|
||||
return False
|
||||
return True
|
||||
|
|
@ -43,8 +45,8 @@ class _ResourcePayload(BaseModel):
|
|||
|
||||
|
||||
class _TextContentBlock(TypedDict):
|
||||
type: Literal["text"]
|
||||
text: str
|
||||
type: ReadOnly[Literal["text"]]
|
||||
text: ReadOnly[str]
|
||||
|
||||
|
||||
def _resource_text(resource: _ResourcePayload) -> str | None:
|
||||
|
|
@ -73,8 +75,8 @@ def _as_text_block(block: object) -> _TextContentBlock:
|
|||
if resource is not None:
|
||||
text: Final = _resource_text(resource)
|
||||
if text is not None:
|
||||
return {"type": "text", "text": text}
|
||||
return {"type": "text", "text": json.dumps(block, default=str)}
|
||||
return _TextContentBlock(type="text", text=text)
|
||||
return _TextContentBlock(type="text", text=json.dumps(block, default=str))
|
||||
|
||||
|
||||
class TolerantCallToolResult(MCPCallToolResult):
|
||||
|
|
@ -90,10 +92,14 @@ class TolerantCallToolResult(MCPCallToolResult):
|
|||
return data
|
||||
if all(_block_is_valid(block) for block in content):
|
||||
return data
|
||||
return {
|
||||
**data,
|
||||
"content": [block if _block_is_valid(block) else _as_text_block(block) for block in content],
|
||||
}
|
||||
return MappingProxyType(
|
||||
{
|
||||
**data,
|
||||
"content": tuple(
|
||||
block if _block_is_valid(block) else _as_text_block(block) for block in content
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TolerantClientSession(ClientSession):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue