From 82bcc602c386c66f1d3f1a2d251f589d10ffff9e Mon Sep 17 00:00:00 2001 From: cenab Date: Sun, 27 Sep 2026 02:52:05 -0600 Subject: [PATCH 1/2] feat(providers): add Tsubasa inference provider --- docs/my-website/docs/providers/tsubasa.md | 46 ++++++++++ litellm/constants.py | 1 + litellm/llms/openai_like/providers.json | 7 ++ ...odel_prices_and_context_window_backup.json | 28 ++++++ litellm/types/utils.py | 1 + model_prices_and_context_window.json | 28 ++++++ .../llms/openai_like/test_tsubasa_provider.py | 87 +++++++++++++++++++ 7 files changed, 198 insertions(+) create mode 100644 docs/my-website/docs/providers/tsubasa.md create mode 100644 tests/unit/llms/openai_like/test_tsubasa_provider.py diff --git a/docs/my-website/docs/providers/tsubasa.md b/docs/my-website/docs/providers/tsubasa.md new file mode 100644 index 00000000000..3eb8369d501 --- /dev/null +++ b/docs/my-website/docs/providers/tsubasa.md @@ -0,0 +1,46 @@ +# Tsubasa + +Tsubasa uses LiteLLM's existing OpenAI-compatible Chat Completions transport + +Tsubasa is currently in website-only mode. API access is unavailable, and live +tool-calling and structured-output qualification is pending. These examples are +for use after the service is enabled + +## 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 diff --git a/litellm/constants.py b/litellm/constants.py index a292b654778..c9ac96599bc 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1019,6 +1019,7 @@ openai_compatible_providers: Final[list] = [ "ragflow", "pinstripes", # Pinstripes - JSON-configured provider "darkbloom", + "tsubasa", "meta", # Meta Model API (Muse Spark) - JSON-configured provider "cognition", "scx-ai", diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index ae09b48bd1e..54bed3ba00a 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -206,5 +206,12 @@ "api_key_env": "SAIL_API_KEY", "api_base_env": "SAIL_API_BASE", "supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"] + }, + "tsubasa": { + "base_url": "https://api.tsubasa.sh/v1", + "api_key_env": "TSUBASA_API_KEY", + "param_mappings": { + "max_completion_tokens": "max_tokens" + } } } diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index b36d84ea027..d9c91b36eb3 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -77910,5 +77910,33 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true + }, + "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 } } diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 8862df9dc22..b8f8dc94ddd 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4140,6 +4140,7 @@ class LlmProviders(str, Enum): COGNITION = "cognition" SCX_AI = "scx-ai" DARKBLOOM = "darkbloom" + TSUBASA = "tsubasa" META = "meta" SAIL = "sail" LITELLM_AGENT = "litellm_agent" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index b36d84ea027..d9c91b36eb3 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -77910,5 +77910,33 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true + }, + "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 } } diff --git a/tests/unit/llms/openai_like/test_tsubasa_provider.py b/tests/unit/llms/openai_like/test_tsubasa_provider.py new file mode 100644 index 00000000000..efb62ff97bc --- /dev/null +++ b/tests/unit/llms/openai_like/test_tsubasa_provider.py @@ -0,0 +1,87 @@ +import json +from typing import Final + +import httpx +import pytest + +from litellm import LlmProviders, 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 + + +@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, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("TSUBASA_API_KEY", "test-credential") + registration = JSONProviderRegistry.get("tsubasa") + assert registration is not None + + def send(_client: httpx.Client, request: httpx.Request, **_kwargs: object) -> httpx.Response: + assert str(request.url) == f"{registration.base_url}/chat/completions" + assert request.headers["authorization"] == "Bearer test-credential" + body = json.loads(request.content) + assert body["model"] == model_id + assert body["messages"] == [{"role": "user", "content": "Hello"}] + assert body["max_tokens"] == 32 + assert set(body) <= {"model", "messages", "max_tokens", "stream", "stream_options"} + result = { + "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) + + monkeypatch.setattr(httpx.Client, "send", send) + result = completion( + model=f"tsubasa/{model_id}", messages=[{"role": "user", "content": "Hello"}], max_tokens=32, stream=stream + ) + if stream: + assert "".join(chunk.choices[0].delta.content or "" for chunk in result if chunk.choices) == "Hello" + else: + assert result.choices[0].message.content == "Hello" + assert result.usage.total_tokens == 2 From 3839daa7d3c222febf47175c6b97b4ec6838d820 Mon Sep 17 00:00:00 2001 From: cenab Date: Sun, 27 Sep 2026 16:58:58 -0600 Subject: [PATCH 2/2] feat(providers): expose Tsubasa in the dashboard and endpoint catalog --- .../provider_endpoints_support_backup.json | 7 + .../provider_create_fields.json | 28 ++++ provider_endpoints_support.json | 7 + .../llms/openai_like/test_tsubasa_provider.py | 138 +++++++++++++++--- .../public_endpoints/test_provider_fields.py | 38 +++++ .../components/provider_info_helpers.test.tsx | 15 ++ .../src/components/provider_info_helpers.tsx | 3 + 7 files changed, 215 insertions(+), 21 deletions(-) create mode 100644 tests/unit/proxy/public_endpoints/test_provider_fields.py diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index 1fcb7600a5e..6431983029c 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -2227,6 +2227,13 @@ "interactions": true } }, + "tsubasa": { + "display_name": "Tsubasa (`tsubasa`)", + "url": "https://docs.litellm.ai/docs/providers/tsubasa", + "endpoints": { + "chat_completions": true + } + }, "v0": { "display_name": "V0 (`v0`)", "url": "https://docs.litellm.ai/docs/providers/v0", diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 8bd7ed81583..ecc2b8c7ebd 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -3207,6 +3207,34 @@ ], "default_model_placeholder": "gpt-3.5-turbo" }, + { + "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", diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 790a050a878..0d804b18d42 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -2573,6 +2573,13 @@ "interactions": true } }, + "tsubasa": { + "display_name": "Tsubasa (`tsubasa`)", + "url": "https://docs.litellm.ai/docs/providers/tsubasa", + "endpoints": { + "chat_completions": true + } + }, "v0": { "display_name": "V0 (`v0`)", "url": "https://docs.litellm.ai/docs/providers/v0", diff --git a/tests/unit/llms/openai_like/test_tsubasa_provider.py b/tests/unit/llms/openai_like/test_tsubasa_provider.py index efb62ff97bc..1169f03b9f4 100644 --- a/tests/unit/llms/openai_like/test_tsubasa_provider.py +++ b/tests/unit/llms/openai_like/test_tsubasa_provider.py @@ -3,11 +3,12 @@ from typing import Final import httpx import pytest +from openai import AsyncOpenAI, OpenAI -from litellm import LlmProviders, completion +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 +from litellm.utils import ProviderConfigManager, get_model_info, supports_response_schema @pytest.mark.parametrize("model_id", ("tsubasa-fast", "tsubasa-pro")) @@ -40,20 +41,20 @@ def test_tsubasa_routes_named_models_and_resolves_credentials(model_id: str, mon @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, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("TSUBASA_API_KEY", "test-credential") - registration = JSONProviderRegistry.get("tsubasa") +def test_tsubasa_chat_transport(model_id: str, stream: bool) -> None: + registration: Final = JSONProviderRegistry.get("tsubasa") assert registration is not None - def send(_client: httpx.Client, request: httpx.Request, **_kwargs: object) -> httpx.Response: + def respond(request: httpx.Request) -> httpx.Response: assert str(request.url) == f"{registration.base_url}/chat/completions" assert request.headers["authorization"] == "Bearer test-credential" - body = json.loads(request.content) - assert body["model"] == model_id - assert body["messages"] == [{"role": "user", "content": "Hello"}] - assert body["max_tokens"] == 32 - assert set(body) <= {"model", "messages", "max_tokens", "stream", "stream_options"} - result = { + 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, @@ -76,12 +77,107 @@ def test_tsubasa_chat_transport(model_id: str, stream: bool, monkeypatch: pytest ) return httpx.Response(200, request=request, json=result) - monkeypatch.setattr(httpx.Client, "send", send) - result = completion( - model=f"tsubasa/{model_id}", messages=[{"role": "user", "content": "Hello"}], max_tokens=32, stream=stream - ) - if stream: - assert "".join(chunk.choices[0].delta.content or "" for chunk in result if chunk.choices) == "Hello" - else: - assert result.choices[0].message.content == "Hello" - assert result.usage.total_tokens == 2 + 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 diff --git a/tests/unit/proxy/public_endpoints/test_provider_fields.py b/tests/unit/proxy/public_endpoints/test_provider_fields.py new file mode 100644 index 00000000000..5909bef0b58 --- /dev/null +++ b/tests/unit/proxy/public_endpoints/test_provider_fields.py @@ -0,0 +1,38 @@ +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"] 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 29ce2d4865a..69a88332646 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import providerFields from "../../../../litellm/proxy/public_endpoints/provider_create_fields.json"; import { Providers, getPlaceholder, @@ -9,6 +10,19 @@ import { } from "./provider_info_helpers"; describe("provider_info_helpers", () => { + it("resolves the Tsubasa dashboard selection to its models and placeholder", () => { + const provider = providerFields.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); + }); + describe("getProviderLogoAndName", () => { it("should return empty logo and dash display name when providerValue is empty", () => { const result = getProviderLogoAndName(""); @@ -195,6 +209,7 @@ describe("provider_info_helpers", () => { Providers.PG_VECTOR, Providers.PREDIBASE, Providers.Sail, + Providers.Tsubasa, Providers.WANDB, Providers.ZAI, ]; diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index b0e33338bab..07b986b2bc1 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -170,6 +170,7 @@ export enum Providers { TogetherAI = "TogetherAI", TOPAZ = "Topaz", Triton = "Triton", + Tsubasa = "Tsubasa", V0 = "V0", VERCEL_AI_GATEWAY = "Vercel Ai Gateway", Vertex_AI = "Vertex AI (Anthropic, Gemini, etc.)", @@ -289,6 +290,7 @@ export const provider_map: Record = { TogetherAI: "together_ai", TOPAZ: "topaz", Triton: "triton", + Tsubasa: "tsubasa", V0: "v0", VERCEL_AI_GATEWAY: "vercel_ai_gateway", Vertex_AI: "vertex_ai", @@ -453,6 +455,7 @@ const providerPlaceholderMap: Partial> = { [Providers.Sail]: "sail/openai/gpt-oss-120b", [Providers.SCX_AI]: "scx-ai/GLM-5.2", [Providers.Snowflake]: "snowflake/mistral-7b", + [Providers.Tsubasa]: "tsubasa/tsubasa-pro", [Providers.Vertex_AI]: "gemini-pro", [Providers.VolcEngine]: "volcengine/", [Providers.Voyage]: "voyage/",