From a8f3304405633328e0b52f9490998264d76ecb96 Mon Sep 17 00:00:00 2001 From: Daniel Dafoe Date: Wed, 26 Aug 2026 22:50:36 -0700 Subject: [PATCH 1/2] feat(aquaduck): add OpenAI-compatible provider Co-authored-by: Cursor --- README.md | 1 + litellm/constants.py | 2 + litellm/llms/openai_like/providers.json | 9 + ...odel_prices_and_context_window_backup.json | 60 ++++ .../provider_endpoints_support_backup.json | 17 ++ .../provider_create_fields.json | 28 ++ litellm/types/utils.py | 1 + model_prices_and_context_window.json | 60 ++++ provider_endpoints_support.json | 17 ++ .../openai_like/test_aquaduck_provider.py | 281 ++++++++++++++++++ .../components/provider_info_helpers.test.tsx | 13 + .../src/components/provider_info_helpers.tsx | 3 + 12 files changed, 492 insertions(+) create mode 100644 tests/test_litellm/llms/openai_like/test_aquaduck_provider.py diff --git a/README.md b/README.md index 68aaa09ec98..b1739333ee0 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,7 @@ curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ | [Anthropic (`anthropic`)](https://docs.litellm.ai/docs/providers/anthropic) | ✅ | ✅ | ✅ | | | | | | ✅ | | | [Anthropic Text (`anthropic_text`)](https://docs.litellm.ai/docs/providers/anthropic) | ✅ | ✅ | ✅ | | | | | | ✅ | | | [Anyscale](https://docs.litellm.ai/docs/providers/anyscale) | ✅ | ✅ | ✅ | | | | | | | | +| [Aquaduck (`aquaduck`)](https://docs.litellm.ai/docs/providers/aquaduck) | ✅ | | | | | | | | | | | [AssemblyAI (`assemblyai`)](https://docs.litellm.ai/docs/pass_through/assembly_ai) | ✅ | ✅ | ✅ | | | ✅ | | | | | | [Auto Router (`auto_router`)](https://docs.litellm.ai/docs/proxy/auto_routing) | ✅ | ✅ | ✅ | | | | | | | | | [AWS - Bedrock (`bedrock`)](https://docs.litellm.ai/docs/providers/bedrock) | ✅ | ✅ | ✅ | ✅ | | | | | | ✅ | diff --git a/litellm/constants.py b/litellm/constants.py index 816397ef047..1682df6f482 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -792,6 +792,7 @@ openai_compatible_endpoints: Final[list] = [ "https://api.meta.ai/v1", "https://api.cognition.ai/v1", "https://api.scx.ai/v1", + "https://aqi.aquaduck.ai/v1", ] @@ -861,6 +862,7 @@ openai_compatible_providers: Final[list] = [ "meta", # Meta Model API (Muse Spark) - JSON-configured provider "cognition", "scx-ai", + "aquaduck", ] openai_text_completion_compatible_providers: Final[list] = [ # providers that support `/v1/completions` "together_ai", diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index a458a209ea9..d11a5011082 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -200,5 +200,14 @@ "temperature_max": 1.99 }, "supported_endpoints": ["/v1/chat/completions"] + }, + "aquaduck": { + "base_url": "https://aqi.aquaduck.ai/v1", + "api_key_env": "AQUADUCK_API_KEY", + "api_base_env": "AQUADUCK_API_BASE", + "param_mappings": { + "max_completion_tokens": "max_tokens" + }, + "supported_endpoints": ["/v1/chat/completions"] } } diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index dd367e875de..a75d0c12f69 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -2816,6 +2816,66 @@ "bedrock_converse_supports_strict_tools": false, "prompt_cache_min_tokens": 1024 }, + "aquaduck/zai-org/glm-4.7-flash": { + "max_tokens": 202752, + "max_input_tokens": 202752, + "max_output_tokens": 202752, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 2e-07, + "litellm_provider": "aquaduck", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": false, + "source": "https://aquaduck.ai" + }, + "aquaduck/qwen/qwen3-14b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 1.2e-07, + "litellm_provider": "aquaduck", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": false, + "source": "https://aquaduck.ai" + }, + "aquaduck/qwen/qwen3.8-27b": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 1.5e-06, + "litellm_provider": "aquaduck", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": true, + "source": "https://aquaduck.ai" + }, + "aquaduck/google/gemma-4-26b-a4b-it": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 2e-07, + "litellm_provider": "aquaduck", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": true, + "source": "https://aquaduck.ai" + }, + "aquaduck/ornith-ai/ornith-1.5-9b": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 1e-07, + "litellm_provider": "aquaduck", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": false, + "source": "https://aquaduck.ai" + }, "assemblyai/best": { "input_cost_per_second": 3.333e-05, "litellm_provider": "assemblyai", diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index 86c14fb4cd8..b5e78d104a3 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -193,6 +193,23 @@ "a2a": false } }, + "aquaduck": { + "display_name": "Aquaduck (`aquaduck`)", + "url": "https://docs.litellm.ai/docs/providers/aquaduck", + "endpoints": { + "chat_completions": true, + "messages": false, + "responses": false, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false + } + }, "assemblyai": { "display_name": "AssemblyAI (`assemblyai`)", "url": "https://docs.litellm.ai/docs/pass_through/assembly_ai", diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 4652719a23b..276842678d0 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -101,6 +101,34 @@ ], "default_model_placeholder": "gpt-3.5-turbo" }, + { + "provider": "Aquaduck", + "provider_display_name": "Aquaduck AI", + "litellm_provider": "aquaduck", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "https://aqi.aquaduck.ai/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": "aquaduck/zai-org/glm-4.7-flash" + }, { "provider": "Bedrock", "provider_display_name": "Amazon Bedrock", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 6e245b0742e..4a2f1d5a27f 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3829,6 +3829,7 @@ class LlmProviders(str, Enum): COGNITION = "cognition" SCX_AI = "scx-ai" DARKBLOOM = "darkbloom" + AQUADUCK = "aquaduck" META = "meta" LITELLM_AGENT = "litellm_agent" CURSOR = "cursor" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index dd367e875de..a75d0c12f69 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -2816,6 +2816,66 @@ "bedrock_converse_supports_strict_tools": false, "prompt_cache_min_tokens": 1024 }, + "aquaduck/zai-org/glm-4.7-flash": { + "max_tokens": 202752, + "max_input_tokens": 202752, + "max_output_tokens": 202752, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 2e-07, + "litellm_provider": "aquaduck", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": false, + "source": "https://aquaduck.ai" + }, + "aquaduck/qwen/qwen3-14b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 1.2e-07, + "litellm_provider": "aquaduck", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": false, + "source": "https://aquaduck.ai" + }, + "aquaduck/qwen/qwen3.8-27b": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 1.5e-06, + "litellm_provider": "aquaduck", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": true, + "source": "https://aquaduck.ai" + }, + "aquaduck/google/gemma-4-26b-a4b-it": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 2e-07, + "litellm_provider": "aquaduck", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": true, + "source": "https://aquaduck.ai" + }, + "aquaduck/ornith-ai/ornith-1.5-9b": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 1e-07, + "litellm_provider": "aquaduck", + "mode": "chat", + "supports_function_calling": true, + "supports_vision": false, + "source": "https://aquaduck.ai" + }, "assemblyai/best": { "input_cost_per_second": 3.333e-05, "litellm_provider": "assemblyai", diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 1d8d374c2c4..8552c39e528 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -210,6 +210,23 @@ "a2a": false } }, + "aquaduck": { + "display_name": "Aquaduck (`aquaduck`)", + "url": "https://docs.litellm.ai/docs/providers/aquaduck", + "endpoints": { + "chat_completions": true, + "messages": false, + "responses": false, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false + } + }, "assemblyai": { "display_name": "AssemblyAI (`assemblyai`)", "url": "https://docs.litellm.ai/docs/pass_through/assembly_ai", diff --git a/tests/test_litellm/llms/openai_like/test_aquaduck_provider.py b/tests/test_litellm/llms/openai_like/test_aquaduck_provider.py new file mode 100644 index 00000000000..e65991d311a --- /dev/null +++ b/tests/test_litellm/llms/openai_like/test_aquaduck_provider.py @@ -0,0 +1,281 @@ +""" +Tests for the Aquaduck provider identity. + +Aquaduck serves an OpenAI-compatible /v1/chat/completions surface at +https://aqi.aquaduck.ai/v1. It must resolve to its own `aquaduck` provider so +OpenAI-specific pricing and provider-level reporting never apply to its traffic. +""" + +import json +from pathlib import Path + +import pytest + +import litellm + + +class TestAquaduckProviderIdentity: + def test_aquaduck_is_a_registered_provider(self): + from litellm import LlmProviders + + assert LlmProviders.AQUADUCK.value == "aquaduck" + assert "aquaduck" in litellm.provider_list + + def test_aquaduck_json_config(self): + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + aquaduck = JSONProviderRegistry.get("aquaduck") + assert aquaduck is not None + assert aquaduck.base_url == "https://aqi.aquaduck.ai/v1" + assert aquaduck.api_key_env == "AQUADUCK_API_KEY" + assert aquaduck.api_base_env == "AQUADUCK_API_BASE" + assert aquaduck.param_mappings.get("max_completion_tokens") == "max_tokens" + assert aquaduck.supported_endpoints == ["/v1/chat/completions"] + + def test_aquaduck_in_openai_compatible_providers(self): + from litellm.constants import openai_compatible_endpoints, openai_compatible_providers + + assert "aquaduck" in openai_compatible_providers + assert "https://aqi.aquaduck.ai/v1" in openai_compatible_endpoints + + def test_prefixed_model_resolves_to_aquaduck_not_openai(self): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + model, provider, _, api_base = get_llm_provider( + model="aquaduck/zai-org/glm-4.7-flash", + custom_llm_provider=None, + api_base=None, + api_key=None, + ) + + assert model == "zai-org/glm-4.7-flash" + assert provider == "aquaduck" + assert api_base == "https://aqi.aquaduck.ai/v1" + + def test_explicit_api_base_and_key_win(self): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + _, provider, api_key, api_base = get_llm_provider( + model="aquaduck/zai-org/glm-4.7-flash", + custom_llm_provider=None, + api_base="https://aquaduck.internal.example/v1", + api_key="sk-test", + ) + + assert provider == "aquaduck" + assert api_base == "https://aquaduck.internal.example/v1" + assert api_key == "sk-test" + + def test_api_base_autodetects_aquaduck(self, monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("AQUADUCK_API_KEY", "sk-aquaduck-env") + + _, provider, api_key, api_base = get_llm_provider( + model="zai-org/glm-4.7-flash", + custom_llm_provider=None, + api_base="https://aqi.aquaduck.ai/v1", + api_key=None, + ) + + assert provider == "aquaduck" + assert api_base == "https://aqi.aquaduck.ai/v1" + assert api_key == "sk-aquaduck-env" + + def test_autodetected_api_base_keeps_the_caller_api_key(self, monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("AQUADUCK_API_KEY", "sk-aquaduck-env") + + _, provider, api_key, _ = get_llm_provider( + model="zai-org/glm-4.7-flash", + custom_llm_provider=None, + api_base="https://aqi.aquaduck.ai/v1", + api_key="sk-aquaduck-caller", + ) + + assert provider == "aquaduck" + assert api_key == "sk-aquaduck-caller" + + def test_env_api_key_is_read_from_aquaduck_variable(self, monkeypatch: pytest.MonkeyPatch): + from litellm.llms.openai_like.dynamic_config import create_config_class + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.setenv("AQUADUCK_API_KEY", "sk-aquaduck-env") + + provider = JSONProviderRegistry.get("aquaduck") + assert provider is not None + + api_base, api_key = create_config_class(provider)()._get_openai_compatible_provider_info(None, None) + assert api_base == "https://aqi.aquaduck.ai/v1" + assert api_key == "sk-aquaduck-env" + + def test_max_completion_tokens_mapped(self): + from litellm.llms.openai_like.dynamic_config import create_config_class + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + provider = JSONProviderRegistry.get("aquaduck") + assert provider is not None + config = create_config_class(provider)() + + optional_params = config.map_openai_params( + non_default_params={"max_completion_tokens": 256}, + optional_params={}, + model="zai-org/glm-4.7-flash", + drop_params=False, + ) + assert optional_params["max_tokens"] == 256 + assert "max_completion_tokens" not in optional_params + + +class TestAquaduckCostTracking: + AQUADUCK_MODELS = ( + "aquaduck/zai-org/glm-4.7-flash", + "aquaduck/qwen/qwen3-14b", + "aquaduck/qwen/qwen3.8-27b", + "aquaduck/google/gemma-4-26b-a4b-it", + "aquaduck/ornith-ai/ornith-1.5-9b", + ) + VISION_MODELS = ( + "aquaduck/qwen/qwen3.8-27b", + "aquaduck/google/gemma-4-26b-a4b-it", + ) + + @pytest.fixture(autouse=True) + def _use_local_model_cost_map(self, monkeypatch: pytest.MonkeyPatch): + original_model_cost = litellm.model_cost + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + litellm.get_model_info.cache_clear() + try: + yield + finally: + litellm.model_cost = original_model_cost + litellm.get_model_info.cache_clear() + + @staticmethod + def _load(path_parts): + json_path = Path(__file__).parents[4].joinpath(*path_parts) + with open(json_path) as f: + return json.load(f) + + @pytest.mark.parametrize( + "model, input_cost, output_cost", + [ + ("aquaduck/zai-org/glm-4.7-flash", 5e-08, 2e-07), + ("aquaduck/qwen/qwen3-14b", 5e-08, 1.2e-07), + ("aquaduck/qwen/qwen3.8-27b", 2e-07, 1.5e-06), + ("aquaduck/google/gemma-4-26b-a4b-it", 5e-08, 2e-07), + ("aquaduck/ornith-ai/ornith-1.5-9b", 5e-08, 1e-07), + ], + ) + def test_cost_map_entries(self, model: str, input_cost: float, output_cost: float): + info = litellm.get_model_info(model=model) + + assert info["litellm_provider"] == "aquaduck" + assert info["mode"] == "chat" + assert info["input_cost_per_token"] == input_cost + assert info["output_cost_per_token"] == output_cost + assert info.get("supports_vision", False) is (model in self.VISION_MODELS) + + def test_aquaduck_models_synced_to_backup(self): + model_cost = self._load(("model_prices_and_context_window.json",)) + backup = self._load(("litellm", "model_prices_and_context_window_backup.json")) + for model in self.AQUADUCK_MODELS: + assert model in backup, f"{model} missing from backup json" + assert backup[model] == model_cost[model], f"{model} differs between root and backup json" + + @pytest.mark.parametrize( + "model, expected_prompt_cost, expected_completion_cost", + [ + ("aquaduck/zai-org/glm-4.7-flash", 0.05, 0.2), + ("aquaduck/qwen/qwen3.8-27b", 0.2, 1.5), + ], + ) + def test_cost_per_million_tokens( + self, model: str, expected_prompt_cost: float, expected_completion_cost: float + ): + from litellm.cost_calculator import cost_per_token + + prompt_cost, completion_cost = cost_per_token( + model=model, + prompt_tokens=1_000_000, + completion_tokens=1_000_000, + custom_llm_provider="aquaduck", + ) + + assert prompt_cost == pytest.approx(expected_prompt_cost) + assert completion_cost == pytest.approx(expected_completion_cost) + + def test_supported_endpoints_matrix(self): + matrix = json.loads((Path(litellm.__file__).parent / "provider_endpoints_support_backup.json").read_text()) + + endpoints = matrix["providers"]["aquaduck"]["endpoints"] + assert endpoints["chat_completions"] is True + assert endpoints["messages"] is False + assert endpoints["responses"] is False + assert endpoints["embeddings"] is False + + +class TestAquaduckRouting: + @pytest.fixture(autouse=True) + def _use_local_model_cost_map(self, monkeypatch: pytest.MonkeyPatch): + original_model_cost = litellm.model_cost + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + litellm.get_model_info.cache_clear() + try: + yield + finally: + litellm.model_cost = original_model_cost + litellm.get_model_info.cache_clear() + + @pytest.mark.asyncio + async def test_router_spend_is_attributed_to_aquaduck_pricing(self): + from litellm import Router + + router = Router( + model_list=[ + { + "model_name": "glm-flash", + "litellm_params": { + "model": "aquaduck/zai-org/glm-4.7-flash", + "api_key": "sk-test", + }, + } + ] + ) + + response = await router.acompletion( + model="glm-flash", + messages=[{"role": "user", "content": "hi"}], + mock_response="hello from aquaduck", + ) + + usage = response.usage + expected = usage.prompt_tokens * 5e-08 + usage.completion_tokens * 2e-07 + assert response._hidden_params["response_cost"] == pytest.approx(expected) + + +class TestAquaduckDashboardRegistration: + @staticmethod + def _provider_create_fields(): + path = Path(litellm.__file__).parent / "proxy" / "public_endpoints" / "provider_create_fields.json" + with open(path) as f: + return json.load(f) + + def test_aquaduck_is_selectable_in_the_add_model_form(self): + entries = [e for e in self._provider_create_fields() if e["litellm_provider"] == "aquaduck"] + assert len(entries) == 1, "aquaduck must appear exactly once in provider_create_fields.json" + + entry = entries[0] + assert entry["provider"] == "Aquaduck" + assert entry["provider_display_name"] == "Aquaduck AI" + assert entry["default_model_placeholder"].startswith("aquaduck/") + + fields = {f["key"]: f for f in entry["credential_fields"]} + assert fields["api_key"]["required"] is True + assert fields["api_key"]["field_type"] == "password" + assert fields["api_base"]["required"] is False + assert fields["api_base"]["placeholder"] == "https://aqi.aquaduck.ai/v1" 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 dfc737ddd45..6917deb8f59 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx @@ -73,6 +73,14 @@ describe("provider_info_helpers", () => { expect(fromEnumKey.logo).toBe(providerLogoMap[Providers.SCX_AI]); }); + it("should map aquaduck slug and Aquaduck enum key to the Aquaduck display name", () => { + const fromSlug = getProviderLogoAndName("aquaduck"); + expect(fromSlug.displayName).toBe(Providers.Aquaduck); + + const fromEnumKey = getProviderLogoAndName("Aquaduck"); + expect(fromEnumKey.displayName).toBe(Providers.Aquaduck); + }); + it("should map bedrock_mantle slug to Bedrock Mantle display name and logo", () => { const result = getProviderLogoAndName("bedrock_mantle"); expect(result.displayName).toBe(Providers.BedrockMantle); @@ -146,6 +154,7 @@ describe("provider_info_helpers", () => { it("should map every provider to a bundled logo except the known logoless set, never a raw /ui/assets path", () => { const knownLogolessProviders = [ Providers.AUTO_ROUTER, + Providers.Aquaduck, Providers.BYTEZ, Providers.CLARIFAI, Providers.Cognition, @@ -195,6 +204,10 @@ describe("provider_info_helpers", () => { expect(getPlaceholder(Providers.SCX_AI)).toBe("scx-ai/GLM-5.2"); }); + it("should return an aquaduck model placeholder for Aquaduck provider", () => { + expect(getPlaceholder(Providers.Aquaduck)).toBe("aquaduck/zai-org/glm-4.7-flash"); + }); + it("should return claude-3-opus placeholder for Anthropic provider", () => { expect(getPlaceholder(Providers.Anthropic)).toBe("claude-3-opus"); }); diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index c97bb8ede3d..733b8ed7a84 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -72,6 +72,7 @@ export enum Providers { AIOHTTP_OPENAI = "Aiohttp Openai", Anthropic = "Anthropic", ANTHROPIC_TEXT = "Anthropic Text", + Aquaduck = "Aquaduck AI", AssemblyAI = "AssemblyAI", AUTO_ROUTER = "Auto Router", Bedrock = "Amazon Bedrock", @@ -184,6 +185,7 @@ export const provider_map: Record = { AIOHTTP_OPENAI: "aiohttp_openai", Anthropic: "anthropic", ANTHROPIC_TEXT: "anthropic_text", + Aquaduck: "aquaduck", AssemblyAI: "assemblyai", AUTO_ROUTER: "auto_router", Azure: "azure", @@ -412,6 +414,7 @@ export const getProviderLogoAndName = (providerValue: string): { logo: string; d const providerPlaceholderMap: Partial> = { [Providers.AIML]: "aiml/flux-pro/v1.1", [Providers.Anthropic]: "claude-3-opus", + [Providers.Aquaduck]: "aquaduck/zai-org/glm-4.7-flash", [Providers.Azure]: "my-deployment", [Providers.Azure_AI_Studio]: "azure_ai/command-r-plus", [Providers.Bedrock]: "claude-3-opus", From ad7b956e79b53a9e8d7a8f297747bc60880596f3 Mon Sep 17 00:00:00 2001 From: Daniel Dafoe Date: Wed, 26 Aug 2026 23:31:23 -0700 Subject: [PATCH 2/2] fix(aquaduck): use monkeypatch for model_cost in tests Direct litellm.model_cost assignment tripped TQ005 in CI. Switch the fixtures to monkeypatch.setattr so the global is restored automatically. Co-authored-by: Cursor --- .../openai_like/test_aquaduck_provider.py | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/test_litellm/llms/openai_like/test_aquaduck_provider.py b/tests/test_litellm/llms/openai_like/test_aquaduck_provider.py index e65991d311a..169c0134cea 100644 --- a/tests/test_litellm/llms/openai_like/test_aquaduck_provider.py +++ b/tests/test_litellm/llms/openai_like/test_aquaduck_provider.py @@ -144,15 +144,11 @@ class TestAquaduckCostTracking: @pytest.fixture(autouse=True) def _use_local_model_cost_map(self, monkeypatch: pytest.MonkeyPatch): - original_model_cost = litellm.model_cost monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + litellm.get_model_info.cache_clear() + yield litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() @staticmethod def _load(path_parts): @@ -221,15 +217,11 @@ class TestAquaduckCostTracking: class TestAquaduckRouting: @pytest.fixture(autouse=True) def _use_local_model_cost_map(self, monkeypatch: pytest.MonkeyPatch): - original_model_cost = litellm.model_cost monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + litellm.get_model_info.cache_clear() + yield litellm.get_model_info.cache_clear() - try: - yield - finally: - litellm.model_cost = original_model_cost - litellm.get_model_info.cache_clear() @pytest.mark.asyncio async def test_router_spend_is_attributed_to_aquaduck_pricing(self):