diff --git a/aidial_adapter_openai/audio_api/transcribe/adapter.py b/aidial_adapter_openai/audio_api/transcribe/adapter.py index 44560f38..0a445486 100644 --- a/aidial_adapter_openai/audio_api/transcribe/adapter.py +++ b/aidial_adapter_openai/audio_api/transcribe/adapter.py @@ -43,7 +43,7 @@ def _get_usage( ) -> TokenUsage | None: # NOTE: whisper has completely different API for its responses duration: Any | None = getattr(chunk, "duration", None) - if duration is not None and isinstance(duration, (float, int)): + if duration is not None and isinstance(duration, float | int): return TokenUsage(prompt_tokens=int(duration)) usage_dict: dict | None = getattr(chunk, "usage", None) # type: ignore diff --git a/aidial_adapter_openai/audio_api/transcribe/prompt.py b/aidial_adapter_openai/audio_api/transcribe/prompt.py index 2a9f9e98..ede2858c 100644 --- a/aidial_adapter_openai/audio_api/transcribe/prompt.py +++ b/aidial_adapter_openai/audio_api/transcribe/prompt.py @@ -1,7 +1,7 @@ from __future__ import annotations import mimetypes -from typing import Any, List +from typing import Any from aidial_sdk.exceptions import RequestValidationError from pydantic import BaseModel @@ -50,7 +50,7 @@ async def from_request( system_message = _collect_system_messages(messages) - audios: List[FileResource] = [] + audios: list[FileResource] = [] for message in result: for file in message.files: diff --git a/aidial_adapter_openai/chat_completions/gpt.py b/aidial_adapter_openai/chat_completions/gpt.py index 80503b54..2b4fae78 100644 --- a/aidial_adapter_openai/chat_completions/gpt.py +++ b/aidial_adapter_openai/chat_completions/gpt.py @@ -1,4 +1,4 @@ -from typing import AsyncIterator, Callable, Coroutine, List, Mapping, Tuple +from collections.abc import AsyncIterator, Callable, Coroutine, Mapping from aidial_sdk.exceptions import InvalidRequestError from openai import AsyncAzureOpenAI, AsyncOpenAI, AsyncStream @@ -29,10 +29,10 @@ async def multi_modal_truncate_prompt( request: dict, - messages: List[MultiModalMessage], + messages: list[MultiModalMessage], max_prompt_tokens: int, tokenizer: Tokenizer, -) -> Tuple[List[MultiModalMessage], DiscardedMessages, TruncatedTokens]: +) -> tuple[list[MultiModalMessage], DiscardedMessages, TruncatedTokens]: return await truncate_prompt( messages=messages, message_tokens=tokenizer.tokenize_request_message, @@ -62,9 +62,9 @@ def _extract_max_prompt_tokens(request: dict) -> int | None: async def _truncate_messages( - request: dict, messages: List[MultiModalMessage], tokenizer: Tokenizer -) -> Tuple[ - List[MultiModalMessage], + request: dict, messages: list[MultiModalMessage], tokenizer: Tokenizer +) -> tuple[ + list[MultiModalMessage], DiscardedMessages | None, Callable[[], Coroutine[None, None, TruncatedTokens]], ]: @@ -109,7 +109,7 @@ async def chat_completion( eliminate_empty_choices: bool, ) -> ResponseWithHeaders[AsyncIterator[dict] | dict]: n: int = request.get("n") or 1 - messages: List[dict] = request["messages"] + messages: list[dict] = request["messages"] model_name = request["model"] multi_modal_messages = await ResourceProcessor( diff --git a/aidial_adapter_openai/chat_completions/gpt_audio.py b/aidial_adapter_openai/chat_completions/gpt_audio.py index f308959b..b3cc8e43 100644 --- a/aidial_adapter_openai/chat_completions/gpt_audio.py +++ b/aidial_adapter_openai/chat_completions/gpt_audio.py @@ -6,7 +6,8 @@ DIAL-compatible format with attachments and stages. """ -from typing import AsyncIterator, TypeVar +from collections.abc import AsyncIterator +from typing import TypeVar from pydantic import BaseModel diff --git a/aidial_adapter_openai/chat_completions/gpt_oss.py b/aidial_adapter_openai/chat_completions/gpt_oss.py index 2c8e75bf..7c44a03e 100644 --- a/aidial_adapter_openai/chat_completions/gpt_oss.py +++ b/aidial_adapter_openai/chat_completions/gpt_oss.py @@ -18,7 +18,8 @@ https://cookbook.openai.com/articles/gpt-oss/handle-raw-cot """ -from typing import AsyncIterator, Set, TypeVar +from collections.abc import AsyncIterator +from typing import TypeVar from pydantic import BaseModel @@ -26,7 +27,7 @@ class _ResponseTransformer(BaseModel): - opened_reasoning_stages: Set[int] = set() + opened_reasoning_stages: set[int] = set() """Indices of choices where a reasoning stage was open. """ diff --git a/aidial_adapter_openai/chat_completions/non_gpt.py b/aidial_adapter_openai/chat_completions/non_gpt.py index 4a1fac3d..470c42f7 100644 --- a/aidial_adapter_openai/chat_completions/non_gpt.py +++ b/aidial_adapter_openai/chat_completions/non_gpt.py @@ -1,4 +1,4 @@ -from typing import AsyncIterator +from collections.abc import AsyncIterator from openai import AsyncAzureOpenAI, AsyncOpenAI, AsyncStream from openai.types.chat.chat_completion import ChatCompletion diff --git a/aidial_adapter_openai/chat_completions/transformation.py b/aidial_adapter_openai/chat_completions/transformation.py index 9625d078..d8c60e18 100644 --- a/aidial_adapter_openai/chat_completions/transformation.py +++ b/aidial_adapter_openai/chat_completions/transformation.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Set, assert_never +from typing import assert_never from aidial_sdk.exceptions import InvalidRequestError from openai.types.chat import ( @@ -46,15 +46,15 @@ class Error: class MessageTransformer: file_storage: FileStorage | None - errors: Set[Error] - images: List[ImageResource] - files: List[FileResource] + errors: set[Error] + images: list[ImageResource] + files: list[FileResource] def __init__( self, *, file_storage: FileStorage | None, - errors: Set[Error] | None = None, + errors: set[Error] | None = None, ): self.file_storage = file_storage self.errors = set() if errors is None else errors @@ -81,12 +81,12 @@ async def try_download_resource( return None async def download_attachments( - self, attachments: List[dict] - ) -> List[ChatCompletionContentPartImageParam | File]: + self, attachments: list[dict] + ) -> list[ChatCompletionContentPartImageParam | File]: if attachments: logger.debug(f"original attachments: {attachments}") - ret: List[ChatCompletionContentPartImageParam | File] = [] + ret: list[ChatCompletionContentPartImageParam | File] = [] for attachment in attachments: if result := await self.download_attachment(attachment): ret.append(result) @@ -145,15 +145,15 @@ async def download_content( self, content: ( str - | List[ChatCompletionContentPartParam | ContentArrayOfContentPart] + | list[ChatCompletionContentPartParam | ContentArrayOfContentPart] ), - ) -> List[ChatCompletionContentPartParam | ContentArrayOfContentPart]: + ) -> list[ChatCompletionContentPartParam | ContentArrayOfContentPart]: if isinstance(content, str): parts = [create_text_content_part(content)] else: parts = content - ret: List[ + ret: list[ ChatCompletionContentPartParam | ContentArrayOfContentPart ] = [] for part in parts: @@ -192,9 +192,9 @@ class ResourceProcessor(BaseModel): file_storage: FileStorage | None async def transform_messages( - self, messages: List[dict] - ) -> List[MultiModalMessage]: - errors: Set[Error] = set() + self, messages: list[dict] + ) -> list[MultiModalMessage]: + errors: set[Error] = set() transformations = [ await MessageTransformer( file_storage=self.file_storage, errors=errors diff --git a/aidial_adapter_openai/completions.py b/aidial_adapter_openai/completions.py index e78a8abe..03dd6083 100644 --- a/aidial_adapter_openai/completions.py +++ b/aidial_adapter_openai/completions.py @@ -1,4 +1,5 @@ -from typing import Any, AsyncIterator, Dict +from collections.abc import AsyncIterator +from typing import Any from aidial_sdk.exceptions import RequestValidationError from openai import AsyncAzureOpenAI, AsyncOpenAI, AsyncStream @@ -18,7 +19,7 @@ def sanitize_text(text: str) -> str: def convert_to_chat_completions_response( chunk: Completion, is_stream: bool -) -> Dict[str, Any]: +) -> dict[str, Any]: converted_chunk = build_chunk( id=chunk.id, model=chunk.model, @@ -37,7 +38,7 @@ def convert_to_chat_completions_response( async def chat_completion( *, - request: Dict[str, Any], + request: dict[str, Any], client: AsyncAzureOpenAI | AsyncOpenAI, prompt_template: str | None, ) -> AsyncIterator[dict] | dict: diff --git a/aidial_adapter_openai/configuration/app_config.py b/aidial_adapter_openai/configuration/app_config.py index e69ad6bf..f71f1cd3 100644 --- a/aidial_adapter_openai/configuration/app_config.py +++ b/aidial_adapter_openai/configuration/app_config.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from typing import Dict, List, assert_never +from typing import assert_never from aidial_sdk.exceptions import InternalServerError @@ -39,23 +39,23 @@ class DeploymentAPIType(ExtraForbidModel): class ApplicationConfig(ExtraForbidModel): - TIKTOKEN_MODEL_MAPPING: Dict[str, str] = {} + TIKTOKEN_MODEL_MAPPING: dict[str, str] = {} - DALLE3_DEPLOYMENTS: List[str] = [] + DALLE3_DEPLOYMENTS: list[str] = [] DALLE3_AZURE_API_VERSION: str = "2024-02-01" - GPT_IMAGE_1_DEPLOYMENTS: List[str] = [] + GPT_IMAGE_1_DEPLOYMENTS: list[str] = [] GPT_IMAGE_1_AZURE_API_VERSION: str = "2025-04-01-preview" - MISTRAL_DEPLOYMENTS: List[str] = [] - DATABRICKS_DEPLOYMENTS: List[str] = [] - GPT4O_DEPLOYMENTS: List[str] = [] - GPT4O_MINI_DEPLOYMENTS: List[str] = [] - AZURE_AI_VISION_DEPLOYMENTS: List[str] = [] + MISTRAL_DEPLOYMENTS: list[str] = [] + DATABRICKS_DEPLOYMENTS: list[str] = [] + GPT4O_DEPLOYMENTS: list[str] = [] + GPT4O_MINI_DEPLOYMENTS: list[str] = [] + AZURE_AI_VISION_DEPLOYMENTS: list[str] = [] - API_VERSIONS_MAPPING: Dict[str, str] = {} - COMPLETION_DEPLOYMENTS_PROMPT_TEMPLATES: Dict[str, str] = {} - NON_STREAMING_DEPLOYMENTS: List[str] = [] + API_VERSIONS_MAPPING: dict[str, str] = {} + COMPLETION_DEPLOYMENTS_PROMPT_TEMPLATES: dict[str, str] = {} + NON_STREAMING_DEPLOYMENTS: list[str] = [] ELIMINATE_EMPTY_CHOICES: bool = False AUDIO_AZURE_API_VERSION: str = "2025-03-01-preview" diff --git a/aidial_adapter_openai/dial_api/embedding_inputs.py b/aidial_adapter_openai/dial_api/embedding_inputs.py index 6ffe3d6b..f77d8c05 100644 --- a/aidial_adapter_openai/dial_api/embedding_inputs.py +++ b/aidial_adapter_openai/dial_api/embedding_inputs.py @@ -1,8 +1,5 @@ +from collections.abc import AsyncIterator, Callable, Coroutine from typing import ( - AsyncIterator, - Callable, - Coroutine, - List, TypeVar, assert_never, cast, @@ -15,7 +12,7 @@ _T = TypeVar("_T") _Coro = Coroutine[None, None, _T] -_Tokens = List[int] +_Tokens = list[int] async def reject_tokens(tokens: _Tokens): @@ -25,7 +22,7 @@ async def reject_tokens(tokens: _Tokens): ) -async def reject_mixed(input: List[str | Attachment]): +async def reject_mixed(input: list[str | Attachment]): raise RequestValidationError( "Embedding inputs composed of multiple texts and/or attachments aren't supported" ) @@ -37,7 +34,7 @@ async def collect_embedding_inputs( on_text: Callable[[str], _Coro[_T]], on_attachment: Callable[[Attachment], _Coro[_T]], on_tokens: Callable[[_Tokens], _Coro[_T]] = reject_tokens, - on_mixed: Callable[[List[str | Attachment]], _Coro[_T]] = reject_mixed, + on_mixed: Callable[[list[str | Attachment]], _Coro[_T]] = reject_mixed, ) -> AsyncIterator[_T]: async def _on_str_or_attachment(input: str | Attachment) -> _T: if isinstance(input, str): @@ -70,7 +67,7 @@ async def _on_str_or_attachment(input: str | Attachment) -> _T: return for input in request.custom_input: - if isinstance(input, (str, Attachment)): + if isinstance(input, str | Attachment): yield await _on_str_or_attachment(input) elif isinstance(input, list): if len(input) == 0: diff --git a/aidial_adapter_openai/dial_api/request.py b/aidial_adapter_openai/dial_api/request.py index a39621c9..65e143ba 100644 --- a/aidial_adapter_openai/dial_api/request.py +++ b/aidial_adapter_openai/dial_api/request.py @@ -1,4 +1,4 @@ -from typing import Any, Type, TypeVar +from typing import Any, TypeVar from aidial_sdk.exceptions import RequestValidationError from pydantic import BaseModel, ValidationError @@ -6,7 +6,7 @@ _T = TypeVar("_T", bound=BaseModel) -def parse_configuration(cls: Type[_T], data: Any) -> _T | None: +def parse_configuration(cls: type[_T], data: Any) -> _T | None: if (cf := data.get("custom_fields")) is None: return None diff --git a/aidial_adapter_openai/dial_api/resource.py b/aidial_adapter_openai/dial_api/resource.py index 1721e715..26a5fa8b 100644 --- a/aidial_adapter_openai/dial_api/resource.py +++ b/aidial_adapter_openai/dial_api/resource.py @@ -1,7 +1,6 @@ import base64 import mimetypes from abc import ABC, abstractmethod -from typing import List from aidial_sdk.chat_completion import Attachment from pydantic import BaseModel, model_validator @@ -25,9 +24,9 @@ class MissingContentTypeError(ValidationError): class UnsupportedContentTypeError(ValidationError): type: str - supported_types: List[str] + supported_types: list[str] - def __init__(self, *, message: str, type: str, supported_types: List[str]): + def __init__(self, *, message: str, type: str, supported_types: list[str]): self.type = type self.supported_types = supported_types super().__init__(message) @@ -35,7 +34,7 @@ def __init__(self, *, message: str, type: str, supported_types: List[str]): class DialResource(ABC, BaseModel): entity_name: str | None = None - supported_types: List[str] | None = None + supported_types: list[str] | None = None @abstractmethod async def download(self, storage: FileStorage | None) -> Resource: ... diff --git a/aidial_adapter_openai/dial_api/sdk_adapter.py b/aidial_adapter_openai/dial_api/sdk_adapter.py index 9b25cd16..aa924455 100644 --- a/aidial_adapter_openai/dial_api/sdk_adapter.py +++ b/aidial_adapter_openai/dial_api/sdk_adapter.py @@ -1,4 +1,4 @@ -from typing import Callable, Coroutine +from collections.abc import Callable, Coroutine import fastapi from aidial_sdk.chat_completion import Request as DIALRequest diff --git a/aidial_adapter_openai/dial_api/storage.py b/aidial_adapter_openai/dial_api/storage.py index 886527cc..d498020d 100644 --- a/aidial_adapter_openai/dial_api/storage.py +++ b/aidial_adapter_openai/dial_api/storage.py @@ -1,7 +1,7 @@ import base64 import hashlib import mimetypes -from typing import Mapping, Optional +from collections.abc import Mapping from urllib.parse import unquote, urljoin from pydantic import BaseModel, SecretStr @@ -28,7 +28,7 @@ class FileStorage(BaseModel): dial_url: str api_key: SecretStr - bucket: Optional[Bucket] = None + bucket: Bucket | None = None @property def headers(self) -> Mapping[str, str]: @@ -129,14 +129,14 @@ def _compute_hash_digest(file_content: str | bytes) -> str: DIAL_USE_FILE_STORAGE = get_env_bool("DIAL_USE_FILE_STORAGE", False) -DIAL_URL: Optional[str] = None +DIAL_URL: str | None = None if DIAL_USE_FILE_STORAGE: DIAL_URL = get_env( "DIAL_URL", "DIAL_URL must be set to use the DIAL file storage" ) -def create_file_storage(headers: Mapping[str, str]) -> Optional[FileStorage]: +def create_file_storage(headers: Mapping[str, str]) -> FileStorage | None: if not DIAL_USE_FILE_STORAGE or DIAL_URL is None: return None diff --git a/aidial_adapter_openai/embeddings/azure_ai_vision.py b/aidial_adapter_openai/embeddings/azure_ai_vision.py index 65348434..f35fc09f 100644 --- a/aidial_adapter_openai/embeddings/azure_ai_vision.py +++ b/aidial_adapter_openai/embeddings/azure_ai_vision.py @@ -31,7 +31,8 @@ """ import asyncio -from typing import AsyncIterator, List, assert_never +from collections.abc import AsyncIterator +from typing import assert_never import httpx from aidial_sdk.chat_completion.request import Attachment @@ -69,7 +70,7 @@ def _get_auth_headers(creds: OpenAICreds) -> dict[str, str]: class VectorizeResponse(ExtraAllowedModel): - vector: List[float] + vector: list[float] async def embeddings( @@ -95,7 +96,7 @@ async def on_attachment(attachment: Attachment) -> Resource: on_attachment=on_attachment, ) - inputs: List[str | Resource] = [input async for input in inputs_iter] + inputs: list[str | Resource] = [input async for input in inputs_iter] headers = _get_auth_headers(creds) diff --git a/aidial_adapter_openai/endpoints/chat_completion.py b/aidial_adapter_openai/endpoints/chat_completion.py index 193469e3..b9af390b 100644 --- a/aidial_adapter_openai/endpoints/chat_completion.py +++ b/aidial_adapter_openai/endpoints/chat_completion.py @@ -1,4 +1,5 @@ -from typing import Mapping, assert_never +from collections.abc import Mapping +from typing import assert_never import fastapi from fastapi import Request diff --git a/aidial_adapter_openai/exception_handlers.py b/aidial_adapter_openai/exception_handlers.py index a582fb91..bcbc9d4a 100644 --- a/aidial_adapter_openai/exception_handlers.py +++ b/aidial_adapter_openai/exception_handlers.py @@ -24,7 +24,7 @@ def to_adapter_exception(exc: Exception) -> AdapterException: def _convert_to_adapter_exception(exc: Exception) -> AdapterException: - if isinstance(exc, (DialException, ResponseWrapper)): + if isinstance(exc, DialException | ResponseWrapper): return exc if isinstance(exc, httpx.HTTPStatusError): diff --git a/aidial_adapter_openai/image_generation/prompt.py b/aidial_adapter_openai/image_generation/prompt.py index 06516dbb..b6f7a565 100644 --- a/aidial_adapter_openai/image_generation/prompt.py +++ b/aidial_adapter_openai/image_generation/prompt.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, List +from typing import Any from aidial_sdk.exceptions import InvalidRequestError from pydantic import BaseModel @@ -15,7 +15,7 @@ class ImageGenPrompt(BaseModel): text_prompt: str - images: List[Resource] + images: list[Resource] @classmethod async def from_request( @@ -26,7 +26,7 @@ async def from_request( ).transform_messages(data["messages"]) text_prompt = "" - images: List[Resource] = [] + images: list[Resource] = [] for message in result: text_prompt += collect_message_text_content(message.raw_message) diff --git a/aidial_adapter_openai/responses/adapter.py b/aidial_adapter_openai/responses/adapter.py index 1807161c..88bd334e 100644 --- a/aidial_adapter_openai/responses/adapter.py +++ b/aidial_adapter_openai/responses/adapter.py @@ -1,6 +1,7 @@ import json import logging -from typing import Any, AsyncIterator, Dict, List +from collections.abc import AsyncIterator +from typing import Any from aidial_sdk.exceptions import RequestValidationError from openai import ( @@ -36,15 +37,15 @@ ) -def _validate_request(request: Dict[str, Any]) -> None: - errors: List[str] = [] +def _validate_request(request: dict[str, Any]) -> None: + errors: list[str] = [] if (n := request.get("n")) not in [None, 1]: errors.append( f"The deployment doesn't support request.n parameter other than 1, but got {n}." ) - unsupported_params: List[str] = [] + unsupported_params: list[str] = [] for param in [ "stop", "seed", @@ -88,7 +89,7 @@ class ResponsesConfig(ExtraAllowedModel): ) -def _get_configuration(request: Dict[str, Any]) -> ResponsesConfig: +def _get_configuration(request: dict[str, Any]) -> ResponsesConfig: configuration = ( parse_configuration(ResponsesConfig, request) or ResponsesConfig() ) @@ -104,7 +105,7 @@ def _get_configuration(request: Dict[str, Any]) -> ResponsesConfig: async def chat_completion( *, - request: Dict[str, Any], + request: dict[str, Any], client: AsyncAzureOpenAI | AsyncOpenAI, file_storage: FileStorage | None, ) -> AsyncIterator[dict] | dict: diff --git a/aidial_adapter_openai/responses/converter.py b/aidial_adapter_openai/responses/converter.py index 767ebced..301de924 100644 --- a/aidial_adapter_openai/responses/converter.py +++ b/aidial_adapter_openai/responses/converter.py @@ -1,4 +1,5 @@ -from typing import Generator, List, assert_never +from collections.abc import Generator +from typing import assert_never from aidial_sdk.chat_completion.request import CustomContent, Stage, Status from aidial_sdk.exceptions import RequestValidationError @@ -118,7 +119,7 @@ def convert_tool_choice( assert_never(tool_choice) -def convert_tools(tools: List[ChatCompletionToolParam]) -> List[ToolParam]: +def convert_tools(tools: list[ChatCompletionToolParam]) -> list[ToolParam]: def _convert_tool(tool: ChatCompletionToolParam) -> ToolParam: function = tool["function"] return FunctionToolParam( @@ -264,17 +265,17 @@ def _convert_message( def convert_messages( - messages: List[ChatCompletionMessageParam], + messages: list[ChatCompletionMessageParam], ) -> ResponseInputParam: return [ param for message in messages for param in _convert_message(message) ] -def _convert_output(output: List[ResponseOutputItem]) -> ChatCompletionMessage: +def _convert_output(output: list[ResponseOutputItem]) -> ChatCompletionMessage: text_content = "" - annotations: List[Annotation] = [] - tool_calls: List[ChatCompletionMessageToolCall] = [] + annotations: list[Annotation] = [] + tool_calls: list[ChatCompletionMessageToolCall] = [] custom_content: CustomContent | None = None for item in output: @@ -307,7 +308,7 @@ def _convert_output(output: List[ResponseOutputItem]) -> ChatCompletionMessage: case ResponseReasoningItem(summary=summary): if summary: - stages: List[Stage] = [] + stages: list[Stage] = [] for index, summary_part in enumerate(summary): suffix = "" if index == 0 else f" #{index + 1}" stages.append( diff --git a/aidial_adapter_openai/responses/event_handler.py b/aidial_adapter_openai/responses/event_handler.py index 41e8cee4..26a06a16 100644 --- a/aidial_adapter_openai/responses/event_handler.py +++ b/aidial_adapter_openai/responses/event_handler.py @@ -1,6 +1,7 @@ import json import logging -from typing import Dict, Generator, assert_never +from collections.abc import Generator +from typing import assert_never import openai import pydantic @@ -117,7 +118,7 @@ class EventHandler(pydantic.BaseModel): created_: int | None = None model_: str | None = None - tool_calls: Dict[str, int] = {} + tool_calls: dict[str, int] = {} """Map item_id for a tool call onto its index in the chat completion response """ diff --git a/aidial_adapter_openai/utils/adapter_exception.py b/aidial_adapter_openai/utils/adapter_exception.py index 823a1122..d5024639 100644 --- a/aidial_adapter_openai/utils/adapter_exception.py +++ b/aidial_adapter_openai/utils/adapter_exception.py @@ -1,5 +1,6 @@ import json -from typing import Any, MutableMapping +from collections.abc import MutableMapping +from typing import Any from aidial_sdk.exceptions import HTTPException as DialException from fastapi.responses import Response as FastAPIResponse @@ -25,11 +26,7 @@ def __init__( def __repr__(self): # headers field is omitted deliberately # since it may contain sensitive information - return "%s(content=%r, status_code=%r)" % ( - self.__class__.__name__, - self.content, - self.status_code, - ) + return f"{self.__class__.__name__}(content={self.content!r}, status_code={self.status_code!r})" def to_fastapi_response(self) -> FastAPIResponse: return FastAPIResponse( diff --git a/aidial_adapter_openai/utils/auth.py b/aidial_adapter_openai/utils/auth.py index 6815845d..5ddbf767 100644 --- a/aidial_adapter_openai/utils/auth.py +++ b/aidial_adapter_openai/utils/auth.py @@ -1,6 +1,6 @@ import os import time -from typing import Mapping +from collections.abc import Mapping from aidial_sdk.exceptions import HTTPException as DialException from azure.core.credentials import AccessToken diff --git a/aidial_adapter_openai/utils/caching.py b/aidial_adapter_openai/utils/caching.py index b3a7c8ee..76ad7455 100644 --- a/aidial_adapter_openai/utils/caching.py +++ b/aidial_adapter_openai/utils/caching.py @@ -1,5 +1,6 @@ import time -from typing import Any, Callable, Coroutine, Mapping +from collections.abc import Callable, Coroutine, Mapping +from typing import Any _DIAL_CACHE_BREAKPOINT_PATH = "X-DIAL-CACHE-BREAKPOINT-PATH" _DIAL_CACHE_EXPIRE_AT = "X-DIAL-CACHE-EXPIRE-AT" diff --git a/aidial_adapter_openai/utils/chat_completion_response.py b/aidial_adapter_openai/utils/chat_completion_response.py index 968cfa35..1822634d 100644 --- a/aidial_adapter_openai/utils/chat_completion_response.py +++ b/aidial_adapter_openai/utils/chat_completion_response.py @@ -1,4 +1,5 @@ -from typing import Any, Iterable, Literal, Self, Set +from collections.abc import Iterable +from typing import Any, Literal, Self from aidial_sdk.utils.merge_chunks import merge_chat_completion_chunks from pydantic import BaseModel @@ -26,7 +27,7 @@ def messages(self) -> Iterable[Any]: def has_messages(self) -> bool: return len(list(self.messages)) > 0 - def get_missing_finish_reasons(self, n: int) -> Set[int]: + def get_missing_finish_reasons(self, n: int) -> set[int]: missing = set(range(n)) for choice in self.response.get("choices") or []: if choice.get("finish_reason") is not None: diff --git a/aidial_adapter_openai/utils/concurrency.py b/aidial_adapter_openai/utils/concurrency.py index aba88f8f..891dd0a3 100644 --- a/aidial_adapter_openai/utils/concurrency.py +++ b/aidial_adapter_openai/utils/concurrency.py @@ -1,7 +1,8 @@ import asyncio import os +from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor -from typing import Callable, TypeVar +from typing import TypeVar _THREAD_POOL_SIZE = os.getenv("THREAD_POOL_SIZE") _THREAD_POOL_SIZE = ( diff --git a/aidial_adapter_openai/utils/env.py b/aidial_adapter_openai/utils/env.py index 4a2146d9..9c929973 100644 --- a/aidial_adapter_openai/utils/env.py +++ b/aidial_adapter_openai/utils/env.py @@ -1,11 +1,12 @@ import json import os -from typing import Callable, Dict, List, Optional, TypeVar +from collections.abc import Callable +from typing import TypeVar from aidial_adapter_openai.utils.log_config import logger -def get_env(name: str, err_msg: Optional[str] = None) -> str: +def get_env(name: str, err_msg: str | None = None) -> str: if (val := os.getenv(name)) is not None: return val raise Exception(err_msg or f"{name} env variable is not set") @@ -17,13 +18,13 @@ def get_env_bool(name: str, default: bool = False) -> bool: return default -def get_env_list(name: str) -> List[str] | None: +def get_env_list(name: str) -> list[str] | None: if (value := os.getenv(name)) is not None: return list(map(str.strip, (value).split(","))) return None -def get_env_dict(key: str) -> Dict[str, str] | None: +def get_env_dict(key: str) -> dict[str, str] | None: if (value := os.getenv(key)) is not None: try: return json.loads(value) @@ -41,7 +42,7 @@ def get_env_var( parser: Callable[[str], _T], name: str, *, - deprecated_names: List[str] | None = None, + deprecated_names: list[str] | None = None, ) -> _T: for alt in deprecated_names or []: if os.getenv(alt) is not None: diff --git a/aidial_adapter_openai/utils/httpx.py b/aidial_adapter_openai/utils/httpx.py index 61396c8a..4cd32bb1 100644 --- a/aidial_adapter_openai/utils/httpx.py +++ b/aidial_adapter_openai/utils/httpx.py @@ -1,6 +1,6 @@ import logging import time -from typing import Any, Tuple +from typing import Any import httpx @@ -20,7 +20,7 @@ def _elapsed_ms(start_time: float, last_time: float | None = None) -> int: TraceCallback = Any # `httpcore` doesn’t publish a public protocol for this -def _build_trace() -> Tuple[TraceCtx, TraceCallback]: +def _build_trace() -> tuple[TraceCtx, TraceCallback]: trace_ctx: TraceCtx = {} starts: dict[str, float] = {} diff --git a/aidial_adapter_openai/utils/multi_modal_message.py b/aidial_adapter_openai/utils/multi_modal_message.py index 62e1967f..e57b1e65 100644 --- a/aidial_adapter_openai/utils/multi_modal_message.py +++ b/aidial_adapter_openai/utils/multi_modal_message.py @@ -1,5 +1,3 @@ -from typing import List - from openai.types.chat import ChatCompletionContentPartTextParam from openai.types.chat.chat_completion_content_part_param import File from pydantic import BaseModel @@ -24,6 +22,6 @@ def create_text_content_part(text: str) -> ChatCompletionContentPartTextParam: class MultiModalMessage(BaseModel): - images: List[ImageResource] = [] - files: List[FileResource] = [] + images: list[ImageResource] = [] + files: list[FileResource] = [] raw_message: dict diff --git a/aidial_adapter_openai/utils/parsers.py b/aidial_adapter_openai/utils/parsers.py index eb640c61..9e1b1234 100644 --- a/aidial_adapter_openai/utils/parsers.py +++ b/aidial_adapter_openai/utils/parsers.py @@ -1,7 +1,7 @@ import re from http import HTTPStatus from json import JSONDecodeError -from typing import Any, Dict +from typing import Any from aidial_sdk.exceptions import HTTPException, InvalidRequestError from fastapi import Request @@ -122,7 +122,7 @@ def try_parse( azure_video_api_parser = EndpointParser(name="video/generations") -async def parse_body(request: Request) -> Dict[str, Any]: +async def parse_body(request: Request) -> dict[str, Any]: try: data = await request.json() except JSONDecodeError as e: diff --git a/aidial_adapter_openai/utils/reflection.py b/aidial_adapter_openai/utils/reflection.py index effb3f8d..9bddd068 100644 --- a/aidial_adapter_openai/utils/reflection.py +++ b/aidial_adapter_openai/utils/reflection.py @@ -1,6 +1,7 @@ import functools import inspect -from typing import Any, Callable, Coroutine, TypeVar +from collections.abc import Callable, Coroutine +from typing import Any, TypeVar from aidial_sdk.exceptions import InvalidRequestError diff --git a/aidial_adapter_openai/utils/resource/base.py b/aidial_adapter_openai/utils/resource/base.py index caef5a2d..5ace5b6f 100644 --- a/aidial_adapter_openai/utils/resource/base.py +++ b/aidial_adapter_openai/utils/resource/base.py @@ -41,7 +41,7 @@ def to_data_url(self) -> str: return f"{self._to_data_url_prefix(self.type)}{self.data_base64}" @staticmethod - def parse_data_url_content_type(data_url: str) -> Optional[str]: + def parse_data_url_content_type(data_url: str) -> str | None: pattern = r"^data:([^;]+);base64," match = re.match(pattern, data_url) return None if match is None else match.group(1) diff --git a/aidial_adapter_openai/utils/resource/image.py b/aidial_adapter_openai/utils/resource/image.py index b34d6d7c..d6f3c200 100644 --- a/aidial_adapter_openai/utils/resource/image.py +++ b/aidial_adapter_openai/utils/resource/image.py @@ -1,5 +1,5 @@ from io import BytesIO -from typing import Literal, Optional, assert_never +from typing import Literal, assert_never from openai.types.chat import ChatCompletionContentPartImageParam from PIL import Image @@ -39,7 +39,7 @@ class ImageResource(BaseModel): @classmethod def _from_resource( - cls, image: Resource, detail: Optional[ImageDetail] + cls, image: Resource, detail: ImageDetail | None ) -> "ImageResource": with Image.open(BytesIO(image.data)) as img: width, height = img.size @@ -53,7 +53,7 @@ def _from_resource( @classmethod async def from_resource( - cls, image: Resource, detail: Optional[ImageDetail] + cls, image: Resource, detail: ImageDetail | None ) -> "ImageResource": return await run_in_threadpool( lambda: cls._from_resource(image, detail) diff --git a/aidial_adapter_openai/utils/sse_stream.py b/aidial_adapter_openai/utils/sse_stream.py index 3090630e..e52bef23 100644 --- a/aidial_adapter_openai/utils/sse_stream.py +++ b/aidial_adapter_openai/utils/sse_stream.py @@ -1,5 +1,6 @@ import json -from typing import Any, AsyncIterator, Mapping +from collections.abc import AsyncIterator, Mapping +from typing import Any from aidial_adapter_openai.utils.adapter_exception import AdapterException diff --git a/aidial_adapter_openai/utils/streaming.py b/aidial_adapter_openai/utils/streaming.py index f682e4fe..f72dd3ae 100644 --- a/aidial_adapter_openai/utils/streaming.py +++ b/aidial_adapter_openai/utils/streaming.py @@ -1,16 +1,9 @@ import logging +from collections.abc import AsyncIterator, Callable, Coroutine, Generator from dataclasses import dataclass from time import time from typing import ( - AsyncIterator, - Callable, - Coroutine, - Generator, Generic, - List, - Optional, - Set, - Tuple, TypeVar, assert_never, ) @@ -46,8 +39,8 @@ def build_chunk( *, id: str, model: str, - finish_reason: Optional[str], - message: dict | List[dict], + finish_reason: str | None, + message: dict | list[dict], created: int, is_stream: bool, **extra, @@ -81,7 +74,7 @@ async def generate_stream( [ChatCompletionResponse], Coroutine[None, None, int] ], model: str, - discarded_messages: Optional[list[int]], + discarded_messages: list[int] | None, eliminate_empty_choices: bool, ) -> AsyncIterator[dict]: empty_chunk = build_chunk( @@ -117,7 +110,7 @@ async def set_usage( def set_default_finish_reasons( chunk: dict | None, - missing_indices: Set[int], + missing_indices: set[int], default_finish_reason: str, ) -> dict: def _set_reason(choice: dict) -> dict: @@ -220,7 +213,7 @@ def block_response_to_streaming_chunk(response: dict) -> dict: return response -def streaming_chunks_to_block_response(chunks: List[dict]) -> dict: +def streaming_chunks_to_block_response(chunks: list[dict]) -> dict: response = merge_chat_completion_chunks(*chunks) response["object"] = "chat.completion" @@ -306,7 +299,7 @@ async def map_stream_generator( async def map_stream( - func: Callable[[T], Optional[V]], iterator: AsyncIterator[T] + func: Callable[[T], V | None], iterator: AsyncIterator[T] ) -> AsyncIterator[V]: async for item in iterator: new_item = func(item) @@ -325,7 +318,7 @@ async def prepend( async def peek_head( iterator: AsyncIterator[T], -) -> Tuple[T | None, AsyncIterator[T]]: +) -> tuple[T | None, AsyncIterator[T]]: try: val = await iterator.__anext__() return val, iterator diff --git a/aidial_adapter_openai/utils/tokenizer.py b/aidial_adapter_openai/utils/tokenizer.py index ae271430..ed01cb8e 100644 --- a/aidial_adapter_openai/utils/tokenizer.py +++ b/aidial_adapter_openai/utils/tokenizer.py @@ -4,8 +4,9 @@ import json from abc import ABC, abstractmethod +from collections.abc import Callable, Coroutine from functools import cached_property -from typing import Any, Callable, Coroutine, Generic, List, Set, TypeVar +from typing import Any, Generic, TypeVar from aidial_sdk.exceptions import InternalServerError from tiktoken import Encoding, encoding_for_model @@ -118,7 +119,7 @@ def _tokens_per_request_message_name(self) -> int: return 1 async def tokenize_request( - self, original_request: dict, messages: List[MessageType] + self, original_request: dict, messages: list[MessageType] ) -> int: tokens = self.TOKENS_PER_REQUEST @@ -188,7 +189,7 @@ async def _tokenize_message( class Tokenizer(BaseTokenizer[MultiModalMessage]): image_tokenizer: ImageTokenizer | None - warnings: Set[str] + warnings: set[str] def __init__( self, *, model: str, image_tokenizer: ImageTokenizer | None = None @@ -237,7 +238,7 @@ async def tokenize_request_message(self, message: MultiModalMessage) -> int: return tokens async def tokenize_request( - self, original_request: dict, messages: List[MultiModalMessage] + self, original_request: dict, messages: list[MultiModalMessage] ) -> int: tokens = await super().tokenize_request(original_request, messages) diff --git a/aidial_adapter_openai/utils/truncate_prompt.py b/aidial_adapter_openai/utils/truncate_prompt.py index e4e5178c..5ae25a0b 100644 --- a/aidial_adapter_openai/utils/truncate_prompt.py +++ b/aidial_adapter_openai/utils/truncate_prompt.py @@ -1,4 +1,5 @@ -from typing import Callable, Coroutine, List, Set, Tuple, TypeVar +from collections.abc import Callable, Coroutine +from typing import TypeVar from aidial_sdk.exceptions import ( TruncatePromptSystemAndLastUserError, @@ -7,21 +8,21 @@ _T = TypeVar("_T") -DiscardedMessages = List[int] +DiscardedMessages = list[int] TruncatedTokens = int async def truncate_prompt( - messages: List[_T], + messages: list[_T], message_tokens: Callable[[_T], Coroutine[None, None, int]], is_system_message: Callable[[_T], bool], max_prompt_tokens: int, initial_prompt_tokens: int, -) -> Tuple[List[_T], DiscardedMessages, TruncatedTokens]: +) -> tuple[list[_T], DiscardedMessages, TruncatedTokens]: prompt_tokens = initial_prompt_tokens system_messages_count = 0 - kept_messages: Set[int] = set() + kept_messages: set[int] = set() # Count system messages first for idx, message_holder in enumerate(messages): diff --git a/aidial_adapter_openai/utils/validation.py b/aidial_adapter_openai/utils/validation.py index ea40724b..2bffce93 100644 --- a/aidial_adapter_openai/utils/validation.py +++ b/aidial_adapter_openai/utils/validation.py @@ -1,10 +1,10 @@ -from typing import Any, Tuple, Type, Union +from typing import Any from aidial_sdk.exceptions import InvalidRequestError def _ensure_type( - name: str, value: Any, expected: Union[Type, Tuple[Type, ...]] + name: str, value: Any, expected: type | tuple[type, ...] ) -> Any: expected = expected if isinstance(expected, tuple) else (expected,) if isinstance(value, expected): diff --git a/aidial_adapter_openai/video_generation/azure/adapter.py b/aidial_adapter_openai/video_generation/azure/adapter.py index 5b998883..28e34595 100644 --- a/aidial_adapter_openai/video_generation/azure/adapter.py +++ b/aidial_adapter_openai/video_generation/azure/adapter.py @@ -1,5 +1,5 @@ import asyncio -from typing import Any, Dict, List, assert_never +from typing import Any, assert_never import fastapi from aidial_sdk.chat_completion import Choice, Stage @@ -29,15 +29,15 @@ ) -def _validate_request(request: Dict[str, Any]) -> None: - errors: List[str] = [] +def _validate_request(request: dict[str, Any]) -> None: + errors: list[str] = [] if (n := request.get("n")) not in [None, 1]: errors.append( f"The deployment doesn't support request.n parameter other than 1, but got {n}." ) - unsupported_params: List[str] = [] + unsupported_params: list[str] = [] for param in [ "stop", "seed", @@ -95,7 +95,7 @@ async def _poll_job( client: AzureVideoAPIClient, job_id: str, polling_interval: float, -) -> List[VideoGeneration]: +) -> list[VideoGeneration]: while True: await asyncio.sleep(polling_interval) @@ -138,7 +138,7 @@ async def _download_videos( response: DIALResponse, choice: Choice, client: AzureVideoAPIClient, - video_generations: List[VideoGeneration], + video_generations: list[VideoGeneration], storage: FileStorage | None, ): n = len(video_generations) @@ -167,7 +167,7 @@ async def _download_videos( async def chat_completion( *, request: fastapi.Request, - request_body: Dict[str, Any], + request_body: dict[str, Any], creds: OpenAICreds, deployment_id: str, upstream_endpoint: str, diff --git a/aidial_adapter_openai/video_generation/azure/client.py b/aidial_adapter_openai/video_generation/azure/client.py index d1b78e61..989ea675 100644 --- a/aidial_adapter_openai/video_generation/azure/client.py +++ b/aidial_adapter_openai/video_generation/azure/client.py @@ -1,5 +1,5 @@ import json -from typing import Dict, List, Literal, NoReturn, Self +from typing import Literal, NoReturn, Self import httpx from aidial_sdk.exceptions import InternalServerError, InvalidRequestError @@ -23,7 +23,7 @@ class VideoGenerationJob(BaseModel): id: str status: JobStatus - generations: List[VideoGeneration] | None = None + generations: list[VideoGeneration] | None = None failure_reason: ( str | Literal["input_moderation", "internal_error"] | None ) = None @@ -57,7 +57,7 @@ def _client(self) -> httpx.AsyncClient: return get_http_client() @property - def _headers(self) -> Dict[str, str]: + def _headers(self) -> dict[str, str]: headers = {"Content-Type": "application/json"} if key := self.creds.get("api_key"): headers["api-key"] = key @@ -66,7 +66,7 @@ def _headers(self) -> Dict[str, str]: return headers @property - def _params(self) -> Dict[str, str]: + def _params(self) -> dict[str, str]: return {"api-version": "preview"} @property diff --git a/aidial_adapter_openai/video_generation/azure/prompt.py b/aidial_adapter_openai/video_generation/azure/prompt.py index 86a59953..54d719db 100644 --- a/aidial_adapter_openai/video_generation/azure/prompt.py +++ b/aidial_adapter_openai/video_generation/azure/prompt.py @@ -1,7 +1,7 @@ from __future__ import annotations import mimetypes -from typing import Any, List, Tuple +from typing import Any from aidial_sdk.exceptions import InvalidRequestError from httpx._types import RequestFiles @@ -21,7 +21,7 @@ class VideoGenPrompt(BaseModel): prompt: str - resources: List[Resource] + resources: list[Resource] @classmethod async def from_request( @@ -40,7 +40,7 @@ async def from_request( ).transform_messages([last_message]) )[0] - resources: List[Resource] = [] + resources: list[Resource] = [] for image in multi_modal_message.images: resources.append(image.image) @@ -49,7 +49,7 @@ async def from_request( return cls(prompt=prompt, resources=resources) - def get_files(self) -> Tuple[List[InpaintItem], RequestFiles]: + def get_files(self) -> tuple[list[InpaintItem], RequestFiles]: items, files = [], [] image_idx = 1 diff --git a/aidial_adapter_openai/video_generation/azure/types.py b/aidial_adapter_openai/video_generation/azure/types.py index 26ac0eec..5f65c673 100644 --- a/aidial_adapter_openai/video_generation/azure/types.py +++ b/aidial_adapter_openai/video_generation/azure/types.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import List from pydantic import BaseModel, RootModel @@ -41,7 +40,7 @@ class InpaintItem(BaseModel): class InpaintItems(RootModel): - root: List[InpaintItem] + root: list[InpaintItem] class CreateVideoGenerationRequest(BaseModel): @@ -68,7 +67,7 @@ def create( height: int, n_seconds: int | None, n_variants: int | None, - inpaint_items: List[InpaintItem] | None, + inpaint_items: list[InpaintItem] | None, ) -> "CreateVideoGenerationRequest": inpaint_items_str = ( InpaintItems(inpaint_items).model_dump_json() diff --git a/pyproject.toml b/pyproject.toml index 809675f0..4c3f5e15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ filterwarnings = [ "ignore::DeprecationWarning:opentelemetry.instrumentation.dependencies", "ignore::DeprecationWarning:pkg_resources", # Workaround for this bug: https://github.com/openai/openai-python/issues/1039 - "ignore:Pydantic serializer warnings:UserWarning:pydantic.*" + "ignore:Pydantic serializer warnings:UserWarning:pydantic.*", ] [tool.pyright] @@ -98,6 +98,7 @@ select = [ "I", # flake8-isort "S", # flake8-bandit "FURB", # refurb + "UP", # pyupgrade ] ignore = [ "E501", # string literal is too long diff --git a/tests/integration_tests/base.py b/tests/integration_tests/base.py index 5030faee..75a7c6da 100644 --- a/tests/integration_tests/base.py +++ b/tests/integration_tests/base.py @@ -2,13 +2,10 @@ import functools import json +from collections.abc import Callable, Generator from typing import ( Any, - Callable, - Dict, - Generator, Generic, - List, Literal, TypeVar, assert_never, @@ -55,17 +52,17 @@ class Features(ExtraAllowedModel): class ModelConfig(ExtraAllowedModel): type: Literal["chat", "embedding"] overrideName: str | None = None - upstreams: List[UpstreamConfig] + upstreams: list[UpstreamConfig] features: Features = Features() - inputAttachmentTypes: List[str] | None = None + inputAttachmentTypes: list[str] | None = None class CoreConfig(ExtraAllowedModel): - models: Dict[str, ModelConfig] + models: dict[str, ModelConfig] @classmethod def from_config(cls, config_path: str): - with open(config_path, "r") as f: + with open(config_path) as f: test_config = json.load(f) return cls(**test_config) @@ -102,14 +99,14 @@ class DeploymentConfig(BaseModel, Generic[_T]): model_name: str model_features: Features - model_attachments: List[str] | None + model_attachments: list[str] | None upstream_endpoint: str upstream_api_key: str | None upstream_idx: int | None @property - def upstream_headers(self) -> Dict[str, str]: + def upstream_headers(self) -> dict[str, str]: headers = {"X-UPSTREAM-ENDPOINT": self.upstream_endpoint} if self.upstream_api_key is not None: headers["X-UPSTREAM-KEY"] = self.upstream_api_key @@ -120,7 +117,7 @@ def create_deployments( cls, core_config: CoreConfig, get_deployment_type: Callable[[ModelConfig, str, str], _T], - ) -> List[DeploymentConfig[_T]]: + ) -> list[DeploymentConfig[_T]]: configs = [] for deployment_id, model_config in core_config.models.items(): for upstream_index, upstream_config in enumerate( @@ -187,7 +184,7 @@ def supports_reasoning(self): class TestDeployments(BaseModel): __test__ = False - deployments: List[DeploymentConfig[DeploymentType]] + deployments: list[DeploymentConfig[DeploymentType]] app_config: ApplicationConfig @classmethod @@ -224,7 +221,7 @@ def sanitize_id_part(value: Any) -> str: """Convert any value to a pytest-safe identifier part.""" if isinstance(value, bool): return "on" if value else "off" - if isinstance(value, (int, float)): + if isinstance(value, int | float): return str(value).replace(".", "p") # e.g., 0.5 -> 0p5 if value is None: return "none" diff --git a/tests/integration_tests/chat_completion/response_format.py b/tests/integration_tests/chat_completion/response_format.py index 7423741b..5f07f2fd 100644 --- a/tests/integration_tests/chat_completion/response_format.py +++ b/tests/integration_tests/chat_completion/response_format.py @@ -1,5 +1,5 @@ import json -from typing import Any, List +from typing import Any from openai.types.chat import ChatCompletionMessageParam @@ -20,7 +20,7 @@ def build_response_format(s: TestSuite) -> None: else: be_brief = {"max_tokens": 32} - messages: List[ChatCompletionMessageParam] = [ + messages: list[ChatCompletionMessageParam] = [ user("extract name and surname from 'John Doe' in json format") ] @@ -31,7 +31,7 @@ def build_response_format(s: TestSuite) -> None: response_format={"type": "json_object"}, **be_brief, expected=lambda r: isinstance( - _assert_valid_json(r.content), (dict, list) + _assert_valid_json(r.content), dict | list ), ) diff --git a/tests/integration_tests/chat_completion/test_case.py b/tests/integration_tests/chat_completion/test_case.py index e8dcadd6..6d0230dd 100644 --- a/tests/integration_tests/chat_completion/test_case.py +++ b/tests/integration_tests/chat_completion/test_case.py @@ -1,7 +1,7 @@ from __future__ import annotations +from collections.abc import Callable, Iterator from dataclasses import dataclass, field -from typing import Callable, Iterator, List from openai import NOT_GIVEN, NotGiven from openai.types import ReasoningEffort @@ -27,18 +27,18 @@ class TestCase: name: str streaming: bool - messages: List[ChatCompletionMessageParam] + messages: list[ChatCompletionMessageParam] expected: Callable[[ChatCompletionResult], bool] | ExpectedException max_tokens: int | NotGiven max_completion_tokens: int | NotGiven - stop: List[str] | NotGiven + stop: list[str] | NotGiven n: int | NotGiven - functions: List[Function] | NotGiven - tools: List[ChatCompletionToolParam] | NotGiven + functions: list[Function] | NotGiven + tools: list[ChatCompletionToolParam] | NotGiven temperature: float | NotGiven reasoning_effort: ReasoningEffort | NotGiven @@ -68,19 +68,19 @@ class TestSuite: deployment_config: DeploymentConfig[ChatCompletionDeploymentType] streaming: bool - test_cases: List[TestCase] = field(default_factory=list) + test_cases: list[TestCase] = field(default_factory=list) def test_case( self, *, name: str, - messages: List[ChatCompletionMessageParam], + messages: list[ChatCompletionMessageParam], max_tokens: int | NotGiven = NOT_GIVEN, max_completion_tokens: int | NotGiven = NOT_GIVEN, - stop: List[str] | NotGiven = NOT_GIVEN, + stop: list[str] | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, - functions: List[Function] | NotGiven = NOT_GIVEN, - tools: List[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, + functions: list[Function] | NotGiven = NOT_GIVEN, + tools: list[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, reasoning_effort: ReasoningEffort | NotGiven = NOT_GIVEN, response_format: ResponseFormat | NotGiven = NOT_GIVEN, diff --git a/tests/integration_tests/test_audio.py b/tests/integration_tests/test_audio.py index 493eaab2..9b6c364d 100644 --- a/tests/integration_tests/test_audio.py +++ b/tests/integration_tests/test_audio.py @@ -1,5 +1,5 @@ +from collections.abc import Callable from pathlib import Path -from typing import Callable, List from unittest.mock import patch import openai @@ -41,7 +41,7 @@ def mock_storage(request): D = DeploymentConfig[ChatCompletionDeploymentType] -_tts_deployments: List[D] = [ +_tts_deployments: list[D] = [ d for d in TEST_DEPLOYMENTS_CONFIG.chat_deployments if d.supports_tts ] @@ -58,7 +58,7 @@ def tts_deployment(request) -> D: pytest.skip("No TTS deployments were found") -_stt_deployments: List[D] = [ +_stt_deployments: list[D] = [ d for d in TEST_DEPLOYMENTS_CONFIG.chat_deployments if d.supports_stt ] diff --git a/tests/integration_tests/test_chat_completion.py b/tests/integration_tests/test_chat_completion.py index 77e0ddc9..a0d0aaed 100644 --- a/tests/integration_tests/test_chat_completion.py +++ b/tests/integration_tests/test_chat_completion.py @@ -1,6 +1,6 @@ import logging import re -from typing import Generator, List +from collections.abc import Generator import openai import pytest @@ -34,7 +34,7 @@ def create_test_cases( - builders: List[TestSuiteBuilder], + builders: list[TestSuiteBuilder], ) -> Generator[TestCase, None, None]: for streaming in (False, True): for deployment in TEST_DEPLOYMENTS_CONFIG.chat_deployments: diff --git a/tests/integration_tests/test_embeddings.py b/tests/integration_tests/test_embeddings.py index 539c4d57..3014a76e 100644 --- a/tests/integration_tests/test_embeddings.py +++ b/tests/integration_tests/test_embeddings.py @@ -1,5 +1,3 @@ -from typing import List - import openai import pytest @@ -15,7 +13,7 @@ D = DeploymentConfig[EmbeddingsDeploymentType] -_deployments: List[D] = list(TEST_DEPLOYMENTS_CONFIG.embedding_deployments) +_deployments: list[D] = list(TEST_DEPLOYMENTS_CONFIG.embedding_deployments) @pytest.fixture diff --git a/tests/integration_tests/test_image_generation.py b/tests/integration_tests/test_image_generation.py index 53e085b2..8648f61a 100644 --- a/tests/integration_tests/test_image_generation.py +++ b/tests/integration_tests/test_image_generation.py @@ -1,5 +1,5 @@ +from collections.abc import Callable from pathlib import Path -from typing import Callable, List from unittest.mock import patch import openai @@ -33,7 +33,7 @@ def mock_storage(request): D = DeploymentConfig[ChatCompletionDeploymentType] -_deployments: List[D] = [ +_deployments: list[D] = [ d for d in TEST_DEPLOYMENTS_CONFIG.chat_deployments if d.model_features.imageGenerationSupported @@ -54,7 +54,7 @@ def imagen_deployment(request) -> D: @pytest.fixture def vision_deployment() -> D: - vision_deployments: List[D] = [ + vision_deployments: list[D] = [ d for d in TEST_DEPLOYMENTS_CONFIG.chat_deployments if d.supports_vision and not d.supports_reasoning diff --git a/tests/integration_tests/test_video_generation.py b/tests/integration_tests/test_video_generation.py index f8dda8b4..a4a46951 100644 --- a/tests/integration_tests/test_video_generation.py +++ b/tests/integration_tests/test_video_generation.py @@ -1,5 +1,5 @@ +from collections.abc import Callable from pathlib import Path -from typing import Callable, List from unittest.mock import patch import openai @@ -33,7 +33,7 @@ def mock_storage(request): D = DeploymentConfig[ChatCompletionDeploymentType] -_deployments: List[D] = [ +_deployments: list[D] = [ d for d in TEST_DEPLOYMENTS_CONFIG.chat_deployments if d.supports_video_generation diff --git a/tests/unit_tests/test_caching.py b/tests/unit_tests/test_caching.py index dc91bb47..8abcffef 100644 --- a/tests/unit_tests/test_caching.py +++ b/tests/unit_tests/test_caching.py @@ -1,6 +1,5 @@ import dataclasses import json -from typing import List import httpx import pytest @@ -14,7 +13,7 @@ from tests.utils.stream import OpenAIStream, single_choice_chunk -def _mock_response(upstream_url: str, stream: bool, chunks: List[dict]): +def _mock_response(upstream_url: str, stream: bool, chunks: list[dict]): mock_stream = OpenAIStream(*chunks) if stream: respx.post(upstream_url).respond( diff --git a/tests/unit_tests/test_discard_messages.py b/tests/unit_tests/test_discard_messages.py index 35ee8b55..08058644 100644 --- a/tests/unit_tests/test_discard_messages.py +++ b/tests/unit_tests/test_discard_messages.py @@ -1,5 +1,3 @@ -from typing import List, Tuple - import pytest from aidial_sdk.exceptions import HTTPException as DialException @@ -13,21 +11,21 @@ TruncatedTokens, ) -PlainTextMessages = List[dict] +PlainTextMessages = list[dict] MaxPromptTokens = int -TestCase = Tuple[ +TestCase = tuple[ PlainTextMessages, MaxPromptTokens, - Tuple[PlainTextMessages, DiscardedMessages], + tuple[PlainTextMessages, DiscardedMessages], ] async def plain_text_truncate_prompt( request: dict, - messages: List[dict], + messages: list[dict], max_prompt_tokens: int, tokenizer: Tokenizer, -) -> Tuple[List[dict], DiscardedMessages, TruncatedTokens]: +) -> tuple[list[dict], DiscardedMessages, TruncatedTokens]: (msgs, disc, tokens) = await multi_modal_truncate_prompt( request=request, messages=[MultiModalMessage(raw_message=m) for m in messages], @@ -39,7 +37,7 @@ async def plain_text_truncate_prompt( return (msgs, disc, tokens) -normal_cases: List[TestCase] = [ +normal_cases: list[TestCase] = [ ( [], 3, @@ -124,8 +122,8 @@ async def plain_text_truncate_prompt( ] ErrorMessage = str -error_cases: List[ - Tuple[ +error_cases: list[ + tuple[ PlainTextMessages, MaxPromptTokens, ErrorMessage, @@ -159,9 +157,9 @@ async def plain_text_truncate_prompt( @pytest.mark.parametrize("messages, max_prompt_tokens, response", normal_cases) async def test_discarded_messages_without_error( - messages: List[dict], + messages: list[dict], max_prompt_tokens: int, - response: Tuple[List[dict], List[int]], + response: tuple[list[dict], list[int]], ): tokenizer = Tokenizer(model="gpt-4") ( @@ -178,7 +176,7 @@ async def test_discarded_messages_without_error( "messages, max_prompt_tokens, error_message", error_cases ) async def test_discarded_messages_with_error( - messages: List[dict], + messages: list[dict], max_prompt_tokens: int, error_message: str, ): diff --git a/tests/unit_tests/test_errors.py b/tests/unit_tests/test_errors.py index 71d65390..fcd71077 100644 --- a/tests/unit_tests/test_errors.py +++ b/tests/unit_tests/test_errors.py @@ -1,5 +1,6 @@ import json -from typing import Any, AsyncIterable, AsyncIterator, Callable +from collections.abc import AsyncIterable, AsyncIterator, Callable +from typing import Any from unittest.mock import patch import httpx diff --git a/tests/unit_tests/test_gpt_audio.py b/tests/unit_tests/test_gpt_audio.py index 33c0b6ba..b3a35e5a 100644 --- a/tests/unit_tests/test_gpt_audio.py +++ b/tests/unit_tests/test_gpt_audio.py @@ -1,6 +1,7 @@ """Unit tests for GPT-4o audio completions support.""" -from typing import Any, AsyncIterator, Callable +from collections.abc import AsyncIterator, Callable +from typing import Any import pytest diff --git a/tests/unit_tests/test_image_tokenization.py b/tests/unit_tests/test_image_tokenization.py index 05d3117b..d351c587 100644 --- a/tests/unit_tests/test_image_tokenization.py +++ b/tests/unit_tests/test_image_tokenization.py @@ -1,11 +1,9 @@ -from typing import List, Tuple - import pytest from aidial_adapter_openai.utils.image_tokenizer import GPT4O_IMAGE_TOKENIZER from aidial_adapter_openai.utils.resource.image import ImageDetail -test_cases: List[Tuple[int, int, ImageDetail, int]] = [ +test_cases: list[tuple[int, int, ImageDetail, int]] = [ (1, 1, "auto", 85), (1, 1, "high", 170 * 1 + 85), (100, 100, "low", 85), diff --git a/tests/utils/dictionary.py b/tests/utils/dictionary.py index e5f7b87a..1f964955 100644 --- a/tests/utils/dictionary.py +++ b/tests/utils/dictionary.py @@ -1,4 +1,4 @@ -from typing import Iterable +from collections.abc import Iterable def exclude_keys(d: dict, keys: Iterable[str]) -> dict: diff --git a/tests/utils/json.py b/tests/utils/json.py index 896a2af1..7299682a 100644 --- a/tests/utils/json.py +++ b/tests/utils/json.py @@ -6,7 +6,7 @@ def match_objects(expected: Any, actual: Any) -> bool: assert sorted(expected.keys()) == sorted(actual.keys()) for k, v in expected.items(): match_objects(v, actual[k]) - elif isinstance(expected, (tuple, list)): + elif isinstance(expected, tuple | list): assert len(expected) == len(actual) for i in range(len(expected)): match_objects(expected[i], actual[i]) diff --git a/tests/utils/openai.py b/tests/utils/openai.py index 3a76603b..7a8d9eb6 100644 --- a/tests/utils/openai.py +++ b/tests/utils/openai.py @@ -1,5 +1,6 @@ import json -from typing import Any, Callable, List +from collections.abc import Callable +from typing import Any from aidial_sdk.utils.merge_chunks import ( cleanup_indices, @@ -53,13 +54,13 @@ def ai_function( def ai_tools( - tool_calls: List[ChatCompletionMessageToolCallParam], + tool_calls: list[ChatCompletionMessageToolCallParam], ) -> ChatCompletionAssistantMessageParam: return {"role": "assistant", "tool_calls": tool_calls} def user( - content: str | List[ChatCompletionContentPartParam], + content: str | list[ChatCompletionContentPartParam], **kwargs, ) -> ChatCompletionUserMessageParam: return {"role": "user", "content": content, **kwargs} # type: ignore @@ -186,7 +187,7 @@ def content(self) -> str: return self.message.content or "" @property - def contents(self) -> List[str]: + def contents(self) -> list[str]: return [ choice.message.content or "" for choice in self.response.choices ] @@ -200,10 +201,10 @@ def function_call(self) -> FunctionCall | None: return self.message.function_call @property - def tool_calls(self) -> List[ChatCompletionMessageToolCall] | None: + def tool_calls(self) -> list[ChatCompletionMessageToolCall] | None: return self.message.tool_calls - def content_contains_all(self, matches: List[Any]) -> bool: + def content_contains_all(self, matches: list[Any]) -> bool: return all( str(match).lower() in self.content.lower() for match in matches ) @@ -213,14 +214,14 @@ async def chat_completion( client: AsyncAzureOpenAI, *, deployment_id: str, - messages: List[ChatCompletionMessageParam], + messages: list[ChatCompletionMessageParam], stream: bool, - stop: List[str] | NotGiven = NOT_GIVEN, + stop: list[str] | NotGiven = NOT_GIVEN, max_completion_tokens: int | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, - functions: List[Function] | NotGiven = NOT_GIVEN, - tools: List[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, + functions: list[Function] | NotGiven = NOT_GIVEN, + tools: list[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, temperature: float | NotGiven = NOT_GIVEN, reasoning_effort: ReasoningEffort | NotGiven = NOT_GIVEN, response_format: ResponseFormat | NotGiven = NOT_GIVEN, @@ -246,7 +247,7 @@ async def get_response() -> ChatCompletion: ) if isinstance(response, AsyncStream): - chunks: List[dict] = [ + chunks: list[dict] = [ {} ] # workaround for https://github.com/epam/ai-dial-sdk/pull/269 async for chunk in response: @@ -300,7 +301,7 @@ def is_valid_function_call( def is_valid_tool_call( - calls: List[ChatCompletionMessageToolCall] | None, + calls: list[ChatCompletionMessageToolCall] | None, tool_call_idx: int, check_tool_id: Callable[[str], bool], expected_name: str, diff --git a/tests/utils/storage.py b/tests/utils/storage.py index 0b852929..01c3d331 100644 --- a/tests/utils/storage.py +++ b/tests/utils/storage.py @@ -1,7 +1,6 @@ import mimetypes import os from pathlib import Path -from typing import List from urllib.parse import urlparse from pydantic import SecretStr @@ -42,7 +41,7 @@ async def download_file(self, link: str) -> bytes: class MockFileStorage(FileStorage): root_dir: Path - files: List[Path] + files: list[Path] @classmethod def create(cls, root_dir: Path) -> "MockFileStorage": diff --git a/tests/utils/stream.py b/tests/utils/stream.py index d3db7e33..82c1eb63 100644 --- a/tests/utils/stream.py +++ b/tests/utils/stream.py @@ -1,5 +1,6 @@ import json -from typing import Any, Callable, List +from collections.abc import Callable +from typing import Any import httpx @@ -9,7 +10,7 @@ class OpenAIStream: - chunks: List[dict] + chunks: list[dict] def __init__(self, *chunks: dict): self.chunks = list(chunks) @@ -55,7 +56,7 @@ def chunk( object_: str = "chat.completion.chunk", created: int = 1695940483, model: str = "gpt-4", - choices: List[dict], + choices: list[dict], usage: dict | None = None, **kwargs, ) -> dict: @@ -125,7 +126,7 @@ def many_choices_chunk( created: int = 1695940483, model: str = "gpt-4", usage: dict | None = None, - choices: List[dict] | None = None, + choices: list[dict] | None = None, **kwargs, ) -> dict: return chunk(