This commit is contained in:
Cenab Batu Bora 2026-11-06 18:42:02 -06:00 • committed by GitHub
commit fa960591f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 446 additions and 4 deletions

View file

@ -465,6 +465,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) | ✅ | ✅ | ✅ | | | | | | | |
| [Tsubasa (`tsubasa`)](https://docs.litellm.ai/docs/providers/tsubasa) | Yes | | | | | | | | | |
| [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

@ -0,0 +1,46 @@
# Tsubasa
Tsubasa uses LiteLLM's existing OpenAI-compatible Chat Completions transport
Use an active Tsubasa API key for text chat. This integration does not declare
tool-calling, structured-output, vision, or reasoning support. Hosted inference
has not been qualified through this integration
## API key
Set `TSUBASA_API_KEY` in the process environment. LiteLLM resolves the public
endpoint automatically; no `api_base` override is required
## Completion
Select a public model with the `tsubasa/` provider prefix
```python
from litellm import completion
response = completion(
model="tsubasa/tsubasa-pro",
messages=[{"role": "user", "content": "Explain binary search."}],
max_tokens=256,
)
```
Use `tsubasa/tsubasa-fast` for the other public model. Both use
`https://api.tsubasa.sh/v1/chat/completions`
## Streaming
Set `stream=True` to receive Chat Completions chunks
```python
stream = completion(
model="tsubasa/tsubasa-fast",
messages=[{"role": "user", "content": "Explain a stack."}],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")
```
The integration does not declare Responses, embeddings, image, or audio support.
See [Tsubasa's API documentation](https://tsubasa.sh/docs) for availability

View file

@ -1172,6 +1172,7 @@ openai_compatible_providers: Final[list[str]] = [
"pinstripes", # Pinstripes - JSON-configured provider
"openinfer",
"darkbloom",
"tsubasa",
"meta", # Meta Model API (Muse Spark) - JSON-configured provider
"cognition",
"coralbricks",

View file

@ -30,6 +30,13 @@
"base_url": "https://api.scaleway.ai/v1",
"api_key_env": "SCW_SECRET_KEY"
},
"tsubasa": {
"base_url": "https://api.tsubasa.sh/v1",
"api_key_env": "TSUBASA_API_KEY",
"param_mappings": {
"max_completion_tokens": "max_tokens"
}
},
"synthetic": {
"base_url": "https://api.synthetic.new/openai/v1",
"api_key_env": "SYNTHETIC_API_KEY",

View file

@ -49845,6 +49845,34 @@
"mode": "embedding",
"output_cost_per_token": 0.0
},
"tsubasa/tsubasa-fast": {
"max_input_tokens": 32768,
"max_output_tokens": 8192,
"input_cost_per_token": 2.0000000000000002e-07,
"output_cost_per_token": 1e-06,
"litellm_provider": "tsubasa",
"mode": "chat",
"supports_system_messages": true,
"supports_function_calling": false,
"supports_parallel_function_calling": false,
"supports_response_schema": false,
"supports_vision": false,
"supports_reasoning": false
},
"tsubasa/tsubasa-pro": {
"max_input_tokens": 32768,
"max_output_tokens": 16384,
"input_cost_per_token": 5e-07,
"output_cost_per_token": 4e-06,
"litellm_provider": "tsubasa",
"mode": "chat",
"supports_system_messages": true,
"supports_function_calling": false,
"supports_parallel_function_calling": false,
"supports_response_schema": false,
"supports_vision": false,
"supports_reasoning": false
},
"together_ai/BAAI/bge-base-en-v1.5": {
"input_cost_per_token": 8e-09,
"litellm_provider": "together_ai",

View file

@ -4280,6 +4280,32 @@
"generateContent": false
}
},
"tsubasa": {
"display_name": "Tsubasa (`tsubasa`)",
"url": "https://docs.litellm.ai/docs/providers/tsubasa",
"endpoints": {
"chat_completions": true,
"responses": true,
"text_completion": true,
"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": true,
"interactions": true,
"systemone": false,
"a2a": true,
"generateContent": true
}
},
"v0": {
"display_name": "V0 (`v0`)",
"url": "https://docs.litellm.ai/docs/providers/v0",

View file

@ -4003,6 +4003,34 @@
],
"default_model_placeholder": "typesafe/jev-latest"
},
{
"provider": "Tsubasa",
"provider_display_name": "Tsubasa",
"litellm_provider": "tsubasa",
"credential_fields": [
{
"key": "api_key",
"label": "API Key",
"placeholder": null,
"tooltip": "Your Tsubasa API key",
"required": true,
"field_type": "password",
"options": null,
"default_value": null
},
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://api.tsubasa.sh/v1",
"tooltip": "Optional: override the Tsubasa API base URL",
"required": false,
"field_type": "text",
"options": null,
"default_value": "https://api.tsubasa.sh/v1"
}
],
"default_model_placeholder": "tsubasa/tsubasa-pro"
},
{
"provider": "V0",
"provider_display_name": "V0",

View file

@ -4791,6 +4791,7 @@ class LlmProviders(str, Enum):
HOSTINGER_ROUTER = "hostinger_router"
ZEROGPU = "zerogpu"
DARKBLOOM = "darkbloom"
TSUBASA = "tsubasa"
META = "meta"
SAIL = "sail"
REKA = "reka"

View file

@ -49845,6 +49845,34 @@
"mode": "embedding",
"output_cost_per_token": 0.0
},
"tsubasa/tsubasa-fast": {
"max_input_tokens": 32768,
"max_output_tokens": 8192,
"input_cost_per_token": 2.0000000000000002e-07,
"output_cost_per_token": 1e-06,
"litellm_provider": "tsubasa",
"mode": "chat",
"supports_system_messages": true,
"supports_function_calling": false,
"supports_parallel_function_calling": false,
"supports_response_schema": false,
"supports_vision": false,
"supports_reasoning": false
},
"tsubasa/tsubasa-pro": {
"max_input_tokens": 32768,
"max_output_tokens": 16384,
"input_cost_per_token": 5e-07,
"output_cost_per_token": 4e-06,
"litellm_provider": "tsubasa",
"mode": "chat",
"supports_system_messages": true,
"supports_function_calling": false,
"supports_parallel_function_calling": false,
"supports_response_schema": false,
"supports_vision": false,
"supports_reasoning": false
},
"together_ai/BAAI/bge-base-en-v1.5": {
"input_cost_per_token": 8e-09,
"litellm_provider": "together_ai",

View file

@ -4280,6 +4280,32 @@
"generateContent": false
}
},
"tsubasa": {
"display_name": "Tsubasa (`tsubasa`)",
"url": "https://docs.litellm.ai/docs/providers/tsubasa",
"endpoints": {
"chat_completions": true,
"responses": true,
"text_completion": true,
"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": true,
"interactions": true,
"systemone": false,
"a2a": true,
"generateContent": true
}
},
"v0": {
"display_name": "V0 (`v0`)",
"url": "https://docs.litellm.ai/docs/providers/v0",

View file

@ -0,0 +1,183 @@
import json
from typing import Final
import httpx
import pytest
from openai import AsyncOpenAI, OpenAI
from litellm import AuthenticationError, LlmProviders, RateLimitError, UnsupportedParamsError, acompletion, completion
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
from litellm.utils import ProviderConfigManager, get_model_info, supports_response_schema
@pytest.mark.parametrize("model_id", ("tsubasa-fast", "tsubasa-pro"))
def test_tsubasa_routes_named_models_and_resolves_credentials(model_id: str, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("TSUBASA_API_KEY", "test-credential")
registration: Final = JSONProviderRegistry.get("tsubasa")
assert registration is not None
model, provider, api_key, api_base = get_llm_provider(model=f"tsubasa/{model_id}")
assert (model, provider, api_key, api_base) == (
model_id,
registration.slug,
"test-credential",
registration.base_url,
)
config: Final = ProviderConfigManager.get_provider_chat_config(model=model_id, provider=LlmProviders.TSUBASA)
assert config is not None
assert (
config.get_complete_url(api_base, api_key, model_id, {}, {}, stream=True)
== f"{registration.base_url}/chat/completions"
)
assert config._get_openai_compatible_provider_info(None, "explicit-credential") == (
registration.base_url,
"explicit-credential",
)
assert config.map_openai_params({"max_completion_tokens": 64}, {}, model_id, False) == {"max_tokens": 64}
info: Final = get_model_info(model=f"tsubasa/{model_id}")
assert info["max_output_tokens"] <= info["max_input_tokens"]
assert info["litellm_provider"] == registration.slug
@pytest.mark.parametrize("model_id", ("tsubasa-fast", "tsubasa-pro"))
@pytest.mark.parametrize("stream", (False, True))
def test_tsubasa_chat_transport(model_id: str, stream: bool) -> None:
registration: Final = JSONProviderRegistry.get("tsubasa")
assert registration is not None
def respond(request: httpx.Request) -> httpx.Response:
assert str(request.url) == f"{registration.base_url}/chat/completions"
assert request.headers["authorization"] == "Bearer test-credential"
assert json.loads(request.content) == {
"model": model_id,
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 32,
**({"stream": True, "stream_options": {"include_usage": True}} if stream else {}),
}
result: Final = {
"id": "chatcmpl-contract",
"created": 1,
"model": model_id,
"object": "chat.completion.chunk" if stream else "chat.completion",
"choices": [
{
"index": 0,
"finish_reason": "stop",
"delta" if stream else "message": {"role": "assistant", "content": "Hello"},
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
if stream:
return httpx.Response(
200,
request=request,
headers={"content-type": "text/event-stream"},
content=f"data: {json.dumps(result)}\n\ndata: [DONE]\n\n",
)
return httpx.Response(200, request=request, json=result)
with httpx.Client(transport=httpx.MockTransport(respond)) as client:
result: Final = completion(
model=f"tsubasa/{model_id}",
messages=[{"role": "user", "content": "Hello"}],
api_key="test-credential",
max_completion_tokens=32,
stream=stream,
stream_options={"include_usage": True} if stream else None,
client=OpenAI(api_key="test-credential", base_url=registration.base_url, http_client=client),
)
if stream:
chunks: Final = tuple(result)
assert "".join(chunk.choices[0].delta.content or "" for chunk in chunks if chunk.choices) == "Hello"
assert chunks[-1].usage.total_tokens == 2
else:
assert result.choices[0].message.content == "Hello"
assert result.usage.total_tokens == 2
@pytest.mark.parametrize("status, error", ((401, AuthenticationError), (429, RateLimitError)))
def test_tsubasa_preserves_upstream_error_status(status: int, error: type[Exception]) -> None:
registration: Final = JSONProviderRegistry.get("tsubasa")
assert registration is not None
def respond(request: httpx.Request) -> httpx.Response:
return httpx.Response(status, request=request, json={"error": {"message": "Request rejected"}})
with httpx.Client(transport=httpx.MockTransport(respond)) as client, pytest.raises(error) as raised:
completion(
model="tsubasa/tsubasa-pro",
messages=[{"role": "user", "content": "Hello"}],
api_key="test-credential",
num_retries=0,
client=OpenAI(api_key="test-credential", base_url=registration.base_url, http_client=client, max_retries=0),
)
assert raised.value.status_code == status
@pytest.mark.parametrize("model_id", ("tsubasa-fast", "tsubasa-pro"))
def test_tsubasa_rejects_tools_while_qualification_is_pending(model_id: str) -> None:
with pytest.raises(UnsupportedParamsError, match="tools"):
completion(
model=f"tsubasa/{model_id}",
messages=[{"role": "user", "content": "Hello"}],
api_key="test-credential",
tools=[{"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object"}}}],
)
async def test_tsubasa_forwards_json_schema_without_declaring_qualified_support() -> None:
registration: Final = JSONProviderRegistry.get("tsubasa")
assert registration is not None
response_format: Final = {
"type": "json_schema",
"json_schema": {
"name": "answer",
"strict": True,
"schema": {
"type": "object",
"properties": {"answer": {"type": "string"}},
"required": ["answer"],
"additionalProperties": False,
},
},
}
def respond(request: httpx.Request) -> httpx.Response:
assert str(request.url) == f"{registration.base_url}/chat/completions"
assert request.headers["authorization"] == "Bearer test-credential"
assert json.loads(request.content) == {
"model": "tsubasa-pro",
"messages": [{"role": "user", "content": "Hello"}],
"response_format": response_format,
}
return httpx.Response(
200,
json={
"id": "chatcmpl-contract",
"created": 1,
"model": "tsubasa-pro",
"object": "chat.completion",
"choices": [
{
"index": 0,
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": '{"answer":"Hello"}',
},
}
],
},
)
async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as client:
result: Final = await acompletion(
model="tsubasa/tsubasa-pro",
messages=[{"role": "user", "content": "Hello"}],
response_format=response_format,
api_key="test-credential",
client=AsyncOpenAI(api_key="test-credential", base_url=registration.base_url, http_client=client),
)
assert json.loads(result.choices[0].message.content) == {"answer": "Hello"}
assert supports_response_schema("tsubasa/tsubasa-pro") is False

View file

@ -0,0 +1,46 @@
from typing import Final
import httpx
from fastapi import FastAPI
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
from litellm.proxy.public_endpoints.public_endpoints import router
from litellm.types.proxy.public_endpoints.public_endpoints import ProviderCreateInfo, SupportedEndpointsResponse
from litellm.utils import get_model_info
async def test_tsubasa_dashboard_fields_resolve_to_a_registered_model() -> None:
app: Final = FastAPI()
app.include_router(router)
async with httpx.AsyncClient(transport=httpx.ASGITransport(app), base_url="http://test") as client:
response: Final = await client.get("/public/providers/fields")
endpoints_response: Final = await client.get("/public/endpoints")
assert response.status_code == 200, response.text
providers: Final = tuple(ProviderCreateInfo.model_validate(entry) for entry in response.json())
matches: Final = tuple(provider for provider in providers if provider.litellm_provider == "tsubasa")
assert len(matches) == 1
provider: Final = matches[0]
assert provider.provider == provider.provider_display_name == "Tsubasa"
assert provider.default_model_placeholder is not None
assert get_model_info(provider.default_model_placeholder)["litellm_provider"] == provider.litellm_provider
registration: Final = JSONProviderRegistry.get(provider.litellm_provider)
assert registration is not None
assert {
field.key: (field.field_type, field.required, field.default_value) for field in provider.credential_fields
} == {
"api_key": ("password", True, None),
"api_base": ("text", False, registration.base_url),
}
assert endpoints_response.status_code == 200, endpoints_response.text
endpoints: Final = SupportedEndpointsResponse.model_validate(endpoints_response.json()).endpoints
assert {
endpoint.key for endpoint in endpoints if any(p.slug == provider.litellm_provider for p in endpoint.providers)
} == {
"chat_completions",
"responses",
"text_completion",
"messages",
"interactions",
"a2a",
"generateContent",
}

View file

@ -11,16 +11,33 @@ import {
resolveLitellmProviderSlug,
} from "./provider_info_helpers";
const PROVIDER_CREATE_FIELDS: { provider: string; provider_display_name: string; litellm_provider: string }[] =
JSON.parse(
readFileSync(resolve(__dirname, "../../../../litellm/proxy/public_endpoints/provider_create_fields.json"), "utf8"),
);
const PROVIDER_CREATE_FIELDS: {
provider: string;
litellm_provider: string;
default_model_placeholder: string;
provider_display_name: string;
}[] = JSON.parse(
readFileSync(resolve(__dirname, "../../../../litellm/proxy/public_endpoints/provider_create_fields.json"), "utf8"),
);
const BUNDLED_MODEL_MAP: Record<string, { litellm_provider?: string }> = JSON.parse(
readFileSync(resolve(__dirname, "../../../../model_prices_and_context_window.json"), "utf8"),
);
describe("provider_info_helpers", () => {
it("resolves the Tsubasa dashboard selection to its models and placeholder", () => {
const provider = PROVIDER_CREATE_FIELDS.find((entry) => entry.litellm_provider === "tsubasa")!;
const models = {
"tsubasa/tsubasa-fast": { litellm_provider: provider.litellm_provider },
"tsubasa/tsubasa-pro": { litellm_provider: provider.litellm_provider },
"openai/gpt-4o": { litellm_provider: "openai" },
};
expect(getProviderModels(provider.provider, models)).toEqual(["tsubasa/tsubasa-fast", "tsubasa/tsubasa-pro"]);
expect(getPlaceholder(provider.provider)).toBe(provider.default_model_placeholder);
expect(getProviderLogoAndName(provider.litellm_provider).displayName).toBe(provider.provider_display_name);
});
it("should identify the upstream LiteLLM Gateway consistently with provider metadata", () => {
const metadata = PROVIDER_CREATE_FIELDS.find((provider) => provider.provider === "LITELLM_PROXY");
expect(resolveLitellmProviderSlug("LITELLM_PROXY")).toBe(metadata?.litellm_provider);
@ -282,6 +299,7 @@ describe("provider_info_helpers", () => {
Providers.PREDIBASE,
Providers.Sail,
Providers.StreamLake,
Providers.Tsubasa,
Providers.WANDB,
Providers.ZAI,
Providers.ZeroGPU,

View file

@ -193,6 +193,7 @@ export enum Providers {
TogetherAI = "TogetherAI",
TOPAZ = "Topaz",
Triton = "Triton",
Tsubasa = "Tsubasa",
TypeSafe = "TypeSafe",
Nace = "Nace",
PARALLEL_AI = "Parallel AI",
@ -330,6 +331,7 @@ export const provider_map: Record<string, string> = {
TogetherAI: "together_ai",
TOPAZ: "topaz",
Triton: "triton",
Tsubasa: "tsubasa",
TypeSafe: "typesafe",
Nace: "nace",
PARALLEL_AI: "parallel_ai",
@ -522,6 +524,7 @@ const providerPlaceholderMap: Partial<Record<Providers, string>> = {
[Providers.Bespoke]: "bespoke/nimble",
[Providers.Laya]: "laya/english",
[Providers.Snowflake]: "snowflake/mistral-7b",
[Providers.Tsubasa]: "tsubasa/tsubasa-pro",
[Providers.StrandsDecider]: "strands_decider/strands-decider-2B-hobson-v19",
[Providers.Tencent]: "tencent/deepseek-v4-pro",
[Providers.TypeSafe]: "typesafe/jev-latest",