diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index fd1d2973951..2088b6f6af7 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -191,10 +191,10 @@ jobs: - name: Check tests/e2e basedpyright (zero errors) if: steps.changes.outputs.decision != 'skip' run: | - if git diff --name-only --diff-filter=ACMRD "$GATE_BASE_SHA" HEAD -- ':(glob)tests/e2e/**/*.py' ':(glob)tests/e2e_harness/**/*.py' pyrightconfig.json | grep -q .; then + if git diff --name-only --diff-filter=ACMRD "$GATE_BASE_SHA" HEAD -- ':(glob)tests/e2e/**/*.py' ':(glob)tests/e2e_harness/**/*.py' ':(glob)litellm/**/*.py' pyrightconfig.json pyproject.toml uv.lock | grep -q .; then uv run --no-sync basedpyright tests/e2e tests/e2e_harness else - echo "No changed tests/e2e Python files; skipping." + echo "No changed tests/e2e, litellm Python, or dependency files; skipping." fi - name: Run the e2e harness tests @@ -623,6 +623,9 @@ jobs: - name: check_e2e_no_raw_requests run: uv run --no-sync python ./tests/code_coverage_tests/check_e2e_no_raw_requests.py + - name: check_router_constructor_stub + run: uv run --no-sync python ./tests/code_coverage_tests/check_router_constructor_stub.py + - name: check_migrations_no_data_rewrites run: uv run --no-sync python ./tests/code_coverage_tests/check_migrations_no_data_rewrites.py diff --git a/AGENTS.md b/AGENTS.md index d8de447be90..7bea7a56cbc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,7 +101,7 @@ Follow these coding conventions for new/updated code (a three-line fix in a lega - Never-nester: early returns over deep nesting - Don't throw. Model failures as values (One function (e.g., raise_public) maps error union to existing public exception contracts via exhaustive match + assert_never) - No mutation. Don't reassign variables, global or local. Instead of mutable lists and dicts, prefer tuples, frozen dataclasses (with slots=True), `MappingProxyType`, etc. - - Annotate every variable with `: Final` (LIT010). Unpacking and walrus targets cannot carry the annotation, so they are implicitly final. Don't rebind them. Never rebind or mutate function parameters (LIT011). `self`/`cls` attribute stores are the exception. If rebinding or in-place mutation is truly unavoidable, suppress with `# rebind-ok: ` + - Annotate every variable with `: Final` (LIT010). A `TypeVar`, `ParamSpec`, or `TypeVarTuple` is the exception and stays bare, since mypy reads a Final one as `Any`. Unpacking and walrus targets cannot carry the annotation, so they are implicitly final. Don't rebind them. Never rebind or mutate function parameters (LIT011). `self`/`cls` attribute stores are the exception. If rebinding or in-place mutation is truly unavoidable, suppress with `# rebind-ok: ` - Qualify every TypedDict field with `ReadOnly[...]` (LIT012), which nests freely with `Required` / `NotRequired` / `Annotated` in any order. If making the key writable is truly unavoidable, suppress with `# writable-ok: ` - Comprehensions take at most one `for` clause and one `if` clause (LIT014). Split stacked clauses into a helper generator, a named intermediate, or a plain loop. Suppress with `# comprehension-ok: ` only when unavoidable - Every pydantic model must be frozen (LIT015), set via `model_config = ConfigDict(frozen=True)`, a dict-literal `model_config`, an inner `class Config`, or the class keywords. Subclasses inherit it unless they override it. Replace in-place field writes with `model_copy(update=...)`. If making the model mutable is truly unavoidable, suppress with `# frozen-ok: ` on the `class` line diff --git a/litellm/caching/redis_batch.py b/litellm/caching/redis_batch.py index ade2ac1b6ca..9c0cd433bb8 100644 --- a/litellm/caching/redis_batch.py +++ b/litellm/caching/redis_batch.py @@ -43,7 +43,7 @@ if TYPE_CHECKING: from litellm.rust_bridge._native import NativeRequestRedis, NativeSend _T = TypeVar("_T") -_T_co: Final = TypeVar("_T_co", covariant=True) +_T_co = TypeVar("_T_co", covariant=True) _ScriptArg = str | bytes | int | float SettledHook = Callable[[asyncio.Future[_T]], Awaitable[None] | None] POST_CALL_FLUSH_DEADLINE_SECONDS: Final = 1.0 diff --git a/litellm/caching/redis_cluster_cache.py b/litellm/caching/redis_cluster_cache.py index 6fb84df331e..4061f1eadb6 100644 --- a/litellm/caching/redis_cluster_cache.py +++ b/litellm/caching/redis_cluster_cache.py @@ -31,7 +31,7 @@ else: Span = Any -_T: Final = TypeVar("_T") +_T = TypeVar("_T") def _connection_limit(max_connections: object) -> int | None: diff --git a/litellm/google_genai/adapters/transformation.py b/litellm/google_genai/adapters/transformation.py index c1895376675..9f9785ad191 100644 --- a/litellm/google_genai/adapters/transformation.py +++ b/litellm/google_genai/adapters/transformation.py @@ -118,7 +118,7 @@ _JSON_MIME_TYPE: Final = "application/json" _GEMINI_ONLY_SCHEMA_KEYS: Final = frozenset({"propertyOrdering", "property_ordering"}) _CONFIG_FIELDS: Final = TypeAdapter(Mapping[str, object]) _JSON_OBJECT_SCHEMA: Final = TypeAdapter(dict[str, JsonValue]) -_Validated: Final = TypeVar("_Validated") +_Validated = TypeVar("_Validated") _GENAI_CONTENT_PARTS: Final = TypeAdapter(Sequence[object]) _GENAI_INLINE_DATA: Final = TypeAdapter(_GenAIInlineData) _GENAI_FILE_DATA: Final = TypeAdapter(_GenAIFileData) diff --git a/litellm/integrations/anthropic_cache_control_hook.py b/litellm/integrations/anthropic_cache_control_hook.py index ea518d506fb..cfcb5ee93cf 100644 --- a/litellm/integrations/anthropic_cache_control_hook.py +++ b/litellm/integrations/anthropic_cache_control_hook.py @@ -140,7 +140,7 @@ def _validated_object_list(value: object) -> list[object] | None: return None -_MessageT: Final = TypeVar("_MessageT") +_MessageT = TypeVar("_MessageT") _CacheControlRewrite: TypeAlias = Callable[[object], ChatCompletionCachedContent | None] _MARK_CARRYING_MESSAGE_FIELDS: Final = ("content", "tool_calls") diff --git a/litellm/integrations/prometheus.py b/litellm/integrations/prometheus.py index fc1a601f9da..592188503f0 100644 --- a/litellm/integrations/prometheus.py +++ b/litellm/integrations/prometheus.py @@ -111,8 +111,8 @@ class _BudgetedCustomerFilter(TypedDict): budget_id: ReadOnly[_IsNotNull] -_BudgetRowT: Final = TypeVar("_BudgetRowT") -_TableRowT: Final = TypeVar("_TableRowT", bound=BaseModel) +_BudgetRowT = TypeVar("_BudgetRowT") +_TableRowT = TypeVar("_TableRowT", bound=BaseModel) _DEFAULT_BUDGET_METRICS_PER_REQUEST_TIMEOUT: Final = 5.0 @@ -263,7 +263,7 @@ class _LabeledMetric: _MetricLike: TypeAlias = "NoOpMetric | _LabeledMetric | MetricWrapperBase" -_SeriesLimitT: Final = TypeVar("_SeriesLimitT", int, float) +_SeriesLimitT = TypeVar("_SeriesLimitT", int, float) _REQUEST_METADATA_ADAPTER: Final[TypeAdapter[Mapping[str, object]]] = TypeAdapter(Mapping[str, object]) _POSITIVE_SERIES_CAP: Final[TypeAdapter[int]] = TypeAdapter(Annotated[int, Field(gt=0)]) _POSITIVE_SERIES_TTL: Final[TypeAdapter[float]] = TypeAdapter(Annotated[float, Field(gt=0)]) diff --git a/litellm/integrations/websearch_interception/handler.py b/litellm/integrations/websearch_interception/handler.py index d41647fe5f1..a0eea258d13 100644 --- a/litellm/integrations/websearch_interception/handler.py +++ b/litellm/integrations/websearch_interception/handler.py @@ -102,7 +102,7 @@ _RESPONSE_CONTENT_FIELD: Final = "content" _CLAUDE_CODE_WEB_SEARCH_PROMPT_PREFIX: Final = "Perform a web search for the query:" -_ResponseT: Final = TypeVar("_ResponseT") +_ResponseT = TypeVar("_ResponseT") class _PlanMetadataView(TypedDict): diff --git a/litellm/litellm_core_utils/asyncify.py b/litellm/litellm_core_utils/asyncify.py index 17cfb3ef84f..b0b8e30009d 100644 --- a/litellm/litellm_core_utils/asyncify.py +++ b/litellm/litellm_core_utils/asyncify.py @@ -8,7 +8,7 @@ import anyio import anyio.to_thread from typing_extensions import ParamSpec, TypeVar -T_ParamSpec: Final = ParamSpec("T_ParamSpec") +T_ParamSpec = ParamSpec("T_ParamSpec") T_Retval = TypeVar("T_Retval") diff --git a/litellm/litellm_core_utils/batch_utils.py b/litellm/litellm_core_utils/batch_utils.py index 7c7b7ea5c4b..f7eeb19db05 100644 --- a/litellm/litellm_core_utils/batch_utils.py +++ b/litellm/litellm_core_utils/batch_utils.py @@ -32,7 +32,7 @@ BATCH_COST_REQUEST_ID_SUFFIX: Final = "_batch_cost" _AZURE_PROVIDERS: Final = frozenset({LlmProviders.AZURE.value, LlmProviders.AZURE_AI.value}) _batch_create_routing: Final[ContextVar[bool]] = ContextVar("batch_create_routing", default=False) -_DeploymentT: Final = TypeVar("_DeploymentT", bound=Mapping[str, object]) +_DeploymentT = TypeVar("_DeploymentT", bound=Mapping[str, object]) class InMemoryFile(io.BytesIO): diff --git a/litellm/litellm_core_utils/chat_completion_agentic_loop.py b/litellm/litellm_core_utils/chat_completion_agentic_loop.py index ce98d2cbb01..e09b03351ed 100644 --- a/litellm/litellm_core_utils/chat_completion_agentic_loop.py +++ b/litellm/litellm_core_utils/chat_completion_agentic_loop.py @@ -37,7 +37,7 @@ from litellm.types.integrations.custom_logger import ( from litellm.types.utils import ModelResponse from litellm.utils import CustomStreamWrapper -_DispatchResult: Final = TypeVar("_DispatchResult") +_DispatchResult = TypeVar("_DispatchResult") _FOLLOWUP_INTERNAL_PARAMS: Final = frozenset( ( diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index b28d5a4ea21..b287e564666 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -140,7 +140,7 @@ def attribute_or_none(source: object, name: str) -> object: return getattr(source, name, None) -_Shape: Final = TypeVar("_Shape") +_Shape = TypeVar("_Shape") def has_attributes( diff --git a/litellm/litellm_core_utils/env_utils.py b/litellm/litellm_core_utils/env_utils.py index 462b0b3df47..82848ab6989 100644 --- a/litellm/litellm_core_utils/env_utils.py +++ b/litellm/litellm_core_utils/env_utils.py @@ -6,7 +6,7 @@ import logging import os from typing import Final, TypeVar -_NumberT: Final = TypeVar("_NumberT", int, float) +_NumberT = TypeVar("_NumberT", int, float) # Marked before logging: the LiteLLM loggers' own filters read env knobs through these helpers, so # warning first would re-enter the helper from inside the log call and recurse until RecursionError. diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 9b6ec8d28d6..ed6db5946af 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -601,7 +601,7 @@ _LITELLM_PARAMS: Final = TypeAdapter(dict[str, object]) _SERVED_ANTHROPIC_BODY: Final = TypeAdapter(dict[str, object]) _GOOGLE_USAGE_FIELDS: Final = frozenset(UsageMetadata.__annotations__) _REDACTED_RESULT: Final = TypeAdapter(object) -_LoggedResponse: Final = TypeVar("_LoggedResponse") +_LoggedResponse = TypeVar("_LoggedResponse") _GUARDRAIL_INFORMATION_KEY: Final = "standard_logging_guardrail_information" diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 17a1fa7d81b..280abad77c5 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -2868,7 +2868,7 @@ def _unsigned_thinking_identity(kind: str, thinking: object, signature: object) return None -_Replayed: Final = TypeVar("_Replayed") +_Replayed = TypeVar("_Replayed") def _with_missing_thinking_blocks( diff --git a/litellm/litellm_core_utils/prompt_templates/mid_conversation_system.py b/litellm/litellm_core_utils/prompt_templates/mid_conversation_system.py index ec6de4e206d..db8b8dde707 100644 --- a/litellm/litellm_core_utils/prompt_templates/mid_conversation_system.py +++ b/litellm/litellm_core_utils/prompt_templates/mid_conversation_system.py @@ -63,7 +63,7 @@ _RENDERED_ASSISTANT_PART_TYPES: Final = frozenset({"text", "server_tool_use"}) _THINKING_BLOCK_TYPES: Final = frozenset({"thinking", "redacted_thinking"}) _MessageKind: TypeAlias = Literal["system", "tool", "user", "other"] -_Message: Final = TypeVar("_Message") +_Message = TypeVar("_Message") _TextPart: TypeAlias = tuple[str, ChatCompletionCachedContent | None] diff --git a/litellm/litellm_core_utils/prompt_templates/prompt_cache_breakpoint_utils.py b/litellm/litellm_core_utils/prompt_templates/prompt_cache_breakpoint_utils.py index 27cf83210b7..7b73aa416bf 100644 --- a/litellm/litellm_core_utils/prompt_templates/prompt_cache_breakpoint_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/prompt_cache_breakpoint_utils.py @@ -11,7 +11,7 @@ from litellm.types.llms.openai import ( PromptCacheBreakpoint, ) -_MarkedT: Final = TypeVar("_MarkedT", bound=Mapping[str, object]) +_MarkedT = TypeVar("_MarkedT", bound=Mapping[str, object]) PROMPT_CACHE_BREAKPOINT_DROP_PARAMS_HINT: Final = ( @@ -33,7 +33,7 @@ _PROMPT_CACHE_BREAKPOINT: Final = TypeAdapter(PromptCacheBreakpoint) _CHAT_COMPLETION_PROMPT_CACHE_BREAKPOINT: Final = TypeAdapter(ChatCompletionPromptCacheBreakpoint) -_AcceptedBreakpointT: Final = TypeVar("_AcceptedBreakpointT", bound=Mapping[str, object]) +_AcceptedBreakpointT = TypeVar("_AcceptedBreakpointT", bound=Mapping[str, object]) def _accepted_prompt_cache_breakpoint( diff --git a/litellm/litellm_core_utils/ptu_shares.py b/litellm/litellm_core_utils/ptu_shares.py index d56c9f2ea14..001b865d07c 100644 --- a/litellm/litellm_core_utils/ptu_shares.py +++ b/litellm/litellm_core_utils/ptu_shares.py @@ -20,7 +20,7 @@ from litellm.llms.azure.ptu_capacity import ( is_str_keyed_mapping, ) -_DeploymentT: Final = TypeVar("_DeploymentT", bound=Mapping[str, object]) +_DeploymentT = TypeVar("_DeploymentT", bound=Mapping[str, object]) WildcardRoute: TypeAlias = Callable[[str], Sequence[Mapping[str, object]]] diff --git a/litellm/litellm_core_utils/single_flight.py b/litellm/litellm_core_utils/single_flight.py index 44a84d05925..73f0db068c5 100644 --- a/litellm/litellm_core_utils/single_flight.py +++ b/litellm/litellm_core_utils/single_flight.py @@ -5,8 +5,8 @@ from dataclasses import dataclass, field from functools import lru_cache from typing import Final, Generic, ParamSpec, TypeVar -_P: Final = ParamSpec("_P") -_R: Final = TypeVar("_R") +_P = ParamSpec("_P") +_R = TypeVar("_R") @dataclass(slots=True) diff --git a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py index c87357122ce..81004bd1a70 100644 --- a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py +++ b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py @@ -1327,7 +1327,7 @@ def _as_text_completion_chunk( return _text_completion_chunk_from_mapping(chunk) -_LogprobsValue: Final = TypeVar("_LogprobsValue") +_LogprobsValue = TypeVar("_LogprobsValue") def _joined_or_none(fields: Iterable[Sequence[_LogprobsValue] | None]) -> list[_LogprobsValue] | None: diff --git a/litellm/litellm_core_utils/token_counter.py b/litellm/litellm_core_utils/token_counter.py index e56dd83a277..017044e7bdf 100644 --- a/litellm/litellm_core_utils/token_counter.py +++ b/litellm/litellm_core_utils/token_counter.py @@ -611,7 +611,7 @@ def estimate_image_tokens( _MEDIA_FILE_CHARS_PER_TOKEN: Final = 4 -_EmbeddingInputT: Final = TypeVar("_EmbeddingInputT") +_EmbeddingInputT = TypeVar("_EmbeddingInputT") def _max_input_tokens(model: str) -> int | None: @@ -766,7 +766,7 @@ class _PdfReader(Protocol): EXTRAPOLATION_SAMPLES: Final = 16 -T_ParamSpec: Final = ParamSpec("T_ParamSpec") +T_ParamSpec = ParamSpec("T_ParamSpec") T_Retval = TypeVar("T_Retval") _COUNT_OFFLOAD_LIMITER: Final = anyio.lowlevel.RunVar[anyio.CapacityLimiter]("litellm_count_offload_limiter") _ANTHROPIC_TOOL_KEYS_MAPPED_BY_BRIDGE: Final = frozenset( diff --git a/litellm/litellm_core_utils/tool_fields.py b/litellm/litellm_core_utils/tool_fields.py index 10b6f07bc4d..3573b1bd919 100644 --- a/litellm/litellm_core_utils/tool_fields.py +++ b/litellm/litellm_core_utils/tool_fields.py @@ -180,7 +180,7 @@ def resolve_deployment_tool_fields( return resolve_tool_fields(kwargs, lambda: (drop_policy(kwargs),)) -_ToolT: Final = TypeVar("_ToolT") +_ToolT = TypeVar("_ToolT") HOSTED_MCP_DROP_WARNING: Final = ( "Dropping the hosted MCP tool(s) %s for model=%s on %s (drop_params=True) because this route cannot run a " @@ -295,7 +295,7 @@ def tools_without_mcp_servers( _TOOL_SELECTION_PARAMS: Final = frozenset({"tools", "tool_choice", "parallel_tool_calls"}) -_ParamsT: Final = TypeVar("_ParamsT", bound=dict[str, object]) +_ParamsT = TypeVar("_ParamsT", bound=dict[str, object]) def tool_params_without_hosted_mcp( diff --git a/litellm/llms/anthropic/messages/adapters/transformation.py b/litellm/llms/anthropic/messages/adapters/transformation.py index 5e4fba6d706..323743a8bb4 100644 --- a/litellm/llms/anthropic/messages/adapters/transformation.py +++ b/litellm/llms/anthropic/messages/adapters/transformation.py @@ -427,7 +427,7 @@ class AnthropicAdapter: return anthropic_wrapper.anthropic_sse_wrapper() -_BlockT: Final = TypeVar("_BlockT", bound=Mapping[str, object]) +_BlockT = TypeVar("_BlockT", bound=Mapping[str, object]) _CLAUDE_OVER_REASONING_EFFORT_PROVIDERS: Final = frozenset({LlmProviders.KYMA.value}) diff --git a/litellm/llms/anthropic/messages/context_management/editors/compact.py b/litellm/llms/anthropic/messages/context_management/editors/compact.py index d1cfcc2f2bc..7d489f29b11 100644 --- a/litellm/llms/anthropic/messages/context_management/editors/compact.py +++ b/litellm/llms/anthropic/messages/context_management/editors/compact.py @@ -65,7 +65,7 @@ from ..result import PolyfillResult _SUMMARY_TAG_RE: Final = re.compile(r"(.*?)", re.IGNORECASE | re.DOTALL) -_MsgT: Final = TypeVar("_MsgT", bound=Mapping[str, object]) +_MsgT = TypeVar("_MsgT", bound=Mapping[str, object]) def _as_object(value: object) -> object: diff --git a/litellm/llms/anthropic/messages/utils.py b/litellm/llms/anthropic/messages/utils.py index 95c7914befa..18fd479cb6b 100644 --- a/litellm/llms/anthropic/messages/utils.py +++ b/litellm/llms/anthropic/messages/utils.py @@ -27,7 +27,7 @@ _EFFORT_DEGRADATION_CHAIN: Final[Mapping[str, tuple[str, ...]]] = MappingProxyTy ) _THINKING_OFF: Final = "none" -_JsonValue: Final = TypeVar("_JsonValue") +_JsonValue = TypeVar("_JsonValue") def json_copy_or_original(value: _JsonValue) -> _JsonValue: diff --git a/litellm/llms/bedrock/chat/converse_to_openai.py b/litellm/llms/bedrock/chat/converse_to_openai.py index ce80a2dc8bb..bf78ef37bf5 100644 --- a/litellm/llms/bedrock/chat/converse_to_openai.py +++ b/litellm/llms/bedrock/chat/converse_to_openai.py @@ -21,7 +21,7 @@ from litellm.types.llms.openai import ( ) JsonPath: TypeAlias = tuple[str | int, ...] -_Node: Final = TypeVar("_Node") +_Node = TypeVar("_Node") _OBJECT: Final = TypeAdapter(Mapping[str, object]) _ITEMS: Final = TypeAdapter(Sequence[object]) diff --git a/litellm/llms/bedrock/passthrough/guardrail_translation/handler.py b/litellm/llms/bedrock/passthrough/guardrail_translation/handler.py index 3514a5e6e5c..b587528cd2f 100644 --- a/litellm/llms/bedrock/passthrough/guardrail_translation/handler.py +++ b/litellm/llms/bedrock/passthrough/guardrail_translation/handler.py @@ -849,7 +849,7 @@ class InvokeStreamFrame: _JSON_OBJECT: Final = TypeAdapter(Mapping[str, object]) -_Frame: Final = TypeVar("_Frame", ConverseStreamFrame, InvokeStreamFrame) +_Frame = TypeVar("_Frame", ConverseStreamFrame, InvokeStreamFrame) def _event_message(raw: bytes) -> tuple[str, bytes] | None: diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index 3a6fa5f51da..8b7599f3757 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -76,7 +76,7 @@ _RequestContent: TypeAlias = str | bytes | Iterable[bytes] | AsyncIterable[bytes _IPV4_LOCAL_ADDRESS: Final = "0.0.0.0" -_HttpxTransportT: Final = TypeVar("_HttpxTransportT", httpx.BaseTransport, httpx.AsyncBaseTransport) +_HttpxTransportT = TypeVar("_HttpxTransportT", httpx.BaseTransport, httpx.AsyncBaseTransport) HTTPEventHooks: TypeAlias = Mapping[str, list[Callable[..., object]]] diff --git a/litellm/llms/minimax/videos/transformation.py b/litellm/llms/minimax/videos/transformation.py index 7d458cb8877..a1505d2a71e 100644 --- a/litellm/llms/minimax/videos/transformation.py +++ b/litellm/llms/minimax/videos/transformation.py @@ -176,7 +176,7 @@ _CREATE_REQUEST_PARAMS: Final = frozenset({*_CREATE_BODY_KEYS, "content", "input _TASK_NOT_FOUND_MESSAGE: Final = "record not found" -_ParsedT: Final = TypeVar("_ParsedT") +_ParsedT = TypeVar("_ParsedT") def _minimax_error(status_code: int, message: str) -> MinimaxVideoError: diff --git a/litellm/llms/openai/responses/guardrail_translation/handler.py b/litellm/llms/openai/responses/guardrail_translation/handler.py index 20b7506ae0a..ba357219bca 100644 --- a/litellm/llms/openai/responses/guardrail_translation/handler.py +++ b/litellm/llms/openai/responses/guardrail_translation/handler.py @@ -610,7 +610,7 @@ def tool_output_texts(item: object) -> tuple[tuple[int | None, str], ...]: return ((None, tool_content),) if isinstance(tool_content, str) and tool_content else () -_Grouped: Final = TypeVar("_Grouped") +_Grouped = TypeVar("_Grouped") def _pair_call_id(pair: tuple[str, object]) -> str: diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 52345e1ff40..e04d741f9ee 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -754,7 +754,7 @@ def _integer_enums_fit(schema: JsonValue) -> bool: ) -_GeminiRequestPart: Final = TypeVar("_GeminiRequestPart") +_GeminiRequestPart = TypeVar("_GeminiRequestPart") def with_integer_enums_that_fit( diff --git a/litellm/llms/vertex_ai/context_caching/rejected_cache_retry.py b/litellm/llms/vertex_ai/context_caching/rejected_cache_retry.py index 7334fb0520a..1179c47143d 100644 --- a/litellm/llms/vertex_ai/context_caching/rejected_cache_retry.py +++ b/litellm/llms/vertex_ai/context_caching/rejected_cache_retry.py @@ -13,8 +13,8 @@ from litellm.utils import is_cached_message from .known_cached_contents import names_rejected_cache -Built: Final = TypeVar("Built") -Sent: Final = TypeVar("Sent") +Built = TypeVar("Built") +Sent = TypeVar("Sent") def _build_from_copies( diff --git a/litellm/llms/vertex_ai/gemini/long_decode.py b/litellm/llms/vertex_ai/gemini/long_decode.py index 25aae70a853..5b887f4b3a2 100644 --- a/litellm/llms/vertex_ai/gemini/long_decode.py +++ b/litellm/llms/vertex_ai/gemini/long_decode.py @@ -63,7 +63,7 @@ _CANDIDATE_COUNT_KEYS: Final = ("candidate_count", "candidateCount") LongDecodeHops: TypeAlias = tuple[GenerateContentResponseBody, ...] AsyncHopPost: TypeAlias = Callable[[RequestBody], Awaitable[httpx.Response]] SyncHopPost: TypeAlias = Callable[[RequestBody], httpx.Response] -_Metadata: Final = TypeVar("_Metadata", bound=Mapping[str, object]) +_Metadata = TypeVar("_Metadata", bound=Mapping[str, object]) @dataclass(frozen=True, slots=True) diff --git a/litellm/llms/vertex_ai/gemini/rejected_thought_signature_retry.py b/litellm/llms/vertex_ai/gemini/rejected_thought_signature_retry.py index 32893471c11..01ada902fe9 100644 --- a/litellm/llms/vertex_ai/gemini/rejected_thought_signature_retry.py +++ b/litellm/llms/vertex_ai/gemini/rejected_thought_signature_retry.py @@ -11,8 +11,8 @@ from litellm.types.llms.vertex_ai import ContentType, PartType, RequestBody from ..context_caching.rejected_cache_retry import status_and_message -Body: Final = TypeVar("Body", bound=Mapping[str, object]) -Sent: Final = TypeVar("Sent") +Body = TypeVar("Body", bound=Mapping[str, object]) +Sent = TypeVar("Sent") _REJECTED_SIGNATURE_MARKER: Final = "thought signature" _SIGNATURE_KEYS: Final = frozenset({"thoughtSignature", "thought_signature"}) diff --git a/litellm/llms/volcengine/videos/transformation.py b/litellm/llms/volcengine/videos/transformation.py index a75a4185186..9d6dba15826 100644 --- a/litellm/llms/volcengine/videos/transformation.py +++ b/litellm/llms/volcengine/videos/transformation.py @@ -36,7 +36,7 @@ if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging -_ParsedT: Final = TypeVar("_ParsedT") +_ParsedT = TypeVar("_ParsedT") class VolcEngineVideoError(BaseLLMException): diff --git a/litellm/proxy/auth/model_checks.py b/litellm/proxy/auth/model_checks.py index c1db8b7b4c7..1a7356fb035 100644 --- a/litellm/proxy/auth/model_checks.py +++ b/litellm/proxy/auth/model_checks.py @@ -44,7 +44,7 @@ _PROVIDERS_LISTED_ONLY_THROUGH_A_DEPLOYMENT: Final = frozenset( {LlmProviders.OPENROUTER.value, LlmProviders.HOSTED_VLLM.value, LlmProviders.LITELLM_PROXY.value} ) -_Listed: Final = TypeVar("_Listed") +_Listed = TypeVar("_Listed") async def run_model_listing(listing: Callable[[], _Listed]) -> _Listed: diff --git a/litellm/proxy/bug_report_config.py b/litellm/proxy/bug_report_config.py index 6c0209644b4..019de5ade3e 100644 --- a/litellm/proxy/bug_report_config.py +++ b/litellm/proxy/bug_report_config.py @@ -108,7 +108,7 @@ def _litellm_settings_keys() -> frozenset[str]: def _router_settings_keys() -> frozenset[str]: from litellm.router import Router - return frozenset(name for name in inspect.signature(Router.__init__).parameters if name != "self") # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # untyped params, only names are read + return frozenset(inspect.signature(Router).parameters) @functools.cache diff --git a/litellm/proxy/client/cli/commands/agents.py b/litellm/proxy/client/cli/commands/agents.py index b70432d1e72..53d08ee98e4 100644 --- a/litellm/proxy/client/cli/commands/agents.py +++ b/litellm/proxy/client/cli/commands/agents.py @@ -540,7 +540,7 @@ class ModelUpstream: _NO_UPSTREAMS: Final[Mapping[str, ModelUpstream]] = MappingProxyType({}) -_Pinned: Final = TypeVar("_Pinned", bound=Hashable) +_Pinned = TypeVar("_Pinned", bound=Hashable) def _public_name(deployment: _Deployment) -> str: @@ -1230,7 +1230,7 @@ def _launch( raise click.ClickException(str(e)) -_Decorated: Final = TypeVar("_Decorated", bound=Callable[..., object] | click.Command) +_Decorated = TypeVar("_Decorated", bound=Callable[..., object] | click.Command) def _subscription_option(binary: str) -> Callable[[_Decorated], _Decorated]: diff --git a/litellm/proxy/client/cli/commands/auth.py b/litellm/proxy/client/cli/commands/auth.py index e38f54adc4f..f1e6b017e43 100644 --- a/litellm/proxy/client/cli/commands/auth.py +++ b/litellm/proxy/client/cli/commands/auth.py @@ -138,7 +138,7 @@ class CliAuthResult(TypedDict): team_id: str | None -_TeamMapping: Final = TypeVar("_TeamMapping", bound=Mapping[str, object]) +_TeamMapping = TypeVar("_TeamMapping", bound=Mapping[str, object]) _JSON_OBJECT: Final = TypeAdapter(Mapping[str, object], config=ConfigDict(hide_input_in_errors=True)) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index e812b03385d..0ff094f6ad9 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -160,8 +160,8 @@ from litellm.types.router_weights import RouterWeights, validate_router_weights _LateResponseT = TypeVar("_LateResponseT", bound=Response) _LlmCallT = TypeVar("_LlmCallT") -_ResponseT: Final = TypeVar("_ResponseT") -_ChunkT: Final = TypeVar("_ChunkT") +_ResponseT = TypeVar("_ResponseT") +_ChunkT = TypeVar("_ChunkT") KNOWN_PROXY_ROUTES: Final = frozenset( route for member in LiteLLMRoutes for route in member.value if route.startswith("/") diff --git a/litellm/proxy/common_utils/openapi_route.py b/litellm/proxy/common_utils/openapi_route.py index 69fbeb67277..761b221f732 100644 --- a/litellm/proxy/common_utils/openapi_route.py +++ b/litellm/proxy/common_utils/openapi_route.py @@ -12,7 +12,7 @@ from starlette.routing import BaseRoute, Route from litellm.proxy.common_utils.custom_openapi_spec import JsonObject OpenAPISchema: TypeAlias = JsonObject -Inputs: Final = TypeVar("Inputs") +Inputs = TypeVar("Inputs") _SERVERS: Final = TypeAdapter(list[dict[str, object]]) diff --git a/litellm/proxy/common_utils/token_count_log.py b/litellm/proxy/common_utils/token_count_log.py index 04c07a29117..1da2dd70461 100644 --- a/litellm/proxy/common_utils/token_count_log.py +++ b/litellm/proxy/common_utils/token_count_log.py @@ -25,9 +25,9 @@ def _logfmt_value(value: str | None) -> str: return json.dumps(value).translate(_JSON_ESCAPED_BRACES) -_Counted: Final = TypeVar("_Counted") -_Rejection: Final = TypeVar("_Rejection") -_ProviderCount: Final = TypeVar("_ProviderCount", bound=Mapping[str, object]) +_Counted = TypeVar("_Counted") +_Rejection = TypeVar("_Rejection") +_ProviderCount = TypeVar("_ProviderCount", bound=Mapping[str, object]) @dataclass(frozen=True, slots=True) diff --git a/litellm/proxy/db/exception_handler.py b/litellm/proxy/db/exception_handler.py index c2d58512fea..e42042e8987 100644 --- a/litellm/proxy/db/exception_handler.py +++ b/litellm/proxy/db/exception_handler.py @@ -602,7 +602,7 @@ def _coerce_timeout(value: object, fallback: float) -> float: return fallback -_ReadResultT: Final = TypeVar("_ReadResultT") +_ReadResultT = TypeVar("_ReadResultT") class _DBReconnectClient(Protocol): diff --git a/litellm/proxy/db/prisma_client.py b/litellm/proxy/db/prisma_client.py index be5de040fdf..9d6214dc06d 100644 --- a/litellm/proxy/db/prisma_client.py +++ b/litellm/proxy/db/prisma_client.py @@ -105,7 +105,7 @@ class _PrismaDrainTracker: await self._drained.wait() -_EngineResponseT: Final = TypeVar("_EngineResponseT") +_EngineResponseT = TypeVar("_EngineResponseT") class _TrackedPrismaEngine: diff --git a/litellm/proxy/guardrails/guardrail_hooks/akto/akto_attachments.py b/litellm/proxy/guardrails/guardrail_hooks/akto/akto_attachments.py index 54d09ff0b1e..c72f31fdf8d 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/akto/akto_attachments.py +++ b/litellm/proxy/guardrails/guardrail_hooks/akto/akto_attachments.py @@ -42,7 +42,7 @@ _FILE_SOURCE_FIELDS: Final = frozenset(("file_data", "file_id")) _ATTACHMENT_BLOCK_TYPES: Final = frozenset(_FILE_CHECKED_FIELDS) _OBJECT_MAPPING: Final[TypeAdapter[dict[str, object]]] = TypeAdapter(dict[str, object]) -_T: Final = TypeVar("_T") +_T = TypeVar("_T") @dataclass(frozen=True, slots=True) diff --git a/litellm/proxy/guardrails/guardrail_hooks/custom_code/bounded_execution.py b/litellm/proxy/guardrails/guardrail_hooks/custom_code/bounded_execution.py index d926b40da6a..eaf17d2751c 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/custom_code/bounded_execution.py +++ b/litellm/proxy/guardrails/guardrail_hooks/custom_code/bounded_execution.py @@ -22,7 +22,7 @@ from typing import Final, Generic, TypeVar from litellm._logging import verbose_proxy_logger -T: Final = TypeVar("T") +T = TypeVar("T") _INTERRUPT_GRACE_SECONDS: Final = 1.0 _INTERRUPT_POLL_SECONDS: Final = 0.05 diff --git a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py index 71b8976bebf..1877b0c97a9 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py +++ b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py @@ -82,7 +82,7 @@ from .patterns import PATTERN_EXTRA_CONFIG, get_compiled_pattern MAX_KEYWORD_VALUE_GAP_WORDS: Final = 1 _DECODED_ARGUMENTS: Final = TypeAdapter(object) -_StreamItemT: Final = TypeVar("_StreamItemT") +_StreamItemT = TypeVar("_StreamItemT") _JSON_ENTRIES: Final = TypeAdapter(list[object]) GAP_WORD_TOKENIZER: Final = re.compile(r"\b\w+\b") SENTENCE_TERMINATORS: Final = re.compile(r"[.!?]+") diff --git a/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py b/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py index 4880444ada1..07591209fe3 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py +++ b/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py @@ -52,7 +52,7 @@ SENSITIVE_DATA_DETECTOR_KEYS: Final[list[str]] = ["sensitiveData", "dataDetector # Type aliases MessageRole = Literal["user", "assistant"] LLMResponse = object -_LLMResponseT: Final = TypeVar("_LLMResponseT") +_LLMResponseT = TypeVar("_LLMResponseT") _LEGACY_NOMA_DEPRECATION_WARNED = False if TYPE_CHECKING: diff --git a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py index f2697c0aa11..52d3a07cf10 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py +++ b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py @@ -80,7 +80,7 @@ _DECODED_ARGUMENTS: Final = TypeAdapter(object) _REQUEST_DATA: Final = TypeAdapter(object) _ToolTarget: TypeAlias = tuple[str | None, str | None] _JudgedCalls: TypeAlias = Mapping[str, tuple[tuple[str, str], ...]] -_GroupedT: Final = TypeVar("_GroupedT") +_GroupedT = TypeVar("_GroupedT") def _snake_case(name: str) -> str: diff --git a/litellm/proxy/health_check.py b/litellm/proxy/health_check.py index de2ed4fd4ba..85df98d365c 100644 --- a/litellm/proxy/health_check.py +++ b/litellm/proxy/health_check.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: from litellm.router import Router logger: Final = logging.getLogger(__name__) -_DeploymentT: Final = TypeVar("_DeploymentT", bound=Mapping[str, object]) +_DeploymentT = TypeVar("_DeploymentT", bound=Mapping[str, object]) from litellm.constants import ( BACKGROUND_HEALTH_CHECK_MAX_TOKENS, BACKGROUND_HEALTH_CHECK_MAX_TOKENS_REASONING, diff --git a/litellm/proxy/list_api/in_memory.py b/litellm/proxy/list_api/in_memory.py index 3f3f173ba1a..dad938b1c9f 100644 --- a/litellm/proxy/list_api/in_memory.py +++ b/litellm/proxy/list_api/in_memory.py @@ -25,7 +25,7 @@ from litellm.proxy.list_api.list_framework import ( Within, ) -TRow: Final = TypeVar("TRow") +TRow = TypeVar("TRow") Cell: TypeAlias = str | int | float | datetime | None # A tuple-valued cell is a row's repeated field (a model group's providers, say). A predicate diff --git a/litellm/proxy/management_endpoints/customer_endpoints.py b/litellm/proxy/management_endpoints/customer_endpoints.py index b426369adea..130b92f5e73 100644 --- a/litellm/proxy/management_endpoints/customer_endpoints.py +++ b/litellm/proxy/management_endpoints/customer_endpoints.py @@ -56,7 +56,7 @@ from litellm.types.proxy.management_endpoints.customer_endpoints import ( UnblockUsersResponse, ) -_RowT_co: Final = TypeVar("_RowT_co", covariant=True) +_RowT_co = TypeVar("_RowT_co", covariant=True) _STR_OBJECT_DICT: Final = TypeAdapter(dict[str, object]) _CLEARABLE_LIST_FIELDS: Final = frozenset({"models"}) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index f990f043dea..5d9000aa397 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -978,7 +978,7 @@ _KEY_TYPE_PRESET_ROUTES: Final = MappingProxyType( ) -_KeyJsonT: Final = TypeVar("_KeyJsonT", bound=dict[str, object]) +_KeyJsonT = TypeVar("_KeyJsonT", bound=dict[str, object]) def handle_key_type(data: GenerateKeyRequest, data_json: _KeyJsonT) -> _KeyJsonT: diff --git a/litellm/proxy/management_endpoints/router_settings_endpoints.py b/litellm/proxy/management_endpoints/router_settings_endpoints.py index 013eb1ebff4..5e52422bfb0 100644 --- a/litellm/proxy/management_endpoints/router_settings_endpoints.py +++ b/litellm/proxy/management_endpoints/router_settings_endpoints.py @@ -65,9 +65,8 @@ def _router_setting_source( def _get_routing_strategies_from_router_class() -> list[str]: """ - Dynamically extract routing strategies from the Router class __init__ method. + Dynamically extract routing strategies from the Router constructor's signature. """ - # Get the __init__ signature sig: Final = inspect.signature(Router) # Get the routing_strategy parameter diff --git a/litellm/proxy/management_endpoints/tool_management_endpoints.py b/litellm/proxy/management_endpoints/tool_management_endpoints.py index 003b1499582..5c80af5b3ec 100644 --- a/litellm/proxy/management_endpoints/tool_management_endpoints.py +++ b/litellm/proxy/management_endpoints/tool_management_endpoints.py @@ -59,7 +59,7 @@ from litellm.types.tool_management import ( ToolUsageLogsResponse, ) -_RowT_co: Final = TypeVar("_RowT_co", covariant=True) +_RowT_co = TypeVar("_RowT_co", covariant=True) if TYPE_CHECKING: diff --git a/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py b/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py index 348f5815be6..2938348cf9b 100644 --- a/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py +++ b/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py @@ -139,7 +139,7 @@ _DISTINCT_TAG_ROWS: Final = TypeAdapter(list[_DistinctTagRow]) _ACTIVE_USERS_ROWS: Final = TypeAdapter(list[_ActiveUsersRow]) _TAG_SUMMARY_ROWS: Final = TypeAdapter(list[_TagSummaryRow]) -_RowT_co: Final = TypeVar("_RowT_co", covariant=True) +_RowT_co = TypeVar("_RowT_co", covariant=True) if TYPE_CHECKING: diff --git a/litellm/proxy/management_helpers/audit_logs.py b/litellm/proxy/management_helpers/audit_logs.py index aa31f44dd13..5a4ce90d23d 100644 --- a/litellm/proxy/management_helpers/audit_logs.py +++ b/litellm/proxy/management_helpers/audit_logs.py @@ -135,7 +135,7 @@ def _audit_log_task_done_callback(task: asyncio.Task) -> None: verbose_proxy_logger.error("Audit log callback task failed: %s", exc, exc_info=exc) -_T: Final = TypeVar("_T") +_T = TypeVar("_T") class AuditTaskRegistry: diff --git a/litellm/proxy/management_helpers/bulk_user_creation.py b/litellm/proxy/management_helpers/bulk_user_creation.py index cca37ebd7ed..e4d66e5e41c 100644 --- a/litellm/proxy/management_helpers/bulk_user_creation.py +++ b/litellm/proxy/management_helpers/bulk_user_creation.py @@ -80,7 +80,7 @@ BULK_NEW_USER_CONCURRENCY: Final = 10 TeamRole: TypeAlias = Literal["user", "admin"] KeyGenerator: TypeAlias = Callable[..., Awaitable[object]] -_T: Final = TypeVar("_T") +_T = TypeVar("_T") @dataclass(frozen=True, slots=True) diff --git a/litellm/proxy/management_helpers/utils.py b/litellm/proxy/management_helpers/utils.py index 1c4eada67fb..3b37b9d6e02 100644 --- a/litellm/proxy/management_helpers/utils.py +++ b/litellm/proxy/management_helpers/utils.py @@ -769,8 +769,8 @@ async def _emit_management_endpoint_otel_span( ) -_P: Final = ParamSpec("_P") -_R: Final = TypeVar("_R") +_P = ParamSpec("_P") +_R = TypeVar("_R") def management_endpoint_wrapper(func: Callable[_P, Awaitable[_R]]) -> Callable[_P, Awaitable[_R]]: diff --git a/litellm/proxy/mcp_server/faults/list_outcomes.py b/litellm/proxy/mcp_server/faults/list_outcomes.py index b8af3ead8fb..ef7144f14ac 100644 --- a/litellm/proxy/mcp_server/faults/list_outcomes.py +++ b/litellm/proxy/mcp_server/faults/list_outcomes.py @@ -258,7 +258,7 @@ def rate_limit_retry_delay( return min(max_delay, base_delay * 2.0**attempt * (1 + jitter)) -T: Final = TypeVar("T") +T = TypeVar("T") async def retry_upstream_rate_limit( diff --git a/litellm/proxy/mcp_server/interactions.py b/litellm/proxy/mcp_server/interactions.py index 24cedb57ef8..232a2929673 100644 --- a/litellm/proxy/mcp_server/interactions.py +++ b/litellm/proxy/mcp_server/interactions.py @@ -193,7 +193,7 @@ def seal_continuation( return InputRequiredResult(input_requests=result.input_requests, request_state=sealed.ok, _meta=result.meta) -_Terminal: Final = TypeVar("_Terminal") +_Terminal = TypeVar("_Terminal") @dataclass(frozen=True, slots=True) diff --git a/litellm/proxy/mcp_server/semantic_tool_filter.py b/litellm/proxy/mcp_server/semantic_tool_filter.py index 8666c05079d..1c89acac01f 100644 --- a/litellm/proxy/mcp_server/semantic_tool_filter.py +++ b/litellm/proxy/mcp_server/semantic_tool_filter.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: from litellm.router import Router -_Tool: Final = TypeVar("_Tool") +_Tool = TypeVar("_Tool") class SemanticToolFilterContextWindowError(Exception): diff --git a/litellm/proxy/native_compaction.py b/litellm/proxy/native_compaction.py index fd27e7fbbdc..98cbe528321 100644 --- a/litellm/proxy/native_compaction.py +++ b/litellm/proxy/native_compaction.py @@ -20,7 +20,7 @@ from litellm.router_strategy.complexity_router.context_compaction import ( native_compaction_call, ) -_ResultT: Final = TypeVar("_ResultT") +_ResultT = TypeVar("_ResultT") _ASGI_APP: Final = TypeAdapter[ASGIApp](ASGIApp) _ROOT_PATH: Final = TypeAdapter(str) _JSON_OBJECT: Final = TypeAdapter(Mapping[str, object]) diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 7f5983d5799..fde616c34b4 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -2671,7 +2671,7 @@ WebsocketUpstreamFrameClassifier: TypeAlias = Callable[[Mapping[str, object]], W _SESSION_FRAMES_EVERY_ROW_CARRIES: Final[frozenset[WebsocketUpstreamFrameKind]] = frozenset( {"opens_session", "updates_session"} ) -_FrameT: Final = TypeVar("_FrameT", bound=Mapping[str, object]) +_FrameT = TypeVar("_FrameT", bound=Mapping[str, object]) @dataclass(frozen=True, slots=True) diff --git a/litellm/proxy/roi_calculator/github.py b/litellm/proxy/roi_calculator/github.py index 922faaed992..c67eed3a03d 100644 --- a/litellm/proxy/roi_calculator/github.py +++ b/litellm/proxy/roi_calculator/github.py @@ -18,7 +18,7 @@ from litellm.types.llms.custom_http import httpxSpecialProvider from litellm.types.roi_calculator import ROIPullCommit, ROIPullEvidence, ROIPullFile, ROISettings from litellm.types.roi_observed import ObservedIssue -_T: Final = TypeVar("_T") +_T = TypeVar("_T") class SourceError(Exception): diff --git a/litellm/proxy/roi_calculator/gitlab.py b/litellm/proxy/roi_calculator/gitlab.py index ae53583ac5c..b961a02503f 100644 --- a/litellm/proxy/roi_calculator/gitlab.py +++ b/litellm/proxy/roi_calculator/gitlab.py @@ -19,7 +19,7 @@ from litellm.types.llms.custom_http import httpxSpecialProvider from litellm.types.roi_calculator import ROIPullCommit, ROIPullEvidence, ROIPullFile, ROISettings from litellm.types.roi_observed import ObservedIssue -_T: Final = TypeVar("_T", bound=BaseModel) +_T = TypeVar("_T", bound=BaseModel) class _User(LiteLLMBaseModel): diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 8e977c69557..f15f90a6e23 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -336,7 +336,7 @@ if TYPE_CHECKING: else: Span = Any -_T: Final = TypeVar("_T") +_T = TypeVar("_T") class _ViewCountRow(TypedDict): diff --git a/litellm/repositories/chunked_in.py b/litellm/repositories/chunked_in.py index 7cd4d10c8e5..0e48c9424a6 100644 --- a/litellm/repositories/chunked_in.py +++ b/litellm/repositories/chunked_in.py @@ -21,8 +21,8 @@ IN_LIST_CHUNK_SIZE: Final = 5_000 MAX_IN_LIST_CHUNK_SIZE: Final = 30_000 LOGICAL_KEYS: Final = frozenset({"AND", "OR", "NOT"}) -RowT: Final = TypeVar("RowT") -ResultT: Final = TypeVar("ResultT") +RowT = TypeVar("RowT") +ResultT = TypeVar("ResultT") Atomicity: TypeAlias = Literal["caller_transaction", "per_chunk_ok"] """More than `chunk_size` values means more than one statement. `caller_transaction` diff --git a/litellm/responses/litellm_completion_transformation/compaction_items.py b/litellm/responses/litellm_completion_transformation/compaction_items.py index 8bd82c49090..51d741c8404 100644 --- a/litellm/responses/litellm_completion_transformation/compaction_items.py +++ b/litellm/responses/litellm_completion_transformation/compaction_items.py @@ -17,7 +17,7 @@ from .tool_search import chat_config_builds_anthropic_request, provider_chat_con _JSON_OBJECT: Final = TypeAdapter(dict[str, object]) _OBJECT_LIST: Final = TypeAdapter(list[object]) -_Item: Final = TypeVar("_Item") +_Item = TypeVar("_Item") _INSTRUCTION_ROLES: Final = frozenset(("system", "developer")) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 33ea85d7d53..a67f7acf906 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -282,8 +282,8 @@ _AUDIO_PART_ADAPTER: Final = TypeAdapter(ChatCompletionAudioObject) _IMAGE_URL_ADAPTER: Final = TypeAdapter(ChatCompletionImageUrlObject) _FILE_PART_ADAPTER: Final = TypeAdapter(_ResponsesFilePart) _MessageT = TypeVar("_MessageT") -_MediaT: Final = TypeVar("_MediaT") -_InputItemT: Final = TypeVar("_InputItemT") +_MediaT = TypeVar("_MediaT") +_InputItemT = TypeVar("_InputItemT") _CACHED_CONTENT_ADAPTER: Final = TypeAdapter(ChatCompletionCachedContent) _DICT_ITEMS_LIST_ADAPTER: Final = TypeAdapter(list[dict[object, object]]) _TEXT_ADAPTER: Final = TypeAdapter(str) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 7fc8d5aed32..d275d9dc93f 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -131,7 +131,7 @@ def _as_input_text_part(part: object) -> object: return part -_RequestInputT: Final = TypeVar("_RequestInputT") +_RequestInputT = TypeVar("_RequestInputT") def _provider_mapped_params( diff --git a/litellm/router.py b/litellm/router.py index 4b51c16aa81..8f53ae3056c 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -63,7 +63,7 @@ def _router_init( ## CACHING ## redis_url: str | None = None, redis_host: str | None = None, - redis_port: int | None = None, + redis_port: int | str | None = None, redis_password: str | None = None, redis_db: int | None = None, cache_responses: bool | None = False, @@ -120,7 +120,6 @@ def _router_init( fallback_budget_check: FallbackBudgetCheck | None = None, auto_router_capability_limit: AutoRouterCapabilityLimit | None = None, session_affinity_repin_on_failover: bool = False, - _host: PythonHost = python_host, ) -> None: """ Initialize the Router class with the given parameters for caching, reliability, and routing strategy. @@ -129,7 +128,7 @@ def _router_init( model_list (Optional[list]): List of models to be used. Defaults to None. redis_url (Optional[str]): URL of the Redis server. Defaults to None. redis_host (Optional[str]): Hostname of the Redis server. Defaults to None. - redis_port (Optional[int]): Port of the Redis server. Defaults to None. + redis_port (int | str | None): Port of the Redis server. Defaults to None. redis_password (Optional[str]): Password of the Redis server. Defaults to None. cache_responses (Optional[bool]): Flag to enable caching of responses. Defaults to False. cache_kwargs (dict): Additional kwargs to pass to RedisCache. Defaults to {}. @@ -228,7 +227,7 @@ if TYPE_CHECKING: from litellm.types.llms.openai import AllMessageValues, OpenAIFileObject from litellm.types.utils import EmbeddingResponse, LiteLLMBatch, ModelResponse - _P: Final = ParamSpec("_P") + _P = ParamSpec("_P") def _takes_parameters_of(source: Callable[_P, None]) -> Callable[[Callable[..., None]], Callable[_P, None]]: ... @@ -432,9 +431,7 @@ else: core: Final = self.__dict__.get("_core") return sorted({*super().__dir__(), *(dir(core) if core is not None else ())}) - Router.__init__.__signature__ = _CORE_SIGNATURE.replace( # pyright: ignore[reportFunctionMemberAccess] # inspect reads it - parameters=[parameter for parameter in _CORE_SIGNATURE.parameters.values() if parameter.name != "_host"] - ) + Router.__init__.__signature__ = _CORE_SIGNATURE # pyright: ignore[reportFunctionMemberAccess] # inspect reads it Router.__init__.__doc__ = _router_init.__doc__ diff --git a/litellm/router_deprecated.py b/litellm/router_deprecated.py index c2e1c197259..82a53f001c5 100644 --- a/litellm/router_deprecated.py +++ b/litellm/router_deprecated.py @@ -60,8 +60,8 @@ if TYPE_CHECKING: ) from litellm.types.utils import ModelInfo as ModelMapInfo -_T: Final = TypeVar("_T") -_P: Final = ParamSpec("_P") +_T = TypeVar("_T") +_P = ParamSpec("_P") DEPRECATED_ROUTER_MEMBER_USES: Final[Counter[str]] = Counter() # mutable-ok: the live tally _note increments @@ -831,7 +831,7 @@ class DeprecatedRouterMembers: # Router-family functions and classes that took the Router (``litellm_router_instance=``, # ``router=``, ...) take its RoutingContext now. These keep the old spellings working. -_RoutingCallable: Final = TypeVar("_RoutingCallable", bound=Callable[..., object]) +_RoutingCallable = TypeVar("_RoutingCallable", bound=Callable[..., object]) @functools.cache diff --git a/litellm/router_rust/invoke/streaming.py b/litellm/router_rust/invoke/streaming.py index d8b4f87501a..7b6dbd69a3a 100644 --- a/litellm/router_rust/invoke/streaming.py +++ b/litellm/router_rust/invoke/streaming.py @@ -82,7 +82,7 @@ MAX_HELD_PRE_OUTPUT_RESPONSES_EVENTS: Final = 200 # The hidden params and headers a fallback's response carries, as dicts the proxy writes into. _Prepared: TypeAlias = "tuple[dict[str, object], dict[str, object]]" -_RestT: Final = TypeVar("_RestT", Iterator[object], AsyncIterator[object]) +_RestT = TypeVar("_RestT", Iterator[object], AsyncIterator[object]) # --- the attempt a stream came from ------------------------------------------------------------- @@ -289,7 +289,7 @@ def _record_now(recording: Callable[[], object]) -> None: verbose_router_logger.warning("a stream's outcome was not recorded: %s", error) -_ItemT: Final = TypeVar("_ItemT") +_ItemT = TypeVar("_ItemT") async def _areported( diff --git a/litellm/router_utils/endpoint_eligibility.py b/litellm/router_utils/endpoint_eligibility.py index 7f58c138bba..6761b38fc3a 100644 --- a/litellm/router_utils/endpoint_eligibility.py +++ b/litellm/router_utils/endpoint_eligibility.py @@ -38,7 +38,7 @@ def _endpoint_path(endpoint: str) -> str: CHAT_ENDPOINTS: Final = frozenset(_endpoint_path(endpoint) for endpoint in CHAT_ENDPOINT_BY_CALL.values()) -_Deployment: Final = TypeVar("_Deployment", bound=Mapping[str, object]) +_Deployment = TypeVar("_Deployment", bound=Mapping[str, object]) _ENFORCE_FLAG: Final = TypeAdapter(bool | None) _REQUEST_SELECTION: Final[ContextVar[tuple[str | None, str | None]]] = ContextVar( "router_request_endpoint", default=(None, None) diff --git a/litellm/router_utils/kubernetes_pod_discovery.py b/litellm/router_utils/kubernetes_pod_discovery.py index 34d99db1f6e..d2cdbe7ae9f 100644 --- a/litellm/router_utils/kubernetes_pod_discovery.py +++ b/litellm/router_utils/kubernetes_pod_discovery.py @@ -41,7 +41,7 @@ _SyncLookup: TypeAlias = Future[list[_AddressInfo]] _AsyncLookup: TypeAlias = asyncio.Future[list[_AddressInfo]] _CacheKey: TypeAlias = tuple[str, int | None] _PodSelection: TypeAlias = tuple[str, int, Literal["round_robin", "session_affinity", "session_affinity_retry"]] -_DeploymentT: Final = TypeVar("_DeploymentT") +_DeploymentT = TypeVar("_DeploymentT") @dataclass(frozen=True, slots=True) @@ -564,9 +564,9 @@ class _HasPodDiscovery(Protocol): kubernetes_pod_discovery: KubernetesPodDiscovery -_P: Final = ParamSpec("_P") -_R: Final = TypeVar("_R") -_S: Final = TypeVar("_S", bound=_HasPodDiscovery) +_P = ParamSpec("_P") +_R = TypeVar("_R") +_S = TypeVar("_S", bound=_HasPodDiscovery) def _request_kwargs_from_call( diff --git a/litellm/rust_bridge/diagnostics.py b/litellm/rust_bridge/diagnostics.py index 90a5672a500..5d27c3947ce 100644 --- a/litellm/rust_bridge/diagnostics.py +++ b/litellm/rust_bridge/diagnostics.py @@ -6,7 +6,7 @@ from typing import Final, Protocol, TypeVar, cast from litellm.rust_bridge.bindings import NativeBinding -ResultT: Final = TypeVar("ResultT") +ResultT = TypeVar("ResultT") class NativeDiagnosticProcessor(Protocol): diff --git a/litellm/rust_bridge/dispatch.py b/litellm/rust_bridge/dispatch.py index ca98fe2b5b2..a6b75818fe4 100644 --- a/litellm/rust_bridge/dispatch.py +++ b/litellm/rust_bridge/dispatch.py @@ -9,9 +9,9 @@ from litellm.rust_bridge import catalog, runtime from litellm.rust_bridge.bindings import NativeBinding from litellm.rust_bridge.catalog import Route, RouteContext, Rules -RequestT: Final = TypeVar("RequestT") -NativeT: Final = TypeVar("NativeT") -ResultT: Final = TypeVar("ResultT") +RequestT = TypeVar("RequestT") +NativeT = TypeVar("NativeT") +ResultT = TypeVar("ResultT") NativeHook: TypeAlias = Callable[[RequestT, tuple[object, ...], Mapping[str, object]], ResultT] diff --git a/litellm/rust_bridge/execution.py b/litellm/rust_bridge/execution.py index 430de8d1ad6..12782718bd1 100644 --- a/litellm/rust_bridge/execution.py +++ b/litellm/rust_bridge/execution.py @@ -5,7 +5,7 @@ from contextlib import ExitStack from dataclasses import dataclass from typing import Final, Protocol, TypeVar, cast # noqa: TID251 # trusts the native stub at the binding boundary -_Call: Final = TypeVar("_Call") +_Call = TypeVar("_Call") @dataclass(frozen=True, slots=True) diff --git a/litellm/rust_bridge/public_call.py b/litellm/rust_bridge/public_call.py index 8625ca71bb2..268e34d8928 100644 --- a/litellm/rust_bridge/public_call.py +++ b/litellm/rust_bridge/public_call.py @@ -105,7 +105,7 @@ def native_call(legacy: inspect.Signature, args: tuple[object, ...], kwargs: Map return NativeCall(args=args, kwargs=kwargs, base=_without_bags(legacy, positional.arguments)) -NativeResultT: Final = TypeVar("NativeResultT") +NativeResultT = TypeVar("NativeResultT") def native_call_hook( diff --git a/litellm/rust_bridge/response_metadata.py b/litellm/rust_bridge/response_metadata.py index d2ddf61f7db..814ccd73357 100644 --- a/litellm/rust_bridge/response_metadata.py +++ b/litellm/rust_bridge/response_metadata.py @@ -5,7 +5,7 @@ from litellm.litellm_core_utils.add_retry_fallback_headers import ( get_hidden_params_dict, ) -ResultT: Final = TypeVar("ResultT") +ResultT = TypeVar("ResultT") def mark_rust_response(response: ResultT) -> ResultT: diff --git a/litellm/telemetry/aggregate.py b/litellm/telemetry/aggregate.py index f875026c74d..f56ff210b5e 100644 --- a/litellm/telemetry/aggregate.py +++ b/litellm/telemetry/aggregate.py @@ -12,8 +12,8 @@ QUIET_RETRIES: Final = 3 _LOGGER: Final = logging.getLogger(__name__) -_Key: Final = TypeVar("_Key") -_Row: Final = TypeVar("_Row") +_Key = TypeVar("_Key") +_Row = TypeVar("_Row") class AggregatingSink: diff --git a/litellm/tracing/queries.py b/litellm/tracing/queries.py index 15e9ee1f15a..fd74ac92256 100644 --- a/litellm/tracing/queries.py +++ b/litellm/tracing/queries.py @@ -27,8 +27,8 @@ class ClickHouseSQLEnvelope(LiteLLMBaseModel): statistics: TraceQueryStatistics -ParamsT: Final = TypeVar("ParamsT", bound=BaseModel) -RowT: Final = TypeVar("RowT") +ParamsT = TypeVar("ParamsT", bound=BaseModel) +RowT = TypeVar("RowT") class QueryResponse(LiteLLMBaseModel, Generic[RowT]): diff --git a/litellm/tracing/storage.py b/litellm/tracing/storage.py index 1f1296364f4..9d5cc0ac066 100644 --- a/litellm/tracing/storage.py +++ b/litellm/tracing/storage.py @@ -16,7 +16,7 @@ _TRACE_PAGE: Final = TypeAdapter(TracePage) _TRACE: Final[TypeAdapter[Trace | None]] = TypeAdapter(Trace | None) _SPAN_DETAIL: Final[TypeAdapter[SpanDetail | None]] = TypeAdapter(SpanDetail | None) _SPAN_ERROR_PAGE: Final[TypeAdapter[SpanErrorPage | None]] = TypeAdapter(SpanErrorPage | None) -_ResponseT: Final = TypeVar("_ResponseT") +_ResponseT = TypeVar("_ResponseT") def _decode_query_response(adapter: TypeAdapter[_ResponseT], body: str) -> _ResponseT: diff --git a/litellm/types/router.py b/litellm/types/router.py index c0b182f0f20..2c0fa6717b9 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -547,7 +547,7 @@ _RESERVED_INIT_KEYS: Final = frozenset({"self", "params", "__class__"}) _STRICT_LITELLM_PARAMS: Final = MappingProxyType({"strict_litellm_params": True}) -_StrictlyValidatedParams: Final = TypeVar("_StrictlyValidatedParams", bound=BaseModel) +_StrictlyValidatedParams = TypeVar("_StrictlyValidatedParams", bound=BaseModel) def validate_litellm_params_strictly( diff --git a/litellm/utils.py b/litellm/utils.py index 13f22ddc36e..44953f4e138 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -813,7 +813,7 @@ _OIDC_TOKEN_FETCH_EXECUTOR: Final = ThreadPoolExecutor(thread_name_prefix="litel _PASS_THROUGH_CALL_TYPES: Final = frozenset( {CallTypes.allm_passthrough_route.value, CallTypes.llm_passthrough_route.value} ) -_Resolved: Final = TypeVar("_Resolved") +_Resolved = TypeVar("_Resolved") async def _resolved_off_the_event_loop(resolve: Callable[[OidcResolver], _Resolved], api_key: object) -> _Resolved: @@ -4074,7 +4074,7 @@ def is_generalized_model_info(model_info: ModelInfoBase) -> bool: _REGISTRATION_ATTEMPTS: Final = 3 -_ModelInfoT: Final = TypeVar("_ModelInfoT", bound=ModelInfoBase) +_ModelInfoT = TypeVar("_ModelInfoT", bound=ModelInfoBase) def _lookup_mapped_model_info(lookup: Callable[[], _ModelInfoT]) -> _ModelInfoT | ModelInfoLookupMiss: diff --git a/scripts/check_type_discipline.py b/scripts/check_type_discipline.py index cafec8d249b..2bf9d209e0d 100644 --- a/scripts/check_type_discipline.py +++ b/scripts/check_type_discipline.py @@ -54,8 +54,11 @@ LIT010 Variable assignment without a `Final` declaration. Every local and modul that namespace is the SDK's runtime-settable config surface (users follow the documented `litellm.api_key = ...` pattern and the proxy rebinds these via setattr), and the package ships py.typed, so a Final there turns every - documented downstream assignment into a mypy error. Suppress a deliberately - rebindable name with `# rebind-ok: ` on each offending line. + documented downstream assignment into a mypy error. Type variables are the + reverse: a `TypeVar`, `ParamSpec`, or `TypeVarTuple` stays bare, and a Final one + trips, since mypy reads it as a plain variable and every signature using it as + Any. Suppress a deliberately rebindable name with `# rebind-ok: ` on + each offending line. LIT011 Function-argument mutation: a parameter that is re-bound (`param = ...`, `param += ...`, a `for`/`with`/unpacking/walrus target, `del param`, or a re-bind in a nested function under `nonlocal param`) or mutated in place @@ -458,6 +461,7 @@ def iter_guard_violations(path: Path, tree: ast.AST) -> Iterator[Violation]: # --------------------------------------------------------------------------- # CONSTANT_DECLARATIONS = frozenset(("Final", "TypeAlias")) +TYPE_VARIABLE_FACTORIES = frozenset(("TypeVar", "ParamSpec", "TypeVarTuple")) CONFIG_SURFACE_PARTS = ("litellm", "__init__.py") SELF_PARAMS = frozenset(("self", "cls")) NESTED_SCOPES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda) @@ -480,6 +484,14 @@ def _declares_constant(annotation: ast.expr) -> bool: return isinstance(root, ast.Attribute) and root.attr in CONSTANT_DECLARATIONS +def _creates_type_variable(value: ast.expr | None) -> bool: + match value: + case ast.Call(func=ast.Name(id=name) | ast.Attribute(attr=name)): + return name in TYPE_VARIABLE_FACTORIES + case _: + return False + + def _defaults(args: ast.arguments) -> tuple[ast.expr, ...]: return (*args.defaults, *(d for d in args.kw_defaults if d is not None)) @@ -561,6 +573,8 @@ def _bound_names(target: ast.expr) -> Iterator[tuple[str, int]]: def _node_bindings(node: ast.AST, in_loop: bool) -> Iterator[Binding]: match node: + case ast.Assign(targets=[ast.Name(id=name, lineno=line)], value=value) if _creates_type_variable(value): + yield Binding(name, line, "other", in_loop) case ast.Assign(targets=targets): for target in targets: if isinstance(target, ast.Name): @@ -569,7 +583,7 @@ def _node_bindings(node: ast.AST, in_loop: bool) -> Iterator[Binding]: yield from (Binding(n, line, "unpack", in_loop) for n, line in _bound_names(target)) case ast.AnnAssign(target=ast.Name(id=name, lineno=line), annotation=annotation, value=value): if _declares_constant(annotation): - yield Binding(name, line, "declared", in_loop) + yield Binding(name, line, "final_type_variable" if _creates_type_variable(value) else "declared", in_loop) elif value is not None: yield Binding(name, line, "annassign", in_loop) case ast.AugAssign(target=ast.Name(id=name, lineno=line)): @@ -646,6 +660,15 @@ def iter_final_violations(path: Path, tree: ast.AST) -> Iterator[Violation]: declared = frozenset(b.name for b in bindings if b.form == "declared") first = _first_binding_index(bindings) for i, b in enumerate(bindings): + if b.form == "final_type_variable": + yield Violation( + path, + b.line, + "LIT010", + f"`{b.name}` is a type variable declared Final: mypy then reads it as a plain " + f"variable and every signature using it as Any, so remove the Final", + ) + continue if b.name in declared or b.name in params or b.in_loop: continue if _exempt_final_name(b.name): diff --git a/tests/code_coverage_tests/check_router_constructor_stub.py b/tests/code_coverage_tests/check_router_constructor_stub.py new file mode 100644 index 00000000000..85e5a43c0e7 --- /dev/null +++ b/tests/code_coverage_tests/check_router_constructor_stub.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Fail unless mypy reads the Router constructor stub in litellm/router.py. + +litellm ships py.typed, so a stub that mypy reads as Any accepts every argument name in SDK users' code, +while basedpyright, which `make lint-e2e-basedpyright` runs, can still read it. +""" + +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Final + +PROBE: Final = """\ +from litellm.router import Router + +Router(model_list=[], num_retries=2) +Router(model_list=[], redis_host="localhost", redis_port="6379") +Router(model_list=[], not_a_parameter=1) +""" +EXPECTED_ERROR: Final = 'Unexpected keyword argument "not_a_parameter" for "Router"' + + +def mypy_errors(probe: Path) -> tuple[str, ...]: + completed: Final = subprocess.run( + ( + sys.executable, + "-I", + "-m", + "mypy", + "--config-file=", + "--follow-imports=skip", + "--ignore-missing-imports", + "litellm/router.py", + str(probe), + ), + capture_output=True, + text=True, + check=False, + ) + return tuple(line for line in completed.stdout.splitlines() if ": error: " in line) + + +def main() -> int: + with tempfile.TemporaryDirectory() as directory: + probe: Final = Path(directory) / "router_constructor_probe.py" + probe.write_text(PROBE) + errors: Final = mypy_errors(probe) + if len(errors) == 1 and EXPECTED_ERROR in errors[0]: + print("mypy reads the Router constructor stub") + return 0 + print(f"expected only {EXPECTED_ERROR!r} from mypy, got: {errors}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/e2e/AGENTS.md b/tests/e2e/AGENTS.md index a4c51a53098..b4035045c8d 100644 --- a/tests/e2e/AGENTS.md +++ b/tests/e2e/AGENTS.md @@ -131,7 +131,7 @@ Current limits: Bedrock cannot be mounted in record or replay (SigV4 signs the H ## Typing -The harness is fully typed with no error budget: `make lint-e2e-basedpyright` must report zero basedpyright errors, and CI enforces that on any PR touching `tests/e2e/**/*.py`. When a response field is untyped, model it in `models.py` (just the fields you read) and let pydantic validate it, rather than threading a `dict` or `Any` through the test +The harness is fully typed with no error budget: `make lint-e2e-basedpyright` must report zero basedpyright errors, and CI enforces that on any PR touching `tests/e2e/**/*.py`, `tests/e2e_harness/**/*.py`, `litellm/**/*.py`, `pyrightconfig.json`, `pyproject.toml` or `uv.lock`. When a response field is untyped, model it in `models.py` (just the fields you read) and let pydantic validate it, rather than threading a `dict` or `Any` through the test ## Typed test metadata diff --git a/tests/e2e/CONTRIBUTING.md b/tests/e2e/CONTRIBUTING.md index 4f0c7624e31..6d94db48861 100644 --- a/tests/e2e/CONTRIBUTING.md +++ b/tests/e2e/CONTRIBUTING.md @@ -224,7 +224,7 @@ Mark live tests with `@pytest.mark.e2e` (on the class or the module). Pure cover Before you push -1. Run `make lint-e2e-basedpyright` (or `make check` with your changes staged); the harness is fully typed and the gate allows zero basedpyright errors, enforced in CI on any PR touching `tests/e2e/**/*.py` or `tests/e2e_harness/**/*.py` +1. Run `make lint-e2e-basedpyright` (or `make check` with your changes staged); the harness is fully typed and the gate allows zero basedpyright errors, enforced in CI on any PR touching `tests/e2e/**/*.py`, `tests/e2e_harness/**/*.py`, `litellm/**/*.py`, `pyrightconfig.json`, `pyproject.toml`, or `uv.lock` 2. Add the models your test needs to the config your local proxy loads diff --git a/tests/integration/compatibility/test_openai_sdk_client_paths.py b/tests/integration/compatibility/test_openai_sdk_client_paths.py index d6b6d1d460d..46cf986e498 100644 --- a/tests/integration/compatibility/test_openai_sdk_client_paths.py +++ b/tests/integration/compatibility/test_openai_sdk_client_paths.py @@ -78,7 +78,7 @@ SPEECH_UPSTREAM_MODEL: Final = "gpt-4o-mini-tts" PAYMENT_REQUIRED_DEPLOYMENTS: Final[Mapping[str, str]] = MappingProxyType( {"openai": "openai/gpt-4o-mini", "anthropic": "anthropic/claude-sonnet-4-5"} ) -_R: Final = TypeVar("_R") +_R = TypeVar("_R") def _identity(prefix: str) -> str: diff --git a/tests/integration/sdk/test_openai_sdk_client_sessions_wire.py b/tests/integration/sdk/test_openai_sdk_client_sessions_wire.py index e8ec1a37082..c67b65fdc66 100644 --- a/tests/integration/sdk/test_openai_sdk_client_sessions_wire.py +++ b/tests/integration/sdk/test_openai_sdk_client_sessions_wire.py @@ -21,7 +21,7 @@ from litellm import Router from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.types.utils import ImageResponse, ModelResponse, TextCompletionResponse -_R: Final = TypeVar("_R") +_R = TypeVar("_R") _PROVIDER_KEY: Final = "sk-scripted-provider" _API_VERSION: Final = "2024-10-21" diff --git a/tests/unit/test_check_type_discipline.py b/tests/unit/test_check_type_discipline.py index abb8c0d6a9f..78f5bde5249 100644 --- a/tests/unit/test_check_type_discipline.py +++ b/tests/unit/test_check_type_discipline.py @@ -303,6 +303,24 @@ def test_single_leading_underscore_is_not_exempt(tmp_path): assert "LIT010" in _codes(tmp_path, "_x = 1\n") +@pytest.mark.parametrize( + "declaration", + ("TypeVar('_T')", "ParamSpec('_T')", "TypeVarTuple('_T')", "typing.TypeVar('_T')", "typing_extensions.ParamSpec('_T')"), +) +def test_a_type_variable_stays_bare_and_a_final_one_trips(tmp_path, declaration): + assert "LIT010" not in _codes(tmp_path, f"_T = {declaration}\n") + assert "LIT010" in _codes(tmp_path, f"_T: Final = {declaration}\n") + assert "LIT010" in _codes(tmp_path, f"_T: Final[object] = {declaration}\n") + + +def test_rebinding_a_bare_type_variable_still_trips(tmp_path): + assert "LIT010" in _codes(tmp_path, "_T = TypeVar('_T')\n_T = 1\n") + + +def test_a_final_call_to_any_other_factory_stays_clean(tmp_path): + assert "LIT010" not in _codes(tmp_path, "limit: Final = int('1')\n") + + def test_rebind_ok_suppresses_a_repeated_unpack(tmp_path): src = "a, b = (1, 2)\na, b = (b, a) # rebind-ok: swap in place\n" assert "LIT010" not in _codes(tmp_path, src)