diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index f937e4d2f75..feb22675892 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -290,6 +290,14 @@ "supports_function_calling": true } }, + "apiko": { + "base_url": "https://api.apiko.app/v1", + "api_key_env": "APIKO_API_KEY", + "api_base_env": "APIKO_API_BASE", + "supported_endpoints": [ + "/v1/chat/completions" + ] + }, "streamlake": { "base_url": "https://vanchin.streamlake.ai/api/gateway/v1/endpoints", "api_key_env": "STREAMLAKE_API_KEY", diff --git a/litellm/main.py b/litellm/main.py index 6b9964a450f..b4abb6e16bf 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -2792,9 +2792,17 @@ def _complete_custom_openai( raise litellm.AuthenticationError( message="Set BIGMODEL_API_KEY or pass api_key", llm_provider="bigmodel", model=model ) + if custom_llm_provider == "apiko" and not api_key: + raise litellm.AuthenticationError( + message="Set APIKO_API_KEY or pass api_key", llm_provider="apiko", model=model + ) organization = ( organization - or (None if custom_llm_provider == "bigmodel" else litellm.organization or get_secret("OPENAI_ORGANIZATION")) + or ( + None + if custom_llm_provider in ("bigmodel", "apiko") + else litellm.organization or get_secret("OPENAI_ORGANIZATION") + ) or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105 ) openai.organization = organization diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index 00fee0a0d5a..955769cd166 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -5484,6 +5484,32 @@ "generateContent": true } }, + "apiko": { + "display_name": "APIKO (`apiko`)", + "url": "https://docs.litellm.ai/docs/providers/apiko", + "endpoints": { + "chat_completions": true, + "responses": false, + "text_completion": false, + "embeddings": false, + "image_generations": false, + "image_edits": false, + "audio_transcriptions": false, + "audio_speech": false, + "rerank": false, + "ocr": false, + "search": false, + "moderations": false, + "realtime": false, + "video_generations": false, + "batches": false, + "messages": false, + "interactions": false, + "systemone": false, + "a2a": false, + "generateContent": false + } + }, "bigmodel": { "display_name": "BigModel (Zhipu AI) (`bigmodel`)", "url": "https://docs.bigmodel.cn/cn/guide/develop/responses/introduction", diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 5ded2fa5f5e..d98a10df211 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -535,6 +535,34 @@ ], "default_model_placeholder": "gpt-3.5-turbo" }, + { + "provider": "APIKO", + "provider_display_name": "APIKO", + "litellm_provider": "apiko", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "https://api.apiko.app/v1", + "tooltip": null, + "required": false, + "field_type": "text", + "options": null, + "default_value": null + }, + { + "key": "api_key", + "label": "API Key", + "placeholder": null, + "tooltip": null, + "required": true, + "field_type": "password", + "options": null, + "default_value": null + } + ], + "default_model_placeholder": "apiko/deepseek-v4-pro" + }, { "provider": "AssemblyAI", "provider_display_name": "AssemblyAI", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 7c137b72c3e..53fb16e8f6e 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4761,6 +4761,7 @@ class LlmProviders(str, Enum): MICROSOFT_365_COPILOT = "microsoft_365_copilot" MINIMAX = "minimax" SYNTHETIC = "synthetic" + APIKO = "apiko" APERTIS = "apertis" NANOGPT = "nano-gpt" POE = "poe" diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 00fee0a0d5a..955769cd166 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -5484,6 +5484,32 @@ "generateContent": true } }, + "apiko": { + "display_name": "APIKO (`apiko`)", + "url": "https://docs.litellm.ai/docs/providers/apiko", + "endpoints": { + "chat_completions": true, + "responses": false, + "text_completion": false, + "embeddings": false, + "image_generations": false, + "image_edits": false, + "audio_transcriptions": false, + "audio_speech": false, + "rerank": false, + "ocr": false, + "search": false, + "moderations": false, + "realtime": false, + "video_generations": false, + "batches": false, + "messages": false, + "interactions": false, + "systemone": false, + "a2a": false, + "generateContent": false + } + }, "bigmodel": { "display_name": "BigModel (Zhipu AI) (`bigmodel`)", "url": "https://docs.bigmodel.cn/cn/guide/develop/responses/introduction", diff --git a/tests/unit/llms/openai_like/test_apiko_provider.py b/tests/unit/llms/openai_like/test_apiko_provider.py new file mode 100644 index 00000000000..5a2a1e59e2c --- /dev/null +++ b/tests/unit/llms/openai_like/test_apiko_provider.py @@ -0,0 +1,74 @@ +""" +Unit tests for the APIKO OpenAI-compatible provider. +""" + +import os +import sys + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../.."))) + +import pytest + +import litellm +from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider +from litellm.llms.openai_like.dynamic_config import create_config_class +from litellm.llms.openai_like.json_loader import JSONProviderRegistry + +APIKO_BASE_URL = "https://api.apiko.app/v1" + + +def _get_config(): + provider = JSONProviderRegistry.get("apiko") + assert provider is not None + return create_config_class(provider)() + + +def test_apiko_provider_registered(): + provider = JSONProviderRegistry.get("apiko") + assert provider is not None + assert provider.base_url == APIKO_BASE_URL + assert provider.api_key_env == "APIKO_API_KEY" + assert provider.api_base_env == "APIKO_API_BASE" + assert litellm.LlmProviders.APIKO.value == "apiko" + + +def test_apiko_resolves_env_api_key(monkeypatch): + monkeypatch.delenv("APIKO_API_BASE", raising=False) + monkeypatch.setenv("APIKO_API_KEY", "test-key") + api_base, api_key = _get_config().get_openai_compatible_provider_info(None, None) + assert api_base == APIKO_BASE_URL + assert api_key == "test-key" + + +def test_apiko_model_prefix_routes_to_provider(monkeypatch): + monkeypatch.delenv("APIKO_API_BASE", raising=False) + monkeypatch.setenv("APIKO_API_KEY", "test-key") + model, provider, api_key, api_base = get_llm_provider("apiko/deepseek-v4-pro") + assert model == "deepseek-v4-pro" + assert provider == "apiko" + assert api_key == "test-key" + assert api_base == APIKO_BASE_URL + + +def test_apiko_complete_url_appends_endpoint(): + url = _get_config().get_complete_url( + api_base=APIKO_BASE_URL, + api_key="test-key", + model="apiko/deepseek-v4-pro", + optional_params={}, + litellm_params={}, + stream=False, + ) + assert url == f"{APIKO_BASE_URL}/chat/completions" + + +def test_apiko_does_not_fall_back_to_openai_key(monkeypatch): + monkeypatch.delenv("APIKO_API_KEY", raising=False) + monkeypatch.setenv("OPENAI_API_KEY", "sk-unrelated-openai-key") + monkeypatch.setattr(litellm, "api_key", None) + monkeypatch.setattr(litellm, "openai_key", None) + with pytest.raises(litellm.AuthenticationError, match="Set APIKO_API_KEY"): + litellm.completion( + model="apiko/deepseek-v4-pro", + messages=[{"role": "user", "content": "hi"}], + )