This commit is contained in:
Balys Kriksciunas 2026-11-03 20:47:58 +00:00 • committed by GitHub
commit db16cb7814
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 332 additions and 3 deletions

View file

@ -464,6 +464,7 @@ Set `LITELLM_PROXY_API_BASE` and `LITELLM_PROXY_API_KEY` and every model call th
| [Together AI (`together_ai`)](https://docs.litellm.ai/docs/providers/togetherai) | ✅ | ✅ | ✅ | | | | | | | |
| [Topaz (`topaz`)](https://docs.litellm.ai/docs/providers/topaz) | ✅ | ✅ | ✅ | | | | | | | |
| [Triton (`triton`)](https://docs.litellm.ai/docs/providers/triton-inference-server) | ✅ | ✅ | ✅ | | | | | | | |
| [Turion (`turion`)](https://turion.ai/docs/guides/garden/) | | | | | | | | | | |
| [Typesafe Decisions API (`typesafe`)](https://docs.litellm.ai/docs/providers) | | | | | | | | | | |
| [V0 (`v0`)](https://docs.litellm.ai/docs/providers/v0) | ✅ | ✅ | ✅ | | | | | | | |
| [Vercel AI Gateway (`vercel_ai_gateway`)](https://docs.litellm.ai/docs/providers/vercel_ai_gateway) | ✅ | ✅ | ✅ | | | | | | | |

View file

@ -1753,6 +1753,9 @@ if TYPE_CHECKING:
from .llms.nace.decisions.transformation import (
NaceDecisionsConfig as NaceDecisionsConfig,
)
from .llms.turion.decisions.transformation import (
TurionDecisionsConfig as TurionDecisionsConfig,
)
from .llms.typesafe.decisions.transformation import (
TypeSafeDecisionsConfig as TypeSafeDecisionsConfig,
)
@ -1771,6 +1774,9 @@ if TYPE_CHECKING:
from .llms.cloudflare.extraction.transformation import (
CloudflareExtractionConfig as CloudflareExtractionConfig,
)
from .llms.turion.extraction.transformation import (
TurionExtractionConfig as TurionExtractionConfig,
)
from .llms.strands_decider.decisions.transformation import (
StrandsDeciderDecisionsConfig as StrandsDeciderDecisionsConfig,
)

View file

@ -165,10 +165,12 @@ LLM_CONFIG_NAMES: Final = (
"TypeSafeDecisionsConfig",
"ParallelAIDecisionsConfig",
"NaceDecisionsConfig",
"TurionDecisionsConfig",
"OpenRouterDecisionsConfig",
"FireworksAIDecisionsConfig",
"CloudflareDecisionsConfig",
"CloudflareExtractionConfig",
"TurionExtractionConfig",
"StrandsDeciderDecisionsConfig",
"DatabricksDecisionsConfig",
"HostedVLLMDecisionsConfig",
@ -740,12 +742,14 @@ _LLM_CONFIGS_IMPORT_MAP: Final = {
),
"PerplexityDecisionsConfig": (".llms.perplexity.decisions.transformation", "PerplexityDecisionsConfig"),
"NaceDecisionsConfig": (".llms.nace.decisions.transformation", "NaceDecisionsConfig"),
"TurionDecisionsConfig": (".llms.turion.decisions.transformation", "TurionDecisionsConfig"),
"TypeSafeDecisionsConfig": (".llms.typesafe.decisions.transformation", "TypeSafeDecisionsConfig"),
"ParallelAIDecisionsConfig": (".llms.parallel_ai.decisions.transformation", "ParallelAIDecisionsConfig"),
"OpenRouterDecisionsConfig": (".llms.openrouter.decisions.transformation", "OpenRouterDecisionsConfig"),
"FireworksAIDecisionsConfig": (".llms.fireworks_ai.decisions.transformation", "FireworksAIDecisionsConfig"),
"CloudflareDecisionsConfig": (".llms.cloudflare.decisions.transformation", "CloudflareDecisionsConfig"),
"CloudflareExtractionConfig": (".llms.cloudflare.extraction.transformation", "CloudflareExtractionConfig"),
"TurionExtractionConfig": (".llms.turion.extraction.transformation", "TurionExtractionConfig"),
"StrandsDeciderDecisionsConfig": (
".llms.strands_decider.decisions.transformation",
"StrandsDeciderDecisionsConfig",

View file

@ -0,0 +1,9 @@
from litellm.llms.base_llm.decisions.transformation import BaseDecisionsConfig
class TurionDecisionsConfig(BaseDecisionsConfig):
api_key_env = ("TURION_API_KEY", "TURION_API_TOKEN")
api_base_env = ("TURION_API_BASE",)
def get_default_api_base(self) -> str | None:
return "https://api.turion.ai"

View file

@ -0,0 +1,13 @@
from litellm.litellm_core_utils.url_utils import join_api_base
from litellm.llms.base_llm.extraction.transformation import BaseExtractionConfig
class TurionExtractionConfig(BaseExtractionConfig):
api_key_env = ("TURION_API_KEY", "TURION_API_TOKEN")
api_base_env = ("TURION_API_BASE",)
def get_default_api_base(self) -> str | None:
return "https://api.turion.ai"
def get_complete_url(self, api_base: str, model: str) -> str:
return join_api_base(api_base.rstrip("/").removesuffix("/v1"), "/v1/extract")

View file

@ -73187,6 +73187,26 @@
"output_cost_per_token": 0.0,
"source": "https://console.nace.ai/docs/drex/reference/models"
},
"turion/turion-extract-alpha": {
"input_cost_per_token": 5e-08,
"litellm_provider": "turion",
"max_input_tokens": 16384,
"mode": "extraction",
"output_cost_per_token": 0.0,
"source": "https://turion.ai/docs/guides/usage/#pricing"
},
"turion/turion-jev-31b": {
"input_cost_per_token": 2.1e-08,
"litellm_provider": "turion",
"max_input_tokens": 16384,
"mode": "evaluation",
"output_cost_per_token": 0.0,
"source": "https://turion.ai/docs/guides/usage/#pricing",
"supported_endpoints": [
"/v1/decisions",
"/v1/systemone"
]
},
"typesafe/jev-1.13.0": {
"input_cost_per_token": 4.2e-08,
"litellm_provider": "typesafe",

View file

@ -4046,6 +4046,32 @@
"generateContent": false
}
},
"turion": {
"display_name": "Turion (`turion`)",
"url": "https://turion.ai/docs/guides/garden/",
"endpoints": {
"chat_completions": false,
"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": true,
"a2a": false,
"generateContent": false
}
},
"typesafe": {
"display_name": "TypeSafe (`typesafe`)",
"url": "https://docs.typesafe.ai/models",

View file

@ -2669,6 +2669,34 @@
],
"default_model_placeholder": "parallel_ai/decision"
},
{
"provider": "Turion",
"provider_display_name": "Turion",
"litellm_provider": "turion",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://api.turion.ai",
"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": "turion/turion-jev-31b"
},
{
"provider": "NEBIUS",
"provider_display_name": "Nebius",

View file

@ -4625,6 +4625,7 @@ class LlmProviders(str, Enum):
TYPESAFE = "typesafe"
PARALLEL_AI = "parallel_ai"
NACE = "nace"
TURION = "turion"
STRANDS_DECIDER = "strands_decider"
LAYA = "laya"
BESPOKE = "bespoke"

View file

@ -10281,6 +10281,7 @@ class ProviderConfigManager:
LlmProviders.AZURE_AI,
LlmProviders.EU_JEV,
LlmProviders.PARALLEL_AI,
LlmProviders.TURION,
):
configs: Final = {
LlmProviders.OPENAI: litellm.OpenAIDecisionsConfig,
@ -10288,6 +10289,7 @@ class ProviderConfigManager:
LlmProviders.AZURE_AI: litellm.AzureAIDecisionsConfig,
LlmProviders.EU_JEV: litellm.EuJevDecisionsConfig,
LlmProviders.PARALLEL_AI: litellm.ParallelAIDecisionsConfig,
LlmProviders.TURION: litellm.TurionDecisionsConfig,
}
return configs[provider]()
return None
@ -10296,6 +10298,8 @@ class ProviderConfigManager:
def get_provider_extraction_config(model: str, provider: LlmProviders) -> BaseExtractionConfig | None:
if provider == LlmProviders.CLOUDFLARE:
return litellm.CloudflareExtractionConfig()
if provider == LlmProviders.TURION:
return litellm.TurionExtractionConfig()
return None
@staticmethod

View file

@ -73187,6 +73187,26 @@
"output_cost_per_token": 0.0,
"source": "https://console.nace.ai/docs/drex/reference/models"
},
"turion/turion-extract-alpha": {
"input_cost_per_token": 5e-08,
"litellm_provider": "turion",
"max_input_tokens": 16384,
"mode": "extraction",
"output_cost_per_token": 0.0,
"source": "https://turion.ai/docs/guides/usage/#pricing"
},
"turion/turion-jev-31b": {
"input_cost_per_token": 2.1e-08,
"litellm_provider": "turion",
"max_input_tokens": 16384,
"mode": "evaluation",
"output_cost_per_token": 0.0,
"source": "https://turion.ai/docs/guides/usage/#pricing",
"supported_endpoints": [
"/v1/decisions",
"/v1/systemone"
]
},
"typesafe/jev-1.13.0": {
"input_cost_per_token": 4.2e-08,
"litellm_provider": "typesafe",

View file

@ -4046,6 +4046,32 @@
"generateContent": false
}
},
"turion": {
"display_name": "Turion (`turion`)",
"url": "https://turion.ai/docs/guides/garden/",
"endpoints": {
"chat_completions": false,
"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": true,
"a2a": false,
"generateContent": false
}
},
"typesafe": {
"display_name": "TypeSafe (`typesafe`)",
"url": "https://docs.typesafe.ai/models",

View file

@ -91,6 +91,9 @@ _PROVIDERS: Final = (
),
_Provider("typesafe", "typesafe/jev-1.13.0", "/v1/systemone", "jev-1.13.0", _API_KEY, False, "typesafe/jev-1.13.0"),
_Provider("nace", "nace/drex-v2.0", "/v1/systemone", "drex-v2.0", _API_KEY, False, "nace/drex-v2.0"),
_Provider(
"turion", "turion/turion-jev-31b", "/v1/systemone", "turion-jev-31b", _API_KEY, False, "turion/turion-jev-31b"
),
_Provider(
"openrouter",
"openrouter/typesafe/jev-1.13",

View file

@ -392,7 +392,7 @@ def test_only_a_provider_without_confidence_may_omit_it(
assert raised.value.status_code == 500
@pytest.mark.parametrize("provider", ("nace", "openrouter", "fireworks_ai", "typesafe"))
@pytest.mark.parametrize("provider", ("nace", "openrouter", "fireworks_ai", "typesafe", "turion"))
@pytest.mark.parametrize("legend", (["low", "high"], {"0": "low", "1": "high"}))
def test_score_legends_preserve_labels_probabilities_and_metadata(provider: str, legend: object) -> None:
config: Final = ProviderConfigManager.get_provider_decisions_config("drex-v2.0", LlmProviders(provider))
@ -438,7 +438,7 @@ def test_score_legends_preserve_labels_probabilities_and_metadata(provider: str,
@pytest.mark.parametrize(
"payload", ([], {"answers": []}, {"answers": {"a": None}}, {"answers": {"a": {"type": "score", "legend": [1]}}})
)
@pytest.mark.parametrize("provider", ("nace", "openrouter", "fireworks_ai", "typesafe"))
@pytest.mark.parametrize("provider", ("nace", "openrouter", "fireworks_ai", "typesafe", "turion"))
def test_invalid_provider_response_is_an_api_error(payload: object, provider: str) -> None:
config: Final = ProviderConfigManager.get_provider_decisions_config("drex-v2.0", LlmProviders(provider))
assert config is not None
@ -502,7 +502,7 @@ def test_score_probabilities_preserve_missing_entries_and_explicit_zeros(
@pytest.mark.parametrize("raw_content", (b"", b"not-json", b'{"answers":'))
@pytest.mark.parametrize("provider", ("nace", "openrouter", "fireworks_ai", "typesafe", "openai"))
@pytest.mark.parametrize("provider", ("nace", "openrouter", "fireworks_ai", "typesafe", "turion", "openai"))
def test_non_json_provider_response_is_an_api_error(raw_content: bytes, provider: str) -> None:
config: Final = ProviderConfigManager.get_provider_decisions_config("drex-v2.0", LlmProviders(provider))
assert config is not None

View file

View file

@ -0,0 +1,149 @@
import json
from collections.abc import Mapping
from typing import Final
import httpx
import pytest
import litellm
from litellm.llms.base_llm.decisions.transformation import systemone_request_to_ir
from litellm.llms.base_llm.extraction.transformation import BaseExtractionConfig
from litellm.types.decisions import DecisionsIRChoiceAnswer, DecisionsRequestBody
from litellm.types.extraction import ExtractionRequestBody
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager
# Bodies as https://api.turion.ai answered them (POST /v1/systemone and POST /v1/extract).
TURION_SYSTEMONE_RESPONSE: Final = {
"model": "turion-jev-31b",
"answers": {
"refund": {"type": "noul", "noul": 0.892483},
"team": {
"type": "choice",
"choice": "billing",
"probabilities": {"billing": 0.991577, "technical": 0.008423},
"confidence": 0.991577,
},
"urgency": {
"type": "score",
"score": 1.128746,
"legend": {"0": "Can wait", "1": "This week", "2": "Right now"},
"probabilities": {"0": 0.17257, "1": 0.526114, "2": 0.301316},
"confidence": 0.526114,
},
},
"usage": {"input_tokens": 409, "output_tokens": 0},
}
TURION_EXTRACT_RESPONSE: Final = {
"id": "01a193fd-bf10-742e-a4a4-8f1865b3b83d",
"model": "turion-extract-alpha",
"answers": {
"invoice_number": {
"type": "text",
"value": "INV-2291",
"p_null": 0.00012,
"confidence": 0.99988,
"span": [8, 16],
},
"total": {"type": "number", "value": 1080, "p_null": 0.000008, "confidence": 0.999992},
},
"truncated": False,
"usage": {"input_tokens": 106, "output_tokens": 0, "passes": 1},
}
def _extraction_config() -> BaseExtractionConfig:
config: Final = ProviderConfigManager.get_provider_extraction_config("turion-extract-alpha", LlmProviders.TURION)
assert config is not None
return config
def test_turion_serves_decisions_and_extraction() -> None:
assert isinstance(
ProviderConfigManager.get_provider_decisions_config("turion-jev-31b", LlmProviders.TURION),
litellm.TurionDecisionsConfig,
)
assert isinstance(_extraction_config(), litellm.TurionExtractionConfig)
@pytest.mark.parametrize("api_base", ("https://api.turion.ai", "https://api.turion.ai/", "https://api.turion.ai/v1"))
def test_routes_to_the_turion_api(monkeypatch: pytest.MonkeyPatch, api_base: str) -> None:
monkeypatch.delenv("TURION_API_BASE", raising=False)
decisions: Final = ProviderConfigManager.get_provider_decisions_config("turion-jev-31b", LlmProviders.TURION)
assert decisions is not None
assert decisions.resolve_api_base(None) == "https://api.turion.ai"
assert _extraction_config().resolve_api_base(None) == "https://api.turion.ai"
assert decisions.get_complete_url(api_base, "turion-jev-31b") == "https://api.turion.ai/v1/systemone"
assert _extraction_config().get_complete_url(api_base, "turion-extract-alpha") == "https://api.turion.ai/v1/extract"
def test_reads_turion_api_key_then_the_turion_cli_token(monkeypatch: pytest.MonkeyPatch) -> None:
config: Final = _extraction_config()
monkeypatch.delenv("TURION_API_KEY", raising=False)
monkeypatch.setenv("TURION_API_TOKEN", "tk_from_the_cli")
assert config.resolve_api_key(None) == "tk_from_the_cli"
monkeypatch.setenv("TURION_API_KEY", "tk_litellm")
assert config.resolve_api_key(None) == "tk_litellm"
assert config.resolve_api_key("tk_explicit") == "tk_explicit"
def test_extraction_request_keeps_the_clairic_body() -> None:
body: Final = _extraction_config().transform_extraction_request(
"turion-extract-alpha",
ExtractionRequestBody.model_validate(
{
"state": "Invoice INV-2291 issued 2026-09-02. Total $1080.",
"fields": {"invoice_number": {"type": "text"}, "total": {"type": "number"}},
"reference_date": "2026-10-20",
}
),
)
fields: Final = body["fields"]
assert body["model"] == "turion-extract-alpha"
assert body["reference_date"] == "2026-10-20"
assert isinstance(fields, Mapping)
assert list(fields) == ["invoice_number", "total"]
def test_extraction_response_parses_turion_answers() -> None:
response: Final = _extraction_config().transform_extraction_response(
"turion-extract-alpha",
"turion",
httpx.Response(status_code=200, content=json.dumps(TURION_EXTRACT_RESPONSE).encode()),
)
assert response.truncated is False
assert response.answers["invoice_number"].value == "INV-2291"
assert response.answers["invoice_number"].span == (8, 16)
assert response.answers["total"].value == 1080
assert response.usage.input_tokens == 106
def test_systemone_response_parses_turion_answers() -> None:
config: Final = ProviderConfigManager.get_provider_decisions_config("turion-jev-31b", LlmProviders.TURION)
assert config is not None
request: Final = systemone_request_to_ir(
DecisionsRequestBody.model_validate(
{
"state": "I was charged twice for my subscription.",
"questions": {
"refund": {"type": "noul", "instructions": "Is the customer asking for money back?"},
"team": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {"billing": "Charges and refunds", "technical": "Bugs and outages"},
},
"urgency": {
"type": "score",
"instructions": "How urgent is this?",
"criteria": ["Can wait", "This week", "Right now"],
},
},
}
)
)
response: Final = config.parse_response(TURION_SYSTEMONE_RESPONSE, request)
assert len(response.answers) == 3
team: Final = next(answer for answer in response.answers if isinstance(answer, DecisionsIRChoiceAnswer))
assert team.choice == "billing"
assert response.usage is not None
assert response.usage.input_tokens == 409

View file

@ -528,6 +528,7 @@ def _decisions_provider_entry(provider: str) -> ProviderCreateInfo:
("TypeSafe", "TypeSafe", LlmProviders.TYPESAFE),
("Nace", "Nace", LlmProviders.NACE),
("PARALLEL_AI", "Parallel AI", LlmProviders.PARALLEL_AI),
("Turion", "Turion", LlmProviders.TURION),
("EU_JEV", "eu/jev", LlmProviders.EU_JEV),
),
)

View file

@ -269,6 +269,7 @@ describe("provider_info_helpers", () => {
Providers.PREDIBASE,
Providers.Sail,
Providers.StreamLake,
Providers.Turion,
Providers.WANDB,
Providers.ZAI,
Providers.ZeroGPU,
@ -603,6 +604,20 @@ describe("provider_info_helpers", () => {
expect(getPlaceholder("Nace")).toBe("nace/drex-latest");
});
it("lists Turion's decision and extraction models and resolves the provider name and model placeholder", () => {
const modelMap = {
"turion/turion-jev-31b": { litellm_provider: "turion", mode: "evaluation" },
"turion/turion-extract-alpha": { litellm_provider: "turion", mode: "extraction" },
"typesafe/jev-latest": { litellm_provider: "typesafe", mode: "evaluation" },
};
expect(getProviderModels(Providers.Turion, modelMap)).toEqual([
"turion/turion-jev-31b",
"turion/turion-extract-alpha",
]);
expect(getProviderLogoAndName("turion").displayName).toBe("Turion");
expect(getPlaceholder("Turion")).toBe("turion/turion-jev-31b");
});
it("should list only typesafe decision models for the 'TypeSafe' provider key, not the OpenRouter-hosted one", () => {
const modelMap = {
"typesafe/jev-latest": { litellm_provider: "typesafe", mode: "evaluation" },

View file

@ -193,6 +193,7 @@ export enum Providers {
TypeSafe = "TypeSafe",
Nace = "Nace",
PARALLEL_AI = "Parallel AI",
Turion = "Turion",
V0 = "V0",
VERCEL_AI_GATEWAY = "Vercel Ai Gateway",
Vertex_AI = "Vertex AI (Anthropic, Gemini, etc.)",
@ -328,6 +329,7 @@ export const provider_map: Record<string, string> = {
TypeSafe: "typesafe",
Nace: "nace",
PARALLEL_AI: "parallel_ai",
Turion: "turion",
V0: "v0",
VERCEL_AI_GATEWAY: "vercel_ai_gateway",
Vertex_AI: "vertex_ai",
@ -519,6 +521,7 @@ const providerPlaceholderMap: Partial<Record<Providers, string>> = {
[Providers.TypeSafe]: "typesafe/jev-latest",
[Providers.Nace]: "nace/drex-latest",
[Providers.PARALLEL_AI]: "parallel_ai/decision",
[Providers.Turion]: "turion/turion-jev-31b",
[Providers.StreamLake]: "streamlake/<model-or-endpoint>",
[Providers.Vertex_AI]: "gemini-pro",
[Providers.VolcEngine]: "volcengine/<any-model-on-volcengine>",