This commit is contained in:
Sebastien FAUVART 2026-11-05 07:34:13 +00:00 • committed by GitHub
commit ee49ad15b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 481 additions and 0 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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,

View file

@ -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",

View file

@ -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",

View file

@ -4769,6 +4769,7 @@ class LlmProviders(str, Enum):
COGNITION = "cognition"
CORALBRICKS = "coralbricks"
CORTECS = "cortecs"
CORTI = "corti"
SCX_AI = "scx-ai"
PRISM = "prism"
OPENINFER = "openinfer"

View file

@ -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,

View file

@ -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",

View file

@ -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"

View file

@ -0,0 +1,14 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_612_3664)">
<circle cx="128" cy="128" r="128" fill="black"/>
<path d="M126.415 53.8974C86.2057 53.9971 53.6403 86.6241 53.6403 126.857C53.6403 167.052 86.1438 199.655 126.301 199.816C126.489 204.448 126.875 209.079 127.458 213.71C127.173 213.713 126.887 213.714 126.6 213.714C78.6304 213.714 39.7432 174.827 39.7432 126.857C39.7432 78.8873 78.6304 40 126.6 40C126.993 40 127.385 40.0026 127.776 40.0078C127.125 44.6377 126.672 49.2675 126.415 53.8974Z" fill="white"/>
<path d="M196.297 178.697C196.834 172.672 197.704 166.647 198.909 160.622C203.252 138.908 203.252 117.194 198.909 95.4794C197.526 88.5618 196.583 81.6442 196.081 74.7266C206.992 89.2459 213.458 107.296 213.458 126.857C213.458 146.287 207.077 164.228 196.297 178.697Z" fill="white"/>
<path d="M168.161 203.144C167.295 188.97 168.28 174.797 171.115 160.623C175.457 138.909 175.457 117.194 171.115 95.4801C168.126 80.5388 167.194 65.5975 168.318 50.6562C173.099 53.2789 177.605 56.3382 181.785 59.7806C181.556 71.6804 182.632 83.5803 185.012 95.4801C189.355 117.194 189.355 138.909 185.012 160.623C182.789 171.734 181.704 182.846 181.756 193.957C177.539 197.428 172.989 200.508 168.161 203.144Z" fill="white"/>
<path d="M141.207 212.492C139.158 195.202 139.863 177.913 143.321 160.623C147.663 138.909 147.663 117.195 143.321 95.4803C139.707 77.4114 139.1 59.3424 141.501 41.2734C146.127 42.0733 150.629 43.2393 154.974 44.7404C153.087 61.6537 153.835 78.567 157.218 95.4803C161.561 117.195 161.561 138.909 157.218 160.623C153.989 176.768 153.161 192.913 154.733 209.057C150.371 210.55 145.851 211.705 141.207 212.492Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_612_3664">
<rect width="256" height="256" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -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");
});

View file

@ -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<string, string> = {
COMETAPI: "cometapi",
COMPACTIFAI: "compactifai",
CORALBRICKS: "coralbricks",
CORTI: "corti",
Cursor: "cursor",
Dashscope: "dashscope",
Databricks: "databricks",
@ -378,6 +381,7 @@ export const providerLogoMap: Partial<Record<Providers, string>> = {
[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<Record<Providers, string>> = {
[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/<any-model-on-deepinfra>",
[Providers.EDENAI]: "edenai/openai/gpt-mini-latest",

View file

@ -6,6 +6,7 @@ const BUNDLED_LOGO_PATH = /(?:\/assets\/logos\/|\/_next\/static\/media\/)/;
const TREATMENT_BY_ASSET: Readonly<Record<string, LogoTreatment>> = {
"baseten.svg": "invert",
"corti.svg": "invert",
"cursor.svg": "invert",
"edenai.svg": "invert",
"enkrypt_ai.avif": "invert",