From 40f3248f6b76baf1231f9ebef2a0d897984e95b7 Mon Sep 17 00:00:00 2001 From: Pratik Khulge Date: Mon, 29 Jun 2026 18:00:47 +0530 Subject: [PATCH] feat(moonshot): add Files API support to complete end-to-end batch flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Batch requires uploading a JSONL file first via /v1/files before calling create_batch. Without Files API support, the complete batch workflow was broken for Moonshot. Adds "moonshot" to all five file operation type literals (create, retrieve, delete, list, content) and a dedicated _get_moonshot_file_credentials() helper in files/main.py that applies the same api_base security guard as the batch branches — caller-supplied api_base is only honoured when the caller also supplies their own api_key, preventing the server-side MOONSHOT_API_KEY from being forwarded to a caller-controlled endpoint. All five operations delegate to openai_files_instance since Moonshot's files API is OpenAI-compatible. --- litellm/files/main.py | 107 +++++++++++++++++++++++++++++++++++++++-- litellm/files/types.py | 4 +- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/litellm/files/main.py b/litellm/files/main.py index 3b359b55fe3..842fdcdd8cc 100644 --- a/litellm/files/main.py +++ b/litellm/files/main.py @@ -25,10 +25,13 @@ FileCreateProvider = Literal[ "hosted_vllm", "manus", "anthropic", + "moonshot", ] -FileRetrieveProvider = Literal["openai", "azure", "gemini", "vertex_ai", "hosted_vllm", "manus", "anthropic"] -FileDeleteProvider = Literal["openai", "azure", "gemini", "manus", "anthropic"] -FileListProvider = Literal["openai", "azure", "manus", "anthropic"] +FileRetrieveProvider = Literal[ + "openai", "azure", "gemini", "vertex_ai", "hosted_vllm", "manus", "anthropic", "moonshot" +] +FileDeleteProvider = Literal["openai", "azure", "gemini", "manus", "anthropic", "moonshot"] +FileListProvider = Literal["openai", "azure", "manus", "anthropic", "moonshot"] import litellm from litellm import get_secret_str from litellm.files.streaming import FileContentStreamingResponse @@ -84,6 +87,34 @@ bedrock_files_instance = BedrockFilesHandler() ################################################# +def _get_moonshot_file_credentials( + caller_api_key: str | None, + caller_api_base: str | None, +) -> tuple[str, str]: + """Resolve Moonshot api_key and api_base for file operations. + + Only trust the caller's api_base when they supply their own api_key. + When falling back to the server-side MOONSHOT_API_KEY the base URL is + resolved from server config so the key is never forwarded to a + caller-controlled endpoint. + """ + default_base = get_secret_str("MOONSHOT_API_BASE") or "https://api.moonshot.ai/v1" + if caller_api_key: + api_key = caller_api_key + api_base = caller_api_base or default_base + else: + server_key = get_secret_str("MOONSHOT_API_KEY") + if not server_key: + raise litellm.exceptions.AuthenticationError( + message="No Moonshot API key found. Pass api_key or set MOONSHOT_API_KEY in the environment.", + model="n/a", + llm_provider="moonshot", + ) + api_key = server_key + api_base = default_base + return api_key, api_base + + def _add_trusted_model_credentials_to_litellm_params( litellm_params_dict: Dict[str, Any], kwargs: Dict[str, Any] ) -> None: @@ -227,6 +258,20 @@ def create_file( organization=openai_creds.organization, create_file_data=_create_file_request, ) + elif custom_llm_provider == "moonshot": + api_key, api_base = _get_moonshot_file_credentials( + caller_api_key=optional_params.api_key, + caller_api_base=optional_params.api_base, + ) + response = openai_files_instance.create_file( + _is_async=_is_async, + api_base=api_base, + api_key=api_key, + timeout=timeout, + max_retries=optional_params.max_retries, + organization=None, + create_file_data=_create_file_request, + ) elif custom_llm_provider == "azure": azure_creds = get_azure_credentials( api_base=optional_params.api_base, @@ -350,6 +395,20 @@ def file_retrieve( max_retries=optional_params.max_retries, organization=openai_creds.organization, ) + elif custom_llm_provider == "moonshot": + api_key, api_base = _get_moonshot_file_credentials( + caller_api_key=optional_params.api_key, + caller_api_base=optional_params.api_base, + ) + response = openai_files_instance.retrieve_file( + file_id=file_id, + _is_async=_is_async, + api_base=api_base, + api_key=api_key, + timeout=timeout, + max_retries=optional_params.max_retries, + organization=None, + ) elif custom_llm_provider == "azure": azure_creds = get_azure_credentials( api_base=optional_params.api_base, @@ -533,6 +592,20 @@ def file_delete( max_retries=optional_params.max_retries, organization=openai_creds.organization, ) + elif custom_llm_provider == "moonshot": + api_key, api_base = _get_moonshot_file_credentials( + caller_api_key=optional_params.api_key, + caller_api_base=optional_params.api_base, + ) + response = openai_files_instance.delete_file( + file_id=file_id, + _is_async=_is_async, + api_base=api_base, + api_key=api_key, + timeout=timeout, + max_retries=optional_params.max_retries, + organization=None, + ) elif custom_llm_provider == "azure": azure_creds = get_azure_credentials( api_base=optional_params.api_base, @@ -737,6 +810,20 @@ def file_list( max_retries=optional_params.max_retries, organization=openai_creds.organization, ) + elif custom_llm_provider == "moonshot": + api_key, api_base = _get_moonshot_file_credentials( + caller_api_key=optional_params.api_key, + caller_api_base=optional_params.api_base, + ) + response = openai_files_instance.list_files( + purpose=purpose, + _is_async=_is_async, + api_base=api_base, + api_key=api_key, + timeout=timeout, + max_retries=optional_params.max_retries, + organization=None, + ) elif custom_llm_provider == "azure": azure_creds = get_azure_credentials( api_base=optional_params.api_base, @@ -939,6 +1026,20 @@ def file_content( max_retries=optional_params.max_retries, organization=openai_creds.organization, ) + elif custom_llm_provider == "moonshot": + api_key, api_base = _get_moonshot_file_credentials( + caller_api_key=optional_params.api_key, + caller_api_base=optional_params.api_base, + ) + response = openai_files_instance.file_content( + _is_async=_is_async, + file_content_request=_file_content_request, + api_base=api_base, + api_key=api_key, + timeout=timeout, + max_retries=optional_params.max_retries, + organization=None, + ) elif custom_llm_provider == "azure": azure_creds = get_azure_credentials( api_base=optional_params.api_base, diff --git a/litellm/files/types.py b/litellm/files/types.py index 6bf7b1a1cc2..d07fb6d71b7 100644 --- a/litellm/files/types.py +++ b/litellm/files/types.py @@ -1,6 +1,8 @@ from typing import AsyncIterator, Dict, Iterator, Literal, NamedTuple, Union -FileContentProvider = Literal["openai", "azure", "vertex_ai", "bedrock", "hosted_vllm", "anthropic", "manus"] +FileContentProvider = Literal[ + "openai", "azure", "vertex_ai", "bedrock", "hosted_vllm", "anthropic", "manus", "moonshot" +] class FileContentStreamingResult(NamedTuple):