diff --git a/litellm/constants.py b/litellm/constants.py index 50e090eabaf..ac8ef0e76fe 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1090,6 +1090,7 @@ openai_compatible_endpoints: Final[list[str]] = [ "https://api.cognition.ai/v1", "https://inference.coralbricks.ai/v1", "https://api.cortecs.ai/v1", + "https://ai.eu.corti.app/v1", "https://api.scx.ai/v1", "https://api.prisminference.com/v1", "https://api.reka.ai/v1", @@ -1175,6 +1176,7 @@ openai_compatible_providers: Final[list[str]] = [ "cognition", "coralbricks", "cortecs", + "corti", "scx-ai", "prism", "hostinger_router", diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index f937e4d2f75..2475aeb5da1 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -216,6 +216,18 @@ }, "supported_endpoints": ["/v1/chat/completions", "/v1/responses"] }, + "corti": { + "base_url": "https://ai.eu.corti.app/v1", + "api_key_env": "CORTI_API_KEY", + "api_base_env": "CORTI_API_BASE", + "param_mappings": { + "max_completion_tokens": "max_tokens" + }, + "constraints": { + "temperature_max": 1.0 + }, + "supported_endpoints": ["/v1/chat/completions", "/v1/responses"] + }, "scx-ai": { "base_url": "https://api.scx.ai/v1", "api_key_env": "SCX_API_KEY", diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 76fd85d001b..9dde287dac0 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -48993,6 +48993,66 @@ "supports_tool_choice": true, "supports_vision": true }, + "corti/corti-s1": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "corti", + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://docs.corti.ai/models/models#pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "corti/corti-s1-instant": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "corti", + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://docs.corti.ai/models/models#pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": false, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "corti/corti-s1-mini": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "corti", + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://docs.corti.ai/models/models#pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "corti/corti-s1-mini-instant": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "corti", + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://docs.corti.ai/models/models#pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": false, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, "snowflake/claude-3-5-sonnet": { "litellm_provider": "snowflake", "max_input_tokens": 200000, diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index 00fee0a0d5a..fefbe8c0875 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -1139,6 +1139,24 @@ "generateContent": true } }, + "corti": { + "display_name": "Corti (`corti`)", + "url": "https://docs.litellm.ai/docs/providers/corti", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": true, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false, + "interactions": false + } + }, "custom": { "display_name": "Custom (`custom`)", "url": "https://docs.litellm.ai/docs/providers/custom_llm_server", diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 5ded2fa5f5e..43e82c50a2e 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -1201,6 +1201,34 @@ ], "default_model_placeholder": "cortecs/gpt-6-sol" }, + { + "provider": "CORTI", + "provider_display_name": "Corti", + "litellm_provider": "corti", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "https://ai.eu.corti.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": "corti/corti-s1" + }, { "provider": "CUSTOM", "provider_display_name": "Custom", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index ba29a67a36c..55c387c1c75 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4769,6 +4769,7 @@ class LlmProviders(str, Enum): COGNITION = "cognition" CORALBRICKS = "coralbricks" CORTECS = "cortecs" + CORTI = "corti" SCX_AI = "scx-ai" PRISM = "prism" OPENINFER = "openinfer" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 76fd85d001b..9dde287dac0 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -48993,6 +48993,66 @@ "supports_tool_choice": true, "supports_vision": true }, + "corti/corti-s1": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "corti", + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://docs.corti.ai/models/models#pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "corti/corti-s1-instant": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "corti", + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://docs.corti.ai/models/models#pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": false, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "corti/corti-s1-mini": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "corti", + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://docs.corti.ai/models/models#pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "corti/corti-s1-mini-instant": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "corti", + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://docs.corti.ai/models/models#pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": false, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, "snowflake/claude-3-5-sonnet": { "litellm_provider": "snowflake", "max_input_tokens": 200000, diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 00fee0a0d5a..a2d77cb7dd6 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -1113,6 +1113,24 @@ "generateContent": true } }, + "corti": { + "display_name": "Corti (`corti`)", + "url": "https://docs.litellm.ai/docs/providers/corti", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": true, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false, + "interactions": false + } + }, "crusoe": { "display_name": "Crusoe (`crusoe`)", "url": "https://docs.litellm.ai/docs/providers/crusoe", diff --git a/tests/unit/llms/openai_like/test_corti_provider.py b/tests/unit/llms/openai_like/test_corti_provider.py new file mode 100644 index 00000000000..7d029b84f18 --- /dev/null +++ b/tests/unit/llms/openai_like/test_corti_provider.py @@ -0,0 +1,247 @@ +import json +from pathlib import Path +from typing import Final + +import pytest +import respx + +import litellm +from litellm.caching.llm_caching_handler import LLMClientCache + +_CHAT_COMPLETION: Final = { + "id": "chatcmpl_corti", + "object": "chat.completion", + "created": 1_790_000_000, + "model": "corti-s1", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello from Corti"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 4, "completion_tokens": 3, "total_tokens": 7}, +} + + +def test_corti_provider_resolution(monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("CORTI_API_KEY", "corti-test-key") + monkeypatch.delenv("CORTI_API_BASE", raising=False) + + model, provider, api_key, api_base = get_llm_provider( + model="corti/corti-s1", + custom_llm_provider=None, + api_base=None, + api_key=None, + ) + + assert model == "corti-s1" + assert provider == "corti" + assert api_key == "corti-test-key" + assert api_base == "https://ai.eu.corti.app/v1" + + +def test_corti_provider_keeps_explicit_credentials(monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("CORTI_API_KEY", "corti-env-key") + monkeypatch.setenv("CORTI_API_BASE", "https://corti.env.example/v1") + + _, provider, api_key, api_base = get_llm_provider( + model="corti/corti-s1", + custom_llm_provider=None, + api_base="https://ai.us.corti.app/v1", + api_key="corti-explicit-key", + ) + + assert provider == "corti" + assert api_key == "corti-explicit-key" + assert api_base == "https://ai.us.corti.app/v1" + + +def test_corti_api_base_env_overrides_default(monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("CORTI_API_KEY", "corti-env-key") + monkeypatch.setenv("CORTI_API_BASE", "https://ai.us.corti.app/v1") + + _, provider, _, api_base = get_llm_provider( + model="corti/corti-s1", + custom_llm_provider=None, + api_base=None, + api_key=None, + ) + + assert provider == "corti" + assert api_base == "https://ai.us.corti.app/v1" + + +def test_corti_api_base_autodetects_provider(monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("CORTI_API_KEY", "corti-env-key") + + model, provider, api_key, api_base = get_llm_provider( + model="corti-s1", + custom_llm_provider=None, + api_base="https://ai.eu.corti.app/v1", + api_key=None, + ) + + assert model == "corti-s1" + assert provider == "corti" + assert api_key == "corti-env-key" + assert api_base == "https://ai.eu.corti.app/v1" + + +def test_corti_is_available_in_add_model_form(): + fields_path: Final = Path(litellm.__file__).parent / "proxy" / "public_endpoints" / "provider_create_fields.json" + providers: Final = json.loads(fields_path.read_text()) + corti: Final = next(provider for provider in providers if provider["litellm_provider"] == "corti") + + assert corti["provider"] == "CORTI" + assert corti["provider_display_name"] == "Corti" + assert corti["default_model_placeholder"] == "corti/corti-s1" + assert {field["key"]: field["required"] for field in corti["credential_fields"]} == { + "api_base": False, + "api_key": True, + } + + +def test_corti_supported_endpoints(): + expected: Final = { + "chat_completions": True, + "messages": True, + "responses": True, + "embeddings": False, + "image_generations": False, + "audio_transcriptions": False, + "audio_speech": False, + "moderations": False, + "batches": False, + "rerank": False, + "a2a": False, + "interactions": False, + } + backup_path: Final = Path(litellm.__file__).parent / "provider_endpoints_support_backup.json" + root_path: Final = Path(litellm.__file__).parent.parent / "provider_endpoints_support.json" + + assert json.loads(backup_path.read_text())["providers"]["corti"]["endpoints"] == expected + assert json.loads(root_path.read_text())["providers"]["corti"]["endpoints"] == expected + + +def test_corti_chat_completion_request(): + with respx.mock() as upstream: + route: Final = upstream.post("https://ai.eu.corti.app/v1/chat/completions").respond(200, json=_CHAT_COMPLETION) + response: Final = litellm.completion( + model="corti/corti-s1", + messages=[{"role": "user", "content": "Say hello"}], + api_key="corti-test-key", + ) + + request: Final = route.calls.last.request + body: Final = json.loads(request.content) + assert route.call_count == 1 + assert str(request.url) == "https://ai.eu.corti.app/v1/chat/completions" + assert request.headers["authorization"] == "Bearer corti-test-key" + assert body["model"] == "corti-s1" + assert body["messages"] == [{"role": "user", "content": "Say hello"}] + assert response.choices[0].message.content == "Hello from Corti" + + +def test_corti_chat_completion_maps_unsupported_params(): + with respx.mock() as upstream: + route: Final = upstream.post("https://ai.eu.corti.app/v1/chat/completions").respond(200, json=_CHAT_COMPLETION) + litellm.completion( + model="corti/corti-s1", + messages=[{"role": "user", "content": "Say hello"}], + api_key="corti-test-key", + max_completion_tokens=64, + temperature=1.5, + ) + + body: Final = json.loads(route.calls.last.request.content) + assert body["max_tokens"] == 64 + assert "max_completion_tokens" not in body + assert body["temperature"] == 1.0 + + +@pytest.mark.parametrize("model", ["corti-s1", "corti-s1-instant", "corti-s1-mini", "corti-s1-mini-instant"]) +def test_corti_chat_completion_cost_comes_from_the_price_map(model: str): + with respx.mock() as upstream: + upstream.post("https://ai.eu.corti.app/v1/chat/completions").respond( + 200, json={**_CHAT_COMPLETION, "model": model} + ) + response: Final = litellm.completion( + model=f"corti/{model}", + messages=[{"role": "user", "content": "Say hello"}], + api_key="corti-test-key", + ) + + row: Final = litellm.model_cost[f"corti/{model}"] + expected: Final = 4 * row["input_cost_per_token"] + 3 * row["output_cost_per_token"] + assert litellm.completion_cost(completion_response=response) == pytest.approx(expected) + assert expected > 0 + + +def test_corti_responses_request(): + with respx.mock() as upstream: + route: Final = upstream.post("https://ai.eu.corti.app/v1/responses").respond( + 200, + json={ + "id": "resp_corti", + "object": "response", + "created_at": 1_790_000_000, + "model": "corti-s1", + "status": "completed", + "output": [ + { + "id": "msg_corti", + "type": "message", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": "Hello from Corti", "annotations": []}], + } + ], + "usage": {"input_tokens": 4, "output_tokens": 3, "total_tokens": 7}, + }, + ) + response: Final = litellm.responses( + model="corti/corti-s1", + input="Say hello", + api_key="corti-test-key", + ) + + request: Final = route.calls.last.request + body: Final = json.loads(request.content) + assert route.call_count == 1 + assert str(request.url) == "https://ai.eu.corti.app/v1/responses" + assert request.headers["authorization"] == "Bearer corti-test-key" + assert body["model"] == "corti-s1" + assert body["input"] == "Say hello" + assert response.output[0].content[0].text == "Hello from Corti" + + +@pytest.mark.asyncio +async def test_corti_anthropic_messages_request_is_bridged_to_chat_completions(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", LLMClientCache()) + with respx.mock() as upstream: + route: Final = upstream.post("https://ai.eu.corti.app/v1/chat/completions").respond(200, json=_CHAT_COMPLETION) + response: Final = await litellm.anthropic.messages.acreate( + model="corti/corti-s1", + messages=[{"role": "user", "content": "Say hello"}], + max_tokens=32, + api_key="corti-test-key", + ) + + request: Final = route.calls.last.request + body: Final = json.loads(request.content) + assert route.call_count == 1 + assert request.headers["authorization"] == "Bearer corti-test-key" + assert body["model"] == "corti-s1" + assert body["messages"] == [{"role": "user", "content": "Say hello"}] + assert body["max_tokens"] == 32 + assert response["content"][0]["text"] == "Hello from Corti" diff --git a/ui/litellm-dashboard/public/assets/logos/corti.svg b/ui/litellm-dashboard/public/assets/logos/corti.svg new file mode 100644 index 00000000000..fffe16cdcda --- /dev/null +++ b/ui/litellm-dashboard/public/assets/logos/corti.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx index 604d5923caa..7b11efc7bdd 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx @@ -142,6 +142,17 @@ describe("provider_info_helpers", () => { expect(fromEnumKey.logo).toBe(providerLogoMap[Providers.SCX_AI]); }); + it("should map corti slug and CORTI enum key to the Corti display name and logo", () => { + const fromSlug = getProviderLogoAndName("corti"); + expect(fromSlug.displayName).toBe(Providers.CORTI); + expect(fromSlug.logo).toBe(providerLogoMap[Providers.CORTI]); + expect(fromSlug.logo).toBeTruthy(); + + const fromEnumKey = getProviderLogoAndName("CORTI"); + expect(fromEnumKey.displayName).toBe(Providers.CORTI); + expect(fromEnumKey.logo).toBe(providerLogoMap[Providers.CORTI]); + }); + it("should map bedrock_mantle slug to Bedrock Mantle display name and logo", () => { const result = getProviderLogoAndName("bedrock_mantle"); expect(result.displayName).toBe(Providers.BedrockMantle); @@ -325,6 +336,10 @@ describe("provider_info_helpers", () => { expect(getPlaceholder(Providers.SCX_AI)).toBe("scx-ai/GLM-5.2"); }); + it("should return a corti model placeholder for CORTI provider", () => { + expect(getPlaceholder(Providers.CORTI)).toBe("corti/corti-s1"); + }); + it("should return an edenai model placeholder for EDENAI provider", () => { expect(getPlaceholder(Providers.EDENAI)).toBe("edenai/openai/gpt-mini-latest"); }); diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index 167343fc05e..3ca6a10ca95 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -12,6 +12,7 @@ import cerebrasLogo from "../../public/assets/logos/cerebras.svg"; import cloudflareLogo from "../../public/assets/logos/cloudflare.svg"; import cohereLogo from "../../public/assets/logos/cohere.svg"; import cometapiLogo from "../../public/assets/logos/cometapi.svg"; +import cortiLogo from "../../public/assets/logos/corti.svg"; import cursorLogo from "../../public/assets/logos/cursor.svg"; import databricksLogo from "../../public/assets/logos/databricks.svg"; import deepgramLogo from "../../public/assets/logos/deepgram.png"; @@ -107,6 +108,7 @@ export enum Providers { COMETAPI = "Cometapi", COMPACTIFAI = "Compactifai", CORALBRICKS = "CoralBricks", + CORTI = "Corti", Cursor = "Cursor", Dashscope = "Dashscope", Databricks = "Databricks (Qwen API)", @@ -242,6 +244,7 @@ export const provider_map: Record = { COMETAPI: "cometapi", COMPACTIFAI: "compactifai", CORALBRICKS: "coralbricks", + CORTI: "corti", Cursor: "cursor", Dashscope: "dashscope", Databricks: "databricks", @@ -378,6 +381,7 @@ export const providerLogoMap: Partial> = { [Providers.Cohere]: cohereLogo.src, [Providers.COHERE_CHAT]: cohereLogo.src, [Providers.COMETAPI]: cometapiLogo.src, + [Providers.CORTI]: cortiLogo.src, [Providers.Cursor]: cursorLogo.src, [Providers.Databricks]: databricksLogo.src, [Providers.Dashscope]: qwenLogo.src, @@ -498,6 +502,7 @@ const providerPlaceholderMap: Partial> = { [Providers.CHATGPT]: "chatgpt/gpt-5.4", [Providers.Cognition]: "cognition/swe-1.7", [Providers.CORALBRICKS]: "coralbricks/glm-5.3-fast", + [Providers.CORTI]: "corti/corti-s1", [Providers.Cursor]: "cursor/claude-4-sonnet", [Providers.DeepInfra]: "deepinfra/", [Providers.EDENAI]: "edenai/openai/gpt-mini-latest", diff --git a/ui/litellm-dashboard/src/lib/logoTreatments.ts b/ui/litellm-dashboard/src/lib/logoTreatments.ts index 4cd6f65a613..82a8fb9bb65 100644 --- a/ui/litellm-dashboard/src/lib/logoTreatments.ts +++ b/ui/litellm-dashboard/src/lib/logoTreatments.ts @@ -6,6 +6,7 @@ const BUNDLED_LOGO_PATH = /(?:\/assets\/logos\/|\/_next\/static\/media\/)/; const TREATMENT_BY_ASSET: Readonly> = { "baseten.svg": "invert", + "corti.svg": "invert", "cursor.svg": "invert", "edenai.svg": "invert", "enkrypt_ai.avif": "invert",