-
-
Notifications
You must be signed in to change notification settings - Fork 11.8k
feat(provider): add ModelScope as an OpenAI-compatible provider #28460
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
97894f1
f6ce4e1
84bc74a
1b9acac
76d880a
4b35750
1e966c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| """ | ||
| Translates from OpenAI's `/v1/chat/completions` to ModelScope's `/v1/chat/completions` | ||
| """ | ||
|
|
||
| from typing import Any, Coroutine, List, Literal, Optional, Tuple, Union, cast, overload | ||
|
|
||
| from litellm.secret_managers.main import get_secret_str | ||
| from litellm.types.llms.openai import AllMessageValues | ||
|
|
||
| from ...openai.chat.gpt_transformation import OpenAIGPTConfig | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def _has_non_text_content(message: AllMessageValues) -> bool: | ||
| """Check if a message has non-text content items (e.g. image_url).""" | ||
| content = message.get("content") | ||
| if not isinstance(content, list): | ||
| return False | ||
| return any(item.get("type") != "text" for item in content) | ||
|
|
||
|
|
||
| class ModelScopeChatConfig(OpenAIGPTConfig): | ||
| DEFAULT_BASE_URL: str = "https://api-inference.modelscope.cn/v1" | ||
|
|
||
| @overload | ||
| def _transform_messages( | ||
| self, messages: List[AllMessageValues], model: str, is_async: Literal[True] | ||
| ) -> Coroutine[Any, Any, List[AllMessageValues]]: ... | ||
|
|
||
| @overload | ||
| def _transform_messages( | ||
| self, | ||
| messages: List[AllMessageValues], | ||
| model: str, | ||
| is_async: Literal[False] = False, | ||
| ) -> List[AllMessageValues]: ... | ||
|
|
||
| def _transform_messages( | ||
| self, messages: List[AllMessageValues], model: str, is_async: bool = False | ||
| ) -> Union[List[AllMessageValues], Coroutine[Any, Any, List[AllMessageValues]]]: | ||
| """ | ||
| Flatten text-only content lists to strings for ModelScope. | ||
|
|
||
| Messages with non-text content (e.g. image_url for vision models) | ||
| are kept as lists so the parent class can normalize them properly. | ||
| """ | ||
| messages = [cast(AllMessageValues, {**m}) for m in messages] | ||
| for message in messages: | ||
| if _has_non_text_content(message): | ||
| continue | ||
| content = message.get("content") | ||
| if isinstance(content, list): | ||
| message["content"] = "".join(item.get("text") or "" for item in content) | ||
|
|
||
| if is_async: | ||
| return super()._transform_messages( | ||
| messages=messages, model=model, is_async=True | ||
| ) | ||
| else: | ||
| return super()._transform_messages( | ||
| messages=messages, model=model, is_async=False | ||
| ) | ||
|
|
||
| def _get_openai_compatible_provider_info( | ||
| self, api_base: Optional[str], api_key: Optional[str] | ||
| ) -> Tuple[Optional[str], Optional[str]]: | ||
| api_base = ( | ||
| api_base or get_secret_str("MODELSCOPE_API_BASE") or self.DEFAULT_BASE_URL | ||
| ) # type: ignore | ||
| dynamic_api_key = api_key or get_secret_str("MODELSCOPE_API_KEY") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High: API key disclosure via custom api_base
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is consistent with the existing pattern used by other OpenAI-compatible providers like dashscope, moonshot, and deepseek. So I think there is no need to modify. |
||
| return api_base, dynamic_api_key | ||
|
|
||
| def get_complete_url( | ||
| self, | ||
| api_base: Optional[str], | ||
| api_key: Optional[str], | ||
| model: str, | ||
| optional_params: dict, | ||
| litellm_params: dict, | ||
| stream: Optional[bool] = None, | ||
| ) -> str: | ||
| """ | ||
| If api_base is not provided, use the default ModelScope /chat/completions endpoint. | ||
| """ | ||
| if not api_base: | ||
| api_base = self.DEFAULT_BASE_URL | ||
|
|
||
| if not api_base.endswith("/chat/completions"): | ||
| api_base = f"{api_base}/chat/completions" | ||
|
|
||
| return api_base | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| """ | ||
| ModelScope Image Generation Module | ||
|
|
||
| Factory function for getting the appropriate config class. | ||
| """ | ||
|
|
||
| from litellm.llms.base_llm.image_generation.transformation import ( | ||
| BaseImageGenerationConfig, | ||
| ) | ||
|
|
||
| from .transformation import ModelScopeImageGenerationConfig | ||
|
|
||
| __all__ = [ | ||
| "ModelScopeImageGenerationConfig", | ||
| "get_modelscope_image_generation_config", | ||
| ] | ||
|
|
||
|
|
||
| def get_modelscope_image_generation_config( | ||
| model: str, | ||
| ) -> BaseImageGenerationConfig: | ||
| """ | ||
| Get the ModelScope config for image generation. | ||
|
|
||
| Args: | ||
| model: The model name (e.g., "modelscope/Qwen/Qwen-Image-Edit") | ||
|
|
||
| Returns: | ||
| BaseImageGenerationConfig instance for ModelScope | ||
| """ | ||
| return ModelScopeImageGenerationConfig() |
Uh oh!
There was an error while loading. Please reload this page.